@savvifi/meridian-aion-web 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/dist/index.d.ts +5 -0
- package/dist/index.js +11 -0
- package/dist/not-found.d.ts +13 -0
- package/dist/not-found.js +22 -0
- package/dist/render-client.d.ts +14 -0
- package/dist/render-client.js +15 -0
- package/dist/render-server.d.ts +35 -0
- package/dist/render-server.js +74 -0
- package/dist/savvi-skin.d.ts +8 -0
- package/dist/savvi-skin.js +29 -0
- package/package.json +65 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { renderMeridianEntityView, type RenderMeridianEntityViewInput, type RenderMeridianEntityViewResult, type MeridianDehydratedView, type EntityCaller, } from "./render-server.js";
|
|
2
|
+
export { MeridianEntityView, type MeridianEntityViewProps } from "./render-client.js";
|
|
3
|
+
export { savviSkin } from "./savvi-skin.js";
|
|
4
|
+
export { NotFoundSignal, isNotFoundSignal } from "./not-found.js";
|
|
5
|
+
export { createEntityOpsInvoker } from "@savvifi/meridian-aion-projection";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// @savvifi/meridian-aion-web — the aion/web <-> meridian-ux render seam.
|
|
2
|
+
//
|
|
3
|
+
// Server: renderMeridianEntityView (parse route -> load composition -> project ->
|
|
4
|
+
// pre-fetch page-0 -> render + dehydrate). Client: MeridianEntityView (hydrate).
|
|
5
|
+
// Plus savviSkin (the brand Theme proto) and createEntityOpsInvoker (build a
|
|
6
|
+
// meridian RpcInvoker over a tRPC caller/client's entities.ops).
|
|
7
|
+
export { renderMeridianEntityView, } from "./render-server.js";
|
|
8
|
+
export { MeridianEntityView } from "./render-client.js";
|
|
9
|
+
export { savviSkin } from "./savvi-skin.js";
|
|
10
|
+
export { NotFoundSignal, isNotFoundSignal } from "./not-found.js";
|
|
11
|
+
export { createEntityOpsInvoker } from "@savvifi/meridian-aion-projection";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by renderMeridianEntityView when a route is not a valid entity route or
|
|
3
|
+
* has no installed composition — so the SSR host can catch it and serve a 404
|
|
4
|
+
* (cleaner than branching on a null node). Pass `notFound: "return"` to get
|
|
5
|
+
* `{ node: null }` instead of a throw.
|
|
6
|
+
*/
|
|
7
|
+
export declare class NotFoundSignal extends Error {
|
|
8
|
+
readonly pathname?: string | undefined;
|
|
9
|
+
/** Discriminant so hosts can detect it across module/realm boundaries. */
|
|
10
|
+
readonly kind: "meridian-not-found";
|
|
11
|
+
constructor(pathname?: string | undefined);
|
|
12
|
+
}
|
|
13
|
+
export declare function isNotFoundSignal(error: unknown): error is NotFoundSignal;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thrown by renderMeridianEntityView when a route is not a valid entity route or
|
|
3
|
+
* has no installed composition — so the SSR host can catch it and serve a 404
|
|
4
|
+
* (cleaner than branching on a null node). Pass `notFound: "return"` to get
|
|
5
|
+
* `{ node: null }` instead of a throw.
|
|
6
|
+
*/
|
|
7
|
+
export class NotFoundSignal extends Error {
|
|
8
|
+
pathname;
|
|
9
|
+
/** Discriminant so hosts can detect it across module/realm boundaries. */
|
|
10
|
+
kind = "meridian-not-found";
|
|
11
|
+
constructor(pathname) {
|
|
12
|
+
super(pathname ? `No meridian entity view for route: ${pathname}` : "No meridian entity view for route");
|
|
13
|
+
this.pathname = pathname;
|
|
14
|
+
this.name = "NotFoundSignal";
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function isNotFoundSignal(error) {
|
|
18
|
+
return (error instanceof NotFoundSignal ||
|
|
19
|
+
(typeof error === "object" &&
|
|
20
|
+
error !== null &&
|
|
21
|
+
error.kind === "meridian-not-found"));
|
|
22
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import type { RpcInvoker } from "@savvifi/meridian-schemas/uiview";
|
|
3
|
+
import { type MeridianInitialData } from "@savvifi/meridian-web-react";
|
|
4
|
+
export interface MeridianEntityViewProps {
|
|
5
|
+
/** The dehydrated ViewDescriptor JSON value (from the server render). */
|
|
6
|
+
viewJson: unknown;
|
|
7
|
+
/** The dehydrated page-0 seed (keyed by `${service}.${method}`). */
|
|
8
|
+
initialData: MeridianInitialData;
|
|
9
|
+
/** Browser invoker over the app tRPC client (`createEntityOpsInvoker(client.entities.ops)`). */
|
|
10
|
+
invoker: RpcInvoker;
|
|
11
|
+
/** Resolved color mode; must match the server render for clean hydration. */
|
|
12
|
+
mode?: "light" | "dark";
|
|
13
|
+
}
|
|
14
|
+
export declare function MeridianEntityView({ viewJson, initialData, invoker, mode, }: MeridianEntityViewProps): ReactNode;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
// MeridianEntityView — the client hydration counterpart. The host reads the
|
|
2
|
+
// dehydrated `{ viewJson, initialData }` from its boot script, builds a browser
|
|
3
|
+
// invoker over its tRPC client (`createEntityOpsInvoker(client.entities.ops)`),
|
|
4
|
+
// and mounts this — hydrating the SSR-rendered node in place. Subsequent pages
|
|
5
|
+
// and actions flow through the browser invoker.
|
|
6
|
+
import { createElement } from "react";
|
|
7
|
+
import { fromJson } from "@bufbuild/protobuf";
|
|
8
|
+
import { ViewDescriptorSchema } from "@savvifi/meridian-proto-ts/proto/view_pb.js";
|
|
9
|
+
import { ViewRenderer } from "@savvifi/meridian-web-react";
|
|
10
|
+
import { MeridianMuiProvider } from "@savvifi/meridian-mui-kit";
|
|
11
|
+
import { savviSkin } from "./savvi-skin.js";
|
|
12
|
+
export function MeridianEntityView({ viewJson, initialData, invoker, mode = "light", }) {
|
|
13
|
+
const view = fromJson(ViewDescriptorSchema, viewJson);
|
|
14
|
+
return createElement(MeridianMuiProvider, { invoker, theme: savviSkin, mode }, createElement(ViewRenderer, { view, initialData }));
|
|
15
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
import { type EntityOpsRoot } from "@savvifi/meridian-aion-projection";
|
|
3
|
+
import { type MeridianInitialData } from "@savvifi/meridian-web-react";
|
|
4
|
+
import { type CompositionGraphClient } from "@aion/entity-route-runtime/composition/loader.js";
|
|
5
|
+
/** A tRPC caller/client exposing entity ops at `entities.ops[modulePath][proc]`. */
|
|
6
|
+
export interface EntityCaller {
|
|
7
|
+
entities?: {
|
|
8
|
+
ops?: EntityOpsRoot;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export interface RenderMeridianEntityViewInput {
|
|
12
|
+
/** The request pathname, e.g. `/entities/products` or `/entities/products/id/123`. */
|
|
13
|
+
pathname: string;
|
|
14
|
+
/** Reserved for filter/pagination binding (unused in the first cut). */
|
|
15
|
+
searchParams?: URLSearchParams | Record<string, string>;
|
|
16
|
+
/** Session-bound server caller (e.g. `studioAppRouter().createCaller(ctx)`). */
|
|
17
|
+
caller: EntityCaller;
|
|
18
|
+
/** The request's graph client (`ctx.graph`) for the composition loader. */
|
|
19
|
+
graph: CompositionGraphClient;
|
|
20
|
+
/** Resolved color mode (from the host's theme cookie); default "light". */
|
|
21
|
+
mode?: "light" | "dark";
|
|
22
|
+
/** Not-found behavior: "throw" a NotFoundSignal (default) or "return" `{node:null}`. */
|
|
23
|
+
notFound?: "throw" | "return";
|
|
24
|
+
}
|
|
25
|
+
export interface MeridianDehydratedView {
|
|
26
|
+
/** The projected ViewDescriptor as a JSON value (embed in the boot script). */
|
|
27
|
+
viewJson: unknown;
|
|
28
|
+
/** Pre-fetched page-0 rows keyed by `${service}.${method}` for the client seed. */
|
|
29
|
+
initialData: MeridianInitialData;
|
|
30
|
+
}
|
|
31
|
+
export interface RenderMeridianEntityViewResult {
|
|
32
|
+
node: ReactNode | null;
|
|
33
|
+
dehydrated: MeridianDehydratedView | null;
|
|
34
|
+
}
|
|
35
|
+
export declare function renderMeridianEntityView(input: RenderMeridianEntityViewInput): Promise<RenderMeridianEntityViewResult>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// renderMeridianEntityView — the SSR entry. Parses an entity route, loads its
|
|
2
|
+
// installed composition, projects it to a meridian ViewDescriptor, pre-fetches
|
|
3
|
+
// page-0 through the request-scoped caller, and returns a rendered React node +
|
|
4
|
+
// a dehydrated payload for the client to hydrate against.
|
|
5
|
+
//
|
|
6
|
+
// The host owns the SSR harness (renderToPipeableStream), the session-bound
|
|
7
|
+
// caller, and the graph; meridian owns everything from the composition down.
|
|
8
|
+
import { createElement } from "react";
|
|
9
|
+
import { toJson } from "@bufbuild/protobuf";
|
|
10
|
+
import { ViewDescriptorSchema } from "@savvifi/meridian-proto-ts/proto/view_pb.js";
|
|
11
|
+
import { createEntityOpsInvoker, projectEntityView, } from "@savvifi/meridian-aion-projection";
|
|
12
|
+
import { buildPageRequest, readPage, ViewRenderer, } from "@savvifi/meridian-web-react";
|
|
13
|
+
import { MeridianMuiProvider } from "@savvifi/meridian-mui-kit";
|
|
14
|
+
import { parseAndValidateEntityRoute } from "@aion/entity-route-runtime/entity-route/index.js";
|
|
15
|
+
import { loadInstalledEntityDetailCompositions, loadInstalledEntityListCompositions, } from "@aion/entity-route-runtime/composition/loader.js";
|
|
16
|
+
import { savviSkin } from "./savvi-skin.js";
|
|
17
|
+
import { NotFoundSignal } from "./not-found.js";
|
|
18
|
+
/** `/entities/products/id/123` → `["products","id","123"]` (strip the prefix). */
|
|
19
|
+
function routeSegments(pathname) {
|
|
20
|
+
const parts = pathname.replace(/^\/+|\/+$/g, "").split("/");
|
|
21
|
+
const at = parts.indexOf("entities");
|
|
22
|
+
return at >= 0 ? parts.slice(at + 1) : parts;
|
|
23
|
+
}
|
|
24
|
+
/** The first table panel in a projected view (the list/related-items table). */
|
|
25
|
+
function firstTablePanel(view) {
|
|
26
|
+
for (const slot of view.slots) {
|
|
27
|
+
const body = slot.panel?.body;
|
|
28
|
+
if (body?.case === "table")
|
|
29
|
+
return body.value;
|
|
30
|
+
}
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
export async function renderMeridianEntityView(input) {
|
|
34
|
+
const { caller, graph, mode = "light", notFound = "throw" } = input;
|
|
35
|
+
const parsed = parseAndValidateEntityRoute(routeSegments(input.pathname));
|
|
36
|
+
const miss = () => {
|
|
37
|
+
if (notFound === "return")
|
|
38
|
+
return { node: null, dehydrated: null };
|
|
39
|
+
throw new NotFoundSignal(input.pathname);
|
|
40
|
+
};
|
|
41
|
+
if (!parsed.success)
|
|
42
|
+
return miss();
|
|
43
|
+
const { entityType, entityId } = parsed.parsed;
|
|
44
|
+
const kind = entityId ? "detail" : "list";
|
|
45
|
+
const systemGraphFactory = async () => graph;
|
|
46
|
+
const compositions = kind === "detail"
|
|
47
|
+
? await loadInstalledEntityDetailCompositions({ systemGraphFactory })
|
|
48
|
+
: await loadInstalledEntityListCompositions({ systemGraphFactory });
|
|
49
|
+
const composition = compositions[entityType];
|
|
50
|
+
if (!composition)
|
|
51
|
+
return miss();
|
|
52
|
+
const view = projectEntityView(composition, { kind });
|
|
53
|
+
const invoker = createEntityOpsInvoker(caller.entities?.ops);
|
|
54
|
+
// Pre-fetch page-0 for the table so the SSR HTML carries real rows (and the
|
|
55
|
+
// client hydrates without an immediate refetch). Non-fatal: on failure we
|
|
56
|
+
// leave it unseeded and the client fetches on mount.
|
|
57
|
+
const initialData = {};
|
|
58
|
+
const table = firstTablePanel(view);
|
|
59
|
+
if (table?.populate) {
|
|
60
|
+
try {
|
|
61
|
+
const request = buildPageRequest(table.pagination, { page: 0 });
|
|
62
|
+
const response = await invoker.invoke(table.populate.service, table.populate.method, request);
|
|
63
|
+
initialData[`${table.populate.service}.${table.populate.method}`] = readPage(table.pagination, response, table.rowsField);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
// leave unseeded
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const node = createElement(MeridianMuiProvider, { invoker, theme: savviSkin, mode }, createElement(ViewRenderer, { view, initialData }));
|
|
70
|
+
return {
|
|
71
|
+
node,
|
|
72
|
+
dehydrated: { viewJson: toJson(ViewDescriptorSchema, view), initialData },
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type Theme } from "@savvifi/meridian-proto-ts/proto/theme_pb.js";
|
|
2
|
+
/**
|
|
3
|
+
* The savvi brand skin as a meridian Theme proto — seeded from studio's current
|
|
4
|
+
* MUI palette (light + dark). A convenience default so the meridian render path
|
|
5
|
+
* matches the existing look immediately; long-term the brand values graduate to
|
|
6
|
+
* `aion/brand` and hosts pass their own Theme proto to override.
|
|
7
|
+
*/
|
|
8
|
+
export declare const savviSkin: Theme;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { create } from "@bufbuild/protobuf";
|
|
2
|
+
import { PaletteSchema, ThemeSchema, } from "@savvifi/meridian-proto-ts/proto/theme_pb.js";
|
|
3
|
+
/**
|
|
4
|
+
* The savvi brand skin as a meridian Theme proto — seeded from studio's current
|
|
5
|
+
* MUI palette (light + dark). A convenience default so the meridian render path
|
|
6
|
+
* matches the existing look immediately; long-term the brand values graduate to
|
|
7
|
+
* `aion/brand` and hosts pass their own Theme proto to override.
|
|
8
|
+
*/
|
|
9
|
+
export const savviSkin = create(ThemeSchema, {
|
|
10
|
+
id: "savvi",
|
|
11
|
+
light: create(PaletteSchema, {
|
|
12
|
+
bg: "#f5f5f5",
|
|
13
|
+
surface: "#ffffff",
|
|
14
|
+
fg: "#333333",
|
|
15
|
+
muted: "#666666",
|
|
16
|
+
border: "#e0e0e0",
|
|
17
|
+
accent: "#1976d2",
|
|
18
|
+
accentStrong: "#1565c0",
|
|
19
|
+
}),
|
|
20
|
+
dark: create(PaletteSchema, {
|
|
21
|
+
bg: "#121212",
|
|
22
|
+
surface: "#1e1e1e",
|
|
23
|
+
fg: "#ffffff",
|
|
24
|
+
muted: "#b0b0b0",
|
|
25
|
+
border: "#333333",
|
|
26
|
+
accent: "#90caf9",
|
|
27
|
+
accentStrong: "#648dae",
|
|
28
|
+
}),
|
|
29
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@savvifi/meridian-aion-web",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "The aion/web <-> meridian-ux render seam: renderMeridianEntityView (server — parse route, load composition, project to a ViewDescriptor, pre-fetch page-0, render + dehydrate) + MeridianEntityView (client hydration) + savviSkin. Lets an SSR host (e.g. aion/web on Hono+Vite) render entity routes through meridian instead of the legacy React path.",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/meridian-ux/meridian-aion-web.git"
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./server": {
|
|
21
|
+
"types": "./dist/render-server.d.ts",
|
|
22
|
+
"default": "./dist/render-server.js"
|
|
23
|
+
},
|
|
24
|
+
"./client": {
|
|
25
|
+
"types": "./dist/render-client.d.ts",
|
|
26
|
+
"default": "./dist/render-client.js"
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist/**/*.js",
|
|
32
|
+
"dist/**/*.d.ts"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc -p tsconfig.build.json",
|
|
36
|
+
"typecheck": "tsc -p tsconfig.json"
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@savvifi/meridian-aion-projection": "^0.2.2",
|
|
40
|
+
"@savvifi/meridian-mui-kit": "^0.1.0",
|
|
41
|
+
"@savvifi/meridian-proto-ts": "^0.3.0",
|
|
42
|
+
"@savvifi/meridian-schemas": "^0.3.0",
|
|
43
|
+
"@savvifi/meridian-web-react": "^0.3.3",
|
|
44
|
+
"@bufbuild/protobuf": "2.12.1"
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@aion/entity-route-runtime": ">=0.1.1",
|
|
48
|
+
"@emotion/react": "^11.0.0",
|
|
49
|
+
"@emotion/styled": "^11.0.0",
|
|
50
|
+
"@mui/material": "^7.0.0",
|
|
51
|
+
"react": "^19.0.0",
|
|
52
|
+
"react-dom": "^19.0.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@aion/entity-route-runtime": "^0.2.1",
|
|
56
|
+
"@emotion/react": "11.14.0",
|
|
57
|
+
"@emotion/styled": "11.14.0",
|
|
58
|
+
"@mui/material": "7.3.7",
|
|
59
|
+
"@types/react": "19.2.17",
|
|
60
|
+
"@types/react-dom": "19.2.3",
|
|
61
|
+
"react": "19.2.7",
|
|
62
|
+
"react-dom": "19.2.7",
|
|
63
|
+
"typescript": "5.6.2"
|
|
64
|
+
}
|
|
65
|
+
}
|