@meekojs/meeko 0.0.3 → 0.0.4
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/README.md +18 -1
- package/dist/autoimports-ts.d.ts +26 -0
- package/dist/autoimports-ts.js +127 -0
- package/dist/autoimports-ts.js.map +1 -0
- package/dist/autoimports.d.ts +21 -0
- package/dist/autoimports.js +285 -0
- package/dist/autoimports.js.map +1 -0
- package/dist/check.js +35 -23
- package/dist/check.js.map +1 -0
- package/dist/cli.js +33 -4
- package/dist/cli.js.map +1 -0
- package/dist/compiler/import-graph.d.ts +8 -0
- package/dist/compiler/import-graph.js +33 -0
- package/dist/compiler/import-graph.js.map +1 -0
- package/dist/compiler/mapping.js +1 -0
- package/dist/compiler/mapping.js.map +1 -0
- package/dist/compiler/project-root.d.ts +2 -0
- package/dist/compiler/project-root.js +18 -0
- package/dist/compiler/project-root.js.map +1 -0
- package/dist/compiler/sfc.js +1 -0
- package/dist/compiler/sfc.js.map +1 -0
- package/dist/compiler/sourcemap.d.ts +3 -0
- package/dist/compiler/sourcemap.js +75 -0
- package/dist/compiler/sourcemap.js.map +1 -0
- package/dist/compiler/template.js +1 -0
- package/dist/compiler/template.js.map +1 -0
- package/dist/compiler.js +11 -5
- package/dist/compiler.js.map +1 -0
- package/dist/devtools-ui/index.js +1 -0
- package/dist/devtools-ui/index.js.map +1 -0
- package/dist/devtools.js +1 -0
- package/dist/devtools.js.map +1 -0
- package/dist/dom.d.ts +8 -1
- package/dist/dom.js +10 -5
- package/dist/dom.js.map +1 -0
- package/dist/framework/check-app.d.ts +5 -0
- package/dist/framework/check-app.js +59 -0
- package/dist/framework/check-app.js.map +1 -0
- package/dist/framework/client.d.ts +45 -0
- package/dist/framework/client.js +146 -0
- package/dist/framework/client.js.map +1 -0
- package/dist/framework/clients.d.ts +82 -0
- package/dist/framework/clients.js +182 -0
- package/dist/framework/clients.js.map +1 -0
- package/dist/framework/config-type.d.ts +10 -0
- package/dist/framework/config-type.js +4 -0
- package/dist/framework/config-type.js.map +1 -0
- package/dist/framework/config.d.ts +15 -0
- package/dist/framework/config.js +138 -0
- package/dist/framework/config.js.map +1 -0
- package/dist/framework/contracts-generate.d.ts +8 -0
- package/dist/framework/contracts-generate.js +227 -0
- package/dist/framework/contracts-generate.js.map +1 -0
- package/dist/framework/generate.d.ts +7 -0
- package/dist/framework/generate.js +67 -0
- package/dist/framework/generate.js.map +1 -0
- package/dist/framework/init.d.ts +2 -0
- package/dist/framework/init.js +362 -0
- package/dist/framework/init.js.map +1 -0
- package/dist/framework/modules.d.ts +22 -0
- package/dist/framework/modules.js +29 -0
- package/dist/framework/modules.js.map +1 -0
- package/dist/framework/page.d.ts +60 -0
- package/dist/framework/page.js +42 -0
- package/dist/framework/page.js.map +1 -0
- package/dist/framework/prerender.d.ts +14 -0
- package/dist/framework/prerender.js +104 -0
- package/dist/framework/prerender.js.map +1 -0
- package/dist/framework/router.d.ts +39 -0
- package/dist/framework/router.js +173 -0
- package/dist/framework/router.js.map +1 -0
- package/dist/framework/server.d.ts +51 -0
- package/dist/framework/server.js +192 -0
- package/dist/framework/server.js.map +1 -0
- package/dist/framework/socket-contracts-generate.d.ts +5 -0
- package/dist/framework/socket-contracts-generate.js +159 -0
- package/dist/framework/socket-contracts-generate.js.map +1 -0
- package/dist/framework/socket.d.ts +70 -0
- package/dist/framework/socket.js +150 -0
- package/dist/framework/socket.js.map +1 -0
- package/dist/framework/state.d.ts +15 -0
- package/dist/framework/state.js +72 -0
- package/dist/framework/state.js.map +1 -0
- package/dist/framework-server/auto-clients.d.ts +3 -0
- package/dist/framework-server/auto-clients.js +27 -0
- package/dist/framework-server/auto-clients.js.map +1 -0
- package/dist/framework-server/context.d.ts +21 -0
- package/dist/framework-server/context.js +22 -0
- package/dist/framework-server/context.js.map +1 -0
- package/dist/framework-server/contracts.d.ts +64 -0
- package/dist/framework-server/contracts.js +129 -0
- package/dist/framework-server/contracts.js.map +1 -0
- package/dist/framework-server/discovery.d.ts +40 -0
- package/dist/framework-server/discovery.js +252 -0
- package/dist/framework-server/discovery.js.map +1 -0
- package/dist/framework-server/local.d.ts +19 -0
- package/dist/framework-server/local.js +98 -0
- package/dist/framework-server/local.js.map +1 -0
- package/dist/framework-server/node-upgrade.d.ts +23 -0
- package/dist/framework-server/node-upgrade.js +78 -0
- package/dist/framework-server/node-upgrade.js.map +1 -0
- package/dist/framework-server/node-websocket.d.ts +16 -0
- package/dist/framework-server/node-websocket.js +125 -0
- package/dist/framework-server/node-websocket.js.map +1 -0
- package/dist/framework-server/openapi.d.ts +17 -0
- package/dist/framework-server/openapi.js +118 -0
- package/dist/framework-server/openapi.js.map +1 -0
- package/dist/framework-server/session-stores.d.ts +66 -0
- package/dist/framework-server/session-stores.js +96 -0
- package/dist/framework-server/session-stores.js.map +1 -0
- package/dist/framework-server/sessions.d.ts +64 -0
- package/dist/framework-server/sessions.js +136 -0
- package/dist/framework-server/sessions.js.map +1 -0
- package/dist/framework-server/socket.d.ts +83 -0
- package/dist/framework-server/socket.js +161 -0
- package/dist/framework-server/socket.js.map +1 -0
- package/dist/framework-server.d.ts +21 -0
- package/dist/framework-server.js +12 -0
- package/dist/framework-server.js.map +1 -0
- package/dist/framework.d.ts +29 -0
- package/dist/framework.js +13 -0
- package/dist/framework.js.map +1 -0
- package/dist/hmr.js +1 -0
- package/dist/hmr.js.map +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -0
- package/dist/init.js +4 -2
- package/dist/init.js.map +1 -0
- package/dist/internal.js +1 -0
- package/dist/internal.js.map +1 -0
- package/dist/node.d.ts +12 -0
- package/dist/node.js +52 -0
- package/dist/node.js.map +1 -0
- package/dist/reactivity.d.ts +12 -0
- package/dist/reactivity.js +40 -3
- package/dist/reactivity.js.map +1 -0
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -0
- package/dist/types.js +23 -8
- package/dist/types.js.map +1 -0
- package/dist/vite.js +91 -6
- package/dist/vite.js.map +1 -0
- package/package.json +14 -3
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { signal } from "../reactivity.js";
|
|
2
|
+
import { pageSearchObject, validatePageInputs } from "./page.js";
|
|
3
|
+
import { createNavigator, matchRoute } from "./router.js";
|
|
4
|
+
import { hydrateWithStateStore, StateStore, withStateStore } from "./state.js";
|
|
5
|
+
function isRecord(value) {
|
|
6
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
+
}
|
|
8
|
+
function readPayload(value) {
|
|
9
|
+
const parsed = typeof value === "string" ? JSON.parse(value) : value;
|
|
10
|
+
if (!isRecord(parsed) || typeof parsed.route !== "string" || !isRecord(parsed.props) || !isRecord(parsed.state)) {
|
|
11
|
+
throw new TypeError("Invalid Meeko hydration payload");
|
|
12
|
+
}
|
|
13
|
+
if (!isRecord(parsed.props.params) || !isRecord(parsed.props.search))
|
|
14
|
+
throw new TypeError("Invalid Meeko route props");
|
|
15
|
+
if (parsed.sharedState !== undefined && !isRecord(parsed.sharedState))
|
|
16
|
+
throw new TypeError("Invalid Meeko shared state");
|
|
17
|
+
return { route: parsed.route, props: parsed.props, state: parsed.state, sharedState: parsed.sharedState ?? {} };
|
|
18
|
+
}
|
|
19
|
+
function bind(component, props) {
|
|
20
|
+
return {
|
|
21
|
+
renderWithState: () => component.renderWithState(props),
|
|
22
|
+
renderToString: () => component.renderToString(props),
|
|
23
|
+
hydrateScope: (scope, _unused, state) => component.hydrateScope(scope, props, state),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
async function createTree(route, props, loadPage, fromLayout) {
|
|
27
|
+
let component = bind(await loadPage(route.file), props);
|
|
28
|
+
const layouts = [];
|
|
29
|
+
for (let index = route.layouts.length - 1; index >= fromLayout; index--) {
|
|
30
|
+
const file = route.layouts[index];
|
|
31
|
+
const outlet = signal(component);
|
|
32
|
+
const layoutProps = Object.defineProperty({}, "outlet", {
|
|
33
|
+
enumerable: true,
|
|
34
|
+
get: () => outlet.value,
|
|
35
|
+
});
|
|
36
|
+
component = bind(await loadPage(file), layoutProps);
|
|
37
|
+
layouts.unshift({ file, outlet });
|
|
38
|
+
}
|
|
39
|
+
return { component, layouts };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Hydrates the SSR tree and updates only the first changed outlet on navigation.
|
|
43
|
+
* Layout templates render their outlet with <component is="props.outlet"/>.
|
|
44
|
+
*/
|
|
45
|
+
export async function mountClientApp(options) {
|
|
46
|
+
const browser = options.browser ?? window;
|
|
47
|
+
const payload = readPayload(options.payload);
|
|
48
|
+
const initial = matchRoute(options.routes, browser.location.pathname);
|
|
49
|
+
if (!initial || initial.route.id !== payload.route)
|
|
50
|
+
throw new Error("Meeko route hydration mismatch");
|
|
51
|
+
if (JSON.stringify(initial.params) !== JSON.stringify(payload.props.params))
|
|
52
|
+
throw new Error("Meeko route parameters do not match SSR");
|
|
53
|
+
const stateStore = new StateStore(payload.sharedState);
|
|
54
|
+
const first = await createTree(initial.route, payload.props, options.loadPage, 0);
|
|
55
|
+
let layouts = first.layouts;
|
|
56
|
+
let release = hydrateWithStateStore(stateStore, () => first.component.hydrateScope({ root: options.root }, {}, payload.state));
|
|
57
|
+
let disposed = false;
|
|
58
|
+
const navigator = createNavigator(options.routes, async (context) => {
|
|
59
|
+
let props;
|
|
60
|
+
if (options.serverData && !options.loadRoute) {
|
|
61
|
+
const response = await (options.fetch ?? fetch)(context.url.href, {
|
|
62
|
+
headers: { accept: "application/meeko+json" }, credentials: "same-origin", signal: context.signal,
|
|
63
|
+
});
|
|
64
|
+
if (!response.ok)
|
|
65
|
+
throw new Error(`Route data request failed: HTTP ${response.status}`);
|
|
66
|
+
const csrf = response.headers.get("x-meeko-csrf-token");
|
|
67
|
+
if (csrf !== null) {
|
|
68
|
+
const meta = browser.document.querySelector('meta[name="meeko-csrf-token"]');
|
|
69
|
+
if (csrf && meta)
|
|
70
|
+
meta.content = csrf;
|
|
71
|
+
else if (csrf) {
|
|
72
|
+
const next = browser.document.createElement("meta");
|
|
73
|
+
next.name = "meeko-csrf-token";
|
|
74
|
+
next.content = csrf;
|
|
75
|
+
browser.document.head.append(next);
|
|
76
|
+
}
|
|
77
|
+
else
|
|
78
|
+
meta?.remove();
|
|
79
|
+
}
|
|
80
|
+
const payload = readPayload(await response.text());
|
|
81
|
+
if (payload.route !== context.match.route.id)
|
|
82
|
+
throw new Error("Meeko route data mismatch");
|
|
83
|
+
if (context.signal.aborted || disposed)
|
|
84
|
+
return;
|
|
85
|
+
stateStore.seed(payload.sharedState);
|
|
86
|
+
props = payload.props;
|
|
87
|
+
}
|
|
88
|
+
else {
|
|
89
|
+
const sidecar = context.match.route.sidecar && options.loadRoute
|
|
90
|
+
? await options.loadRoute(context.match.route.sidecar)
|
|
91
|
+
: undefined;
|
|
92
|
+
const validated = await validatePageInputs(sidecar, context.match.params, pageSearchObject(context.url.searchParams));
|
|
93
|
+
const data = sidecar?.load ? await sidecar.load({ ...context, ...validated, state: stateStore }) : undefined;
|
|
94
|
+
props = {
|
|
95
|
+
params: validated.params,
|
|
96
|
+
search: validated.search,
|
|
97
|
+
...(data === undefined ? {} : { data }),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
if (context.signal.aborted || disposed)
|
|
101
|
+
return;
|
|
102
|
+
const retained = Math.min(context.retainedLayouts, layouts.length);
|
|
103
|
+
const next = await createTree(context.match.route, props, options.loadPage, retained);
|
|
104
|
+
if (context.signal.aborted || disposed)
|
|
105
|
+
return;
|
|
106
|
+
if (retained) {
|
|
107
|
+
withStateStore(stateStore, () => { layouts[retained - 1].outlet.value = next.component; });
|
|
108
|
+
layouts = [...layouts.slice(0, retained), ...next.layouts];
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const rendered = withStateStore(stateStore, () => next.component.renderWithState({}));
|
|
112
|
+
release();
|
|
113
|
+
options.root.innerHTML = rendered.html;
|
|
114
|
+
release = hydrateWithStateStore(stateStore, () => next.component.hydrateScope({ root: options.root }, {}, rendered.state));
|
|
115
|
+
layouts = next.layouts;
|
|
116
|
+
}, browser);
|
|
117
|
+
const onClick = (event) => {
|
|
118
|
+
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey)
|
|
119
|
+
return;
|
|
120
|
+
const target = event.target;
|
|
121
|
+
if (!target || !("closest" in target) || typeof target.closest !== "function")
|
|
122
|
+
return;
|
|
123
|
+
const anchor = target.closest("a[href]");
|
|
124
|
+
if (!anchor || anchor.hasAttribute("download") || (anchor.target && anchor.target !== "_self"))
|
|
125
|
+
return;
|
|
126
|
+
const url = new URL(anchor.href, browser.location.href);
|
|
127
|
+
if (url.origin !== browser.location.origin || !matchRoute(options.routes, url.pathname))
|
|
128
|
+
return;
|
|
129
|
+
event.preventDefault();
|
|
130
|
+
void navigator.navigate(url.href).catch((error) => { options.onNavigationError?.(error); });
|
|
131
|
+
};
|
|
132
|
+
browser.document.addEventListener("click", onClick);
|
|
133
|
+
return {
|
|
134
|
+
navigate: navigator.navigate,
|
|
135
|
+
state: stateStore,
|
|
136
|
+
dispose() {
|
|
137
|
+
if (disposed)
|
|
138
|
+
return;
|
|
139
|
+
disposed = true;
|
|
140
|
+
navigator.dispose();
|
|
141
|
+
browser.document.removeEventListener("click", onClick);
|
|
142
|
+
release();
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/framework/client.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAE1C,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAE1D,OAAO,EAAE,qBAAqB,EAAE,UAAU,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAmD/E,SAAS,QAAQ,CAAC,KAAc;IAC9B,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;AAC9E,CAAC;AAED,SAAS,WAAW,CAAC,KAAc;IACjC,MAAM,MAAM,GAAY,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IAC9E,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,OAAO,MAAM,CAAC,KAAK,KAAK,QAAQ,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC;QAChH,MAAM,IAAI,SAAS,CAAC,iCAAiC,CAAC,CAAC;IACzD,CAAC;IACD,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,2BAA2B,CAAC,CAAC;IACvH,IAAI,MAAM,CAAC,WAAW,KAAK,SAAS,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,WAAW,CAAC;QAAE,MAAM,IAAI,SAAS,CAAC,4BAA4B,CAAC,CAAC;IACzH,OAAO,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,WAAW,EAAE,MAAM,CAAC,WAAW,IAAI,EAAE,EAAE,CAAC;AAClH,CAAC;AAED,SAAS,IAAI,CAAC,SAA0B,EAAE,KAA8B;IACtE,OAAO;QACL,eAAe,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,eAAe,CAAC,KAAK,CAAC;QACvD,cAAc,EAAE,GAAG,EAAE,CAAC,SAAS,CAAC,cAAc,CAAC,KAAK,CAAC;QACrD,YAAY,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE,CAAC,SAAS,CAAC,YAAY,CAAC,KAAK,EAAE,KAAK,EAAE,KAAK,CAAC;KACrF,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,UAAU,CACvB,KAAkB,EAClB,KAA8B,EAC9B,QAAsC,EACtC,UAAkB;IAElB,IAAI,SAAS,GAAG,IAAI,CAAC,MAAM,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,CAAC,CAAC;IACxD,MAAM,OAAO,GAAqB,EAAE,CAAC;IACrC,KAAK,IAAI,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,KAAK,IAAI,UAAU,EAAE,KAAK,EAAE,EAAE,CAAC;QACxE,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAE,CAAC;QACnC,MAAM,MAAM,GAAG,MAAM,CAAC,SAAS,CAAC,CAAC;QACjC,MAAM,WAAW,GAAG,MAAM,CAAC,cAAc,CAAC,EAAE,EAAE,QAAQ,EAAE;YACtD,UAAU,EAAE,IAAI;YAChB,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,KAAK;SACxB,CAA4B,CAAC;QAC9B,SAAS,GAAG,IAAI,CAAC,MAAM,QAAQ,CAAC,IAAI,CAAC,EAAE,WAAW,CAAC,CAAC;QACpD,OAAO,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IACpC,CAAC;IACD,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC;AAChC,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,cAAc,CAAC,OAAyB;IAC5D,MAAM,OAAO,GAAG,OAAO,CAAC,OAAO,IAAI,MAAM,CAAC;IAC1C,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IAC7C,MAAM,OAAO,GAAG,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IACtE,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,KAAK,CAAC,EAAE,KAAK,OAAO,CAAC,KAAK;QAAE,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACtG,IAAI,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,MAAM,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IACxI,MAAM,UAAU,GAAG,IAAI,UAAU,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACvD,MAAM,KAAK,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC;IAClF,IAAI,OAAO,GAAG,KAAK,CAAC,OAAO,CAAC;IAC5B,IAAI,OAAO,GAAG,qBAAqB,CAAC,UAAU,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,SAAS,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC;IAC/H,IAAI,QAAQ,GAAG,KAAK,CAAC;IACrB,MAAM,SAAS,GAAG,eAAe,CAAC,OAAO,CAAC,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,EAAE;QAClE,IAAI,KAA8B,CAAC;QACnC,IAAI,OAAO,CAAC,UAAU,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;YAC7C,MAAM,QAAQ,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,EAAE;gBAChE,OAAO,EAAE,EAAE,MAAM,EAAE,wBAAwB,EAAE,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM;aAClG,CAAC,CAAC;YACH,IAAI,CAAC,QAAQ,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,QAAQ,CAAC,MAAM,EAAE,CAAC,CAAC;YACxF,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,oBAAoB,CAAC,CAAC;YACxD,IAAI,IAAI,KAAK,IAAI,EAAE,CAAC;gBAClB,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkB,+BAA+B,CAAC,CAAC;gBAC9F,IAAI,IAAI,IAAI,IAAI;oBAAE,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;qBACjC,IAAI,IAAI,EAAE,CAAC;oBACd,MAAM,IAAI,GAAG,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;oBACpD,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;oBAC/B,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;oBACpB,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;gBACrC,CAAC;;oBAAM,IAAI,EAAE,MAAM,EAAE,CAAC;YACxB,CAAC;YACD,MAAM,OAAO,GAAG,WAAW,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;YACnD,IAAI,OAAO,CAAC,KAAK,KAAK,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,2BAA2B,CAAC,CAAC;YAC3F,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO,IAAI,QAAQ;gBAAE,OAAO;YAC/C,UAAU,CAAC,IAAI,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;YACrC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QACxB,CAAC;aAAM,CAAC;YACN,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,IAAI,OAAO,CAAC,SAAS;gBAC9D,CAAC,CAAC,MAAM,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,OAAO,CAAC;gBACtD,CAAC,CAAC,SAAS,CAAC;YACd,MAAM,SAAS,GAAG,MAAM,kBAAkB,CAAC,OAAO,EAAE,OAAO,CAAC,KAAK,CAAC,MAAM,EAAE,gBAAgB,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,CAAC;YACtH,MAAM,IAAI,GAAG,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,MAAM,OAAO,CAAC,IAAI,CAAC,EAAE,GAAG,OAAO,EAAE,GAAG,SAAS,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;YAC7G,KAAK,GAAG;gBACN,MAAM,EAAE,SAAS,CAAC,MAAM;gBACxB,MAAM,EAAE,SAAS,CAAC,MAAM;gBACxB,GAAG,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,CAAC;aACxC,CAAC;QACJ,CAAC;QACD,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO,IAAI,QAAQ;YAAE,OAAO;QAC/C,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,eAAe,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;QACnE,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,KAAK,EAAE,KAAK,EAAE,OAAO,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QACtF,IAAI,OAAO,CAAC,MAAM,CAAC,OAAO,IAAI,QAAQ;YAAE,OAAO;QAC/C,IAAI,QAAQ,EAAE,CAAC;YACb,cAAc,CAAC,UAAU,EAAE,GAAG,EAAE,GAAG,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAE,CAAC,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC;YAC5F,OAAO,GAAG,CAAC,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,QAAQ,CAAC,EAAE,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC;YAC3D,OAAO;QACT,CAAC;QACD,MAAM,QAAQ,GAAG,cAAc,CAAC,UAAU,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,eAAe,CAAC,EAAE,CAAC,CAAC,CAAC;QACtF,OAAO,EAAE,CAAC;QACV,OAAO,CAAC,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,IAAI,CAAC;QACvC,OAAO,GAAG,qBAAqB,CAAC,UAAU,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC;QAC3H,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC;IACzB,CAAC,EAAE,OAAO,CAAC,CAAC;IACZ,MAAM,OAAO,GAAG,CAAC,KAAiB,EAAE,EAAE;QACpC,IAAI,KAAK,CAAC,gBAAgB,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,OAAO,IAAI,KAAK,CAAC,QAAQ,IAAI,KAAK,CAAC,MAAM;YAAE,OAAO;QAC7H,MAAM,MAAM,GAAG,KAAK,CAAC,MAAM,CAAC;QAC5B,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC,SAAS,IAAI,MAAM,CAAC,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,UAAU;YAAE,OAAO;QACtF,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,SAAS,CAA6B,CAAC;QACrE,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,YAAY,CAAC,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,MAAM,KAAK,OAAO,CAAC;YAAE,OAAO;QACvG,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACxD,IAAI,GAAG,CAAC,MAAM,KAAK,OAAO,CAAC,QAAQ,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,QAAQ,CAAC;YAAE,OAAO;QAChG,KAAK,CAAC,cAAc,EAAE,CAAC;QACvB,KAAK,SAAS,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;IACvG,CAAC,CAAC;IACF,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;IACpD,OAAO;QACL,QAAQ,EAAE,SAAS,CAAC,QAAQ;QAC5B,KAAK,EAAE,UAAU;QACjB,OAAO;YACL,IAAI,QAAQ;gBAAE,OAAO;YACrB,QAAQ,GAAG,IAAI,CAAC;YAChB,SAAS,CAAC,OAAO,EAAE,CAAC;YACpB,OAAO,CAAC,QAAQ,CAAC,mBAAmB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YACvD,OAAO,EAAE,CAAC;QACZ,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import type { Component, DomScope } from \"../dom.js\";\nimport { signal } from \"../reactivity.js\";\nimport type { Signal } from \"../reactivity.js\";\nimport { pageSearchObject, validatePageInputs } from \"./page.js\";\nimport type { PageSchema } from \"./page.js\";\nimport { createNavigator, matchRoute } from \"./router.js\";\nimport type { NavigationContext, Navigator, RouteRecord } from \"./router.js\";\nimport { hydrateWithStateStore, StateStore, withStateStore } from \"./state.js\";\n\n/** The public shape emitted by the .meeko compiler. */\nexport interface ClientComponent {\n renderWithState(props: Record<string, unknown>): { html: string; state: Record<string, unknown> };\n renderToString(props: Record<string, unknown>, state?: Record<string, unknown>): string;\n hydrateScope(scope: DomScope, props: Record<string, unknown>, state?: Record<string, unknown>): () => void;\n}\n\nexport interface ClientRoute {\n /** Client-safe loader. Server-only sidecars must not be passed to loadRoute. */\n params?: PageSchema;\n search?: PageSchema;\n load?: (context: NavigationContext & {\n params: Readonly<Record<string, unknown>>;\n search: Readonly<Record<string, unknown>>;\n state: StateStore;\n }) => Promise<unknown> | unknown;\n}\n\nexport interface ClientAppOptions {\n readonly routes: readonly RouteRecord[];\n readonly root: Element;\n readonly payload: string | unknown;\n readonly loadPage: (file: string) => Promise<ClientComponent>;\n readonly loadRoute?: (file: string) => Promise<ClientRoute>;\n /** Fetches public loader data from the SSR handler during browser navigation. */\n readonly serverData?: boolean;\n readonly fetch?: typeof fetch;\n readonly onNavigationError?: (error: unknown) => void;\n readonly browser?: Window;\n}\n\nexport interface ClientApp {\n navigate: Navigator[\"navigate\"];\n readonly state: StateStore;\n dispose(): void;\n}\n\ninterface HydrationPayload {\n route: string;\n props: Record<string, unknown>;\n state: Record<string, unknown>;\n sharedState: Record<string, unknown>;\n}\n\ninterface LayoutInstance {\n file: string;\n outlet: Signal<Component<Record<string, unknown>>>;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction readPayload(value: unknown): HydrationPayload {\n const parsed: unknown = typeof value === \"string\" ? JSON.parse(value) : value;\n if (!isRecord(parsed) || typeof parsed.route !== \"string\" || !isRecord(parsed.props) || !isRecord(parsed.state)) {\n throw new TypeError(\"Invalid Meeko hydration payload\");\n }\n if (!isRecord(parsed.props.params) || !isRecord(parsed.props.search)) throw new TypeError(\"Invalid Meeko route props\");\n if (parsed.sharedState !== undefined && !isRecord(parsed.sharedState)) throw new TypeError(\"Invalid Meeko shared state\");\n return { route: parsed.route, props: parsed.props, state: parsed.state, sharedState: parsed.sharedState ?? {} };\n}\n\nfunction bind(component: ClientComponent, props: Record<string, unknown>): Component<Record<string, unknown>> {\n return {\n renderWithState: () => component.renderWithState(props),\n renderToString: () => component.renderToString(props),\n hydrateScope: (scope, _unused, state) => component.hydrateScope(scope, props, state),\n };\n}\n\nasync function createTree(\n route: RouteRecord,\n props: Record<string, unknown>,\n loadPage: ClientAppOptions[\"loadPage\"],\n fromLayout: number,\n): Promise<{ component: Component<Record<string, unknown>>; layouts: LayoutInstance[] }> {\n let component = bind(await loadPage(route.file), props);\n const layouts: LayoutInstance[] = [];\n for (let index = route.layouts.length - 1; index >= fromLayout; index--) {\n const file = route.layouts[index]!;\n const outlet = signal(component);\n const layoutProps = Object.defineProperty({}, \"outlet\", {\n enumerable: true,\n get: () => outlet.value,\n }) as Record<string, unknown>;\n component = bind(await loadPage(file), layoutProps);\n layouts.unshift({ file, outlet });\n }\n return { component, layouts };\n}\n\n/**\n * Hydrates the SSR tree and updates only the first changed outlet on navigation.\n * Layout templates render their outlet with <component is=\"props.outlet\"/>.\n */\nexport async function mountClientApp(options: ClientAppOptions): Promise<ClientApp> {\n const browser = options.browser ?? window;\n const payload = readPayload(options.payload);\n const initial = matchRoute(options.routes, browser.location.pathname);\n if (!initial || initial.route.id !== payload.route) throw new Error(\"Meeko route hydration mismatch\");\n if (JSON.stringify(initial.params) !== JSON.stringify(payload.props.params)) throw new Error(\"Meeko route parameters do not match SSR\");\n const stateStore = new StateStore(payload.sharedState);\n const first = await createTree(initial.route, payload.props, options.loadPage, 0);\n let layouts = first.layouts;\n let release = hydrateWithStateStore(stateStore, () => first.component.hydrateScope({ root: options.root }, {}, payload.state));\n let disposed = false;\n const navigator = createNavigator(options.routes, async (context) => {\n let props: Record<string, unknown>;\n if (options.serverData && !options.loadRoute) {\n const response = await (options.fetch ?? fetch)(context.url.href, {\n headers: { accept: \"application/meeko+json\" }, credentials: \"same-origin\", signal: context.signal,\n });\n if (!response.ok) throw new Error(`Route data request failed: HTTP ${response.status}`);\n const csrf = response.headers.get(\"x-meeko-csrf-token\");\n if (csrf !== null) {\n const meta = browser.document.querySelector<HTMLMetaElement>('meta[name=\"meeko-csrf-token\"]');\n if (csrf && meta) meta.content = csrf;\n else if (csrf) {\n const next = browser.document.createElement(\"meta\");\n next.name = \"meeko-csrf-token\";\n next.content = csrf;\n browser.document.head.append(next);\n } else meta?.remove();\n }\n const payload = readPayload(await response.text());\n if (payload.route !== context.match.route.id) throw new Error(\"Meeko route data mismatch\");\n if (context.signal.aborted || disposed) return;\n stateStore.seed(payload.sharedState);\n props = payload.props;\n } else {\n const sidecar = context.match.route.sidecar && options.loadRoute\n ? await options.loadRoute(context.match.route.sidecar)\n : undefined;\n const validated = await validatePageInputs(sidecar, context.match.params, pageSearchObject(context.url.searchParams));\n const data = sidecar?.load ? await sidecar.load({ ...context, ...validated, state: stateStore }) : undefined;\n props = {\n params: validated.params,\n search: validated.search,\n ...(data === undefined ? {} : { data }),\n };\n }\n if (context.signal.aborted || disposed) return;\n const retained = Math.min(context.retainedLayouts, layouts.length);\n const next = await createTree(context.match.route, props, options.loadPage, retained);\n if (context.signal.aborted || disposed) return;\n if (retained) {\n withStateStore(stateStore, () => { layouts[retained - 1]!.outlet.value = next.component; });\n layouts = [...layouts.slice(0, retained), ...next.layouts];\n return;\n }\n const rendered = withStateStore(stateStore, () => next.component.renderWithState({}));\n release();\n options.root.innerHTML = rendered.html;\n release = hydrateWithStateStore(stateStore, () => next.component.hydrateScope({ root: options.root }, {}, rendered.state));\n layouts = next.layouts;\n }, browser);\n const onClick = (event: MouseEvent) => {\n if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;\n const target = event.target;\n if (!target || !(\"closest\" in target) || typeof target.closest !== \"function\") return;\n const anchor = target.closest(\"a[href]\") as HTMLAnchorElement | null;\n if (!anchor || anchor.hasAttribute(\"download\") || (anchor.target && anchor.target !== \"_self\")) return;\n const url = new URL(anchor.href, browser.location.href);\n if (url.origin !== browser.location.origin || !matchRoute(options.routes, url.pathname)) return;\n event.preventDefault();\n void navigator.navigate(url.href).catch((error: unknown) => { options.onNavigationError?.(error); });\n };\n browser.document.addEventListener(\"click\", onClick);\n return {\n navigate: navigator.navigate,\n state: stateStore,\n dispose() {\n if (disposed) return;\n disposed = true;\n navigator.dispose();\n browser.document.removeEventListener(\"click\", onClick);\n release();\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Public descriptors intentionally omit handler files and server schemas. */
|
|
2
|
+
export interface PublicApiEndpoint {
|
|
3
|
+
readonly clientKey: string;
|
|
4
|
+
readonly pathname: string;
|
|
5
|
+
readonly method: "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
6
|
+
readonly contentType?: "json" | "response";
|
|
7
|
+
}
|
|
8
|
+
export interface PublicRpcEndpoint {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly pathname: string;
|
|
11
|
+
}
|
|
12
|
+
export interface PublicServerManifest {
|
|
13
|
+
readonly api: readonly PublicApiEndpoint[];
|
|
14
|
+
readonly rpc: readonly PublicRpcEndpoint[];
|
|
15
|
+
}
|
|
16
|
+
/** Strips private handler paths before serializing a discovered server manifest. */
|
|
17
|
+
export declare function createPublicServerManifest(manifest: PublicServerManifest): PublicServerManifest;
|
|
18
|
+
/** Type-only operation declarations can be inferred from public shared schemas. */
|
|
19
|
+
export interface ApiOperation<Params = undefined, Query = undefined, Body = undefined, Output = unknown> {
|
|
20
|
+
readonly params: Params;
|
|
21
|
+
readonly query: Query;
|
|
22
|
+
readonly body: Body;
|
|
23
|
+
readonly output: Output;
|
|
24
|
+
}
|
|
25
|
+
export interface RpcOperation<Input = unknown, Output = unknown> {
|
|
26
|
+
readonly input: Input;
|
|
27
|
+
readonly output: Output;
|
|
28
|
+
}
|
|
29
|
+
type AnyApiOperation = ApiOperation<unknown, unknown, unknown, unknown>;
|
|
30
|
+
type RequestOptions = {
|
|
31
|
+
readonly signal?: AbortSignal;
|
|
32
|
+
};
|
|
33
|
+
type ParamOption<T> = T extends undefined ? {
|
|
34
|
+
readonly params?: never;
|
|
35
|
+
} : {
|
|
36
|
+
readonly params: T;
|
|
37
|
+
};
|
|
38
|
+
type QueryOption<T> = T extends undefined ? {
|
|
39
|
+
readonly query?: never;
|
|
40
|
+
} : {
|
|
41
|
+
readonly query: T;
|
|
42
|
+
};
|
|
43
|
+
type BodyOption<T> = T extends undefined ? {
|
|
44
|
+
readonly body?: never;
|
|
45
|
+
} : {
|
|
46
|
+
readonly body: T;
|
|
47
|
+
};
|
|
48
|
+
type ApiArguments<O extends AnyApiOperation> = O["params"] extends undefined ? O["query"] extends undefined ? O["body"] extends undefined ? [options?: ParamOption<O["params"]> & QueryOption<O["query"]> & BodyOption<O["body"]> & RequestOptions] : [options: ParamOption<O["params"]> & QueryOption<O["query"]> & BodyOption<O["body"]> & RequestOptions] : [options: ParamOption<O["params"]> & QueryOption<O["query"]> & BodyOption<O["body"]> & RequestOptions] : [options: ParamOption<O["params"]> & QueryOption<O["query"]> & BodyOption<O["body"]> & RequestOptions];
|
|
49
|
+
type Nest<Path extends string, Value> = Path extends `${infer Head}.${infer Tail}` ? {
|
|
50
|
+
[K in Head]: Nest<Tail, Value>;
|
|
51
|
+
} : {
|
|
52
|
+
[K in Path]: Value;
|
|
53
|
+
};
|
|
54
|
+
type Intersection<Union> = (Union extends unknown ? (value: Union) => void : never) extends (value: infer Result) => void ? Result : never;
|
|
55
|
+
type ApiMethods<Operations> = {
|
|
56
|
+
[Method in keyof Operations]: Operations[Method] extends AnyApiOperation ? (...args: ApiArguments<Operations[Method]>) => Promise<Operations[Method]["output"]> : never;
|
|
57
|
+
};
|
|
58
|
+
export type ApiClient<Contract extends Record<string, Record<string, AnyApiOperation>>> = Intersection<{
|
|
59
|
+
[Key in keyof Contract & string]: Nest<Key, ApiMethods<Contract[Key]>>;
|
|
60
|
+
}[keyof Contract & string]>;
|
|
61
|
+
export type RpcClient<Contract extends Record<string, RpcOperation>> = Intersection<{
|
|
62
|
+
[Key in keyof Contract & string]: Nest<Key, (input: Contract[Key]["input"], options?: RequestOptions) => Promise<Contract[Key]["output"]>>;
|
|
63
|
+
}[keyof Contract & string]>;
|
|
64
|
+
export declare class ClientError extends Error {
|
|
65
|
+
readonly code: string;
|
|
66
|
+
readonly status: number;
|
|
67
|
+
readonly requestId?: string | undefined;
|
|
68
|
+
constructor(code: string, status: number, requestId?: string | undefined);
|
|
69
|
+
}
|
|
70
|
+
export interface ClientOptions {
|
|
71
|
+
readonly baseUrl?: string | (() => string);
|
|
72
|
+
readonly fetch?: typeof fetch;
|
|
73
|
+
readonly csrfToken?: string | (() => string | undefined);
|
|
74
|
+
readonly headers?: HeadersInit | (() => HeadersInit);
|
|
75
|
+
}
|
|
76
|
+
/** Reads the current document token at call time, including after client navigation. */
|
|
77
|
+
export declare function createBrowserClientOptions(): ClientOptions;
|
|
78
|
+
/** Creates browser API methods from public endpoint paths, without server imports. */
|
|
79
|
+
export declare function createApiClient<Contract extends Record<string, Record<string, AnyApiOperation>>>(manifest: Pick<PublicServerManifest, "api">, options?: ClientOptions): ApiClient<Contract>;
|
|
80
|
+
/** Creates browser RPC procedures from public endpoint paths. */
|
|
81
|
+
export declare function createRpcClient<Contract extends Record<string, RpcOperation>>(manifest: Pick<PublicServerManifest, "rpc">, options?: ClientOptions): RpcClient<Contract>;
|
|
82
|
+
export {};
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/** Strips private handler paths before serializing a discovered server manifest. */
|
|
2
|
+
export function createPublicServerManifest(manifest) {
|
|
3
|
+
return {
|
|
4
|
+
api: manifest.api.map(({ clientKey, pathname, method, contentType }) => ({
|
|
5
|
+
clientKey, pathname, method, ...(contentType ? { contentType } : {}),
|
|
6
|
+
})),
|
|
7
|
+
rpc: manifest.rpc.map(({ id, pathname }) => ({ id, pathname })),
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export class ClientError extends Error {
|
|
11
|
+
code;
|
|
12
|
+
status;
|
|
13
|
+
requestId;
|
|
14
|
+
constructor(code, status, requestId) {
|
|
15
|
+
super(code);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.status = status;
|
|
18
|
+
this.requestId = requestId;
|
|
19
|
+
this.name = "ClientError";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Reads the current document token at call time, including after client navigation. */
|
|
23
|
+
export function createBrowserClientOptions() {
|
|
24
|
+
return {
|
|
25
|
+
csrfToken: () => typeof document === "undefined"
|
|
26
|
+
? undefined
|
|
27
|
+
: document.querySelector('meta[name="meeko-csrf-token"]')?.getAttribute("content") ?? undefined,
|
|
28
|
+
fetch: async (input, init) => {
|
|
29
|
+
const response = await fetch(input, init);
|
|
30
|
+
const token = response.headers.get("x-meeko-csrf-token");
|
|
31
|
+
if (token && typeof document !== "undefined") {
|
|
32
|
+
let meta = document.querySelector('meta[name="meeko-csrf-token"]');
|
|
33
|
+
if (!meta) {
|
|
34
|
+
meta = document.createElement("meta");
|
|
35
|
+
meta.setAttribute("name", "meeko-csrf-token");
|
|
36
|
+
document.head.append(meta);
|
|
37
|
+
}
|
|
38
|
+
meta.setAttribute("content", token);
|
|
39
|
+
}
|
|
40
|
+
return response;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function pathWithParams(pathname, params = {}) {
|
|
45
|
+
const used = new Set();
|
|
46
|
+
const path = pathname.replace(/\[(\.\.\.)?([A-Za-z_$][\w$]*)\]/g, (_match, rest, name) => {
|
|
47
|
+
const value = params[name];
|
|
48
|
+
if (!value)
|
|
49
|
+
throw new TypeError(`Missing route parameter: ${name}`);
|
|
50
|
+
used.add(name);
|
|
51
|
+
const parts = rest ? value.split("/") : [value];
|
|
52
|
+
if (parts.some((part) => !part || part === "." || part === ".." || part.includes("/") || part.includes("\\")))
|
|
53
|
+
throw new TypeError(`Invalid route parameter: ${name}`);
|
|
54
|
+
return parts.map(encodeURIComponent).join("/");
|
|
55
|
+
});
|
|
56
|
+
for (const key of Object.keys(params))
|
|
57
|
+
if (!used.has(key))
|
|
58
|
+
throw new TypeError(`Unknown route parameter: ${key}`);
|
|
59
|
+
return path;
|
|
60
|
+
}
|
|
61
|
+
function queryString(query) {
|
|
62
|
+
if (!query)
|
|
63
|
+
return "";
|
|
64
|
+
const search = new URLSearchParams();
|
|
65
|
+
for (const [key, raw] of Object.entries(query)) {
|
|
66
|
+
for (const value of Array.isArray(raw) ? raw : [raw]) {
|
|
67
|
+
if (value !== null && value !== undefined)
|
|
68
|
+
search.append(key, String(value));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const result = search.toString();
|
|
72
|
+
return result ? `?${result}` : "";
|
|
73
|
+
}
|
|
74
|
+
async function responseValue(response, contentType = "json") {
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
let payload;
|
|
77
|
+
try {
|
|
78
|
+
payload = await response.json();
|
|
79
|
+
}
|
|
80
|
+
catch { /* Bad error bodies still produce a normalized error. */ }
|
|
81
|
+
const record = payload && typeof payload === "object" ? payload : {};
|
|
82
|
+
const code = typeof record.code === "string" ? record.code : "HTTP_ERROR";
|
|
83
|
+
const requestId = typeof record.requestId === "string" ? record.requestId : response.headers.get("x-request-id") ?? undefined;
|
|
84
|
+
throw new ClientError(code, response.status, requestId);
|
|
85
|
+
}
|
|
86
|
+
if (contentType === "response")
|
|
87
|
+
return response;
|
|
88
|
+
if (response.status === 204)
|
|
89
|
+
return undefined;
|
|
90
|
+
try {
|
|
91
|
+
return await response.json();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
throw new ClientError("INVALID_RESPONSE", response.status, response.headers.get("x-request-id") ?? undefined);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
function endpointUrl(pathname, options) {
|
|
98
|
+
const baseUrl = typeof options.baseUrl === "function" ? options.baseUrl() : options.baseUrl;
|
|
99
|
+
return baseUrl ? new URL(pathname, baseUrl).toString() : pathname;
|
|
100
|
+
}
|
|
101
|
+
function headersFor(options, method, json) {
|
|
102
|
+
const headers = new Headers(typeof options.headers === "function" ? options.headers() : options.headers);
|
|
103
|
+
if (json)
|
|
104
|
+
headers.set("content-type", "application/json");
|
|
105
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
106
|
+
const csrf = typeof options.csrfToken === "function" ? options.csrfToken() : options.csrfToken;
|
|
107
|
+
if (csrf)
|
|
108
|
+
headers.set("x-meeko-csrf", csrf);
|
|
109
|
+
}
|
|
110
|
+
return headers;
|
|
111
|
+
}
|
|
112
|
+
function insert(root, key, value) {
|
|
113
|
+
const parts = key.split(".");
|
|
114
|
+
if (parts.some((part) => !/^[A-Za-z_$][\w$]*$/.test(part)))
|
|
115
|
+
throw new TypeError(`Invalid client key: ${key}`);
|
|
116
|
+
let node = root;
|
|
117
|
+
for (const part of parts.slice(0, -1)) {
|
|
118
|
+
const existing = node[part];
|
|
119
|
+
if (existing === undefined)
|
|
120
|
+
node[part] = Object.create(null);
|
|
121
|
+
else if (!existing || typeof existing !== "object")
|
|
122
|
+
throw new TypeError(`Client key collision: ${key}`);
|
|
123
|
+
node = node[part];
|
|
124
|
+
}
|
|
125
|
+
const last = parts.at(-1);
|
|
126
|
+
if (node[last] !== undefined)
|
|
127
|
+
throw new TypeError(`Client key collision: ${key}`);
|
|
128
|
+
node[last] = value;
|
|
129
|
+
}
|
|
130
|
+
function ensureObject(root, key) {
|
|
131
|
+
const parts = key.split(".");
|
|
132
|
+
if (parts.some((part) => !/^[A-Za-z_$][\w$]*$/.test(part)))
|
|
133
|
+
throw new TypeError(`Invalid client key: ${key}`);
|
|
134
|
+
let node = root;
|
|
135
|
+
for (const part of parts) {
|
|
136
|
+
const existing = node[part];
|
|
137
|
+
if (existing === undefined)
|
|
138
|
+
node[part] = Object.create(null);
|
|
139
|
+
else if (!existing || typeof existing !== "object")
|
|
140
|
+
throw new TypeError(`Client key collision: ${key}`);
|
|
141
|
+
node = node[part];
|
|
142
|
+
}
|
|
143
|
+
return node;
|
|
144
|
+
}
|
|
145
|
+
/** Creates browser API methods from public endpoint paths, without server imports. */
|
|
146
|
+
export function createApiClient(manifest, options = {}) {
|
|
147
|
+
const root = Object.create(null);
|
|
148
|
+
for (const endpoint of manifest.api) {
|
|
149
|
+
const methods = ensureObject(root, endpoint.clientKey);
|
|
150
|
+
const method = endpoint.method.toLowerCase();
|
|
151
|
+
if (methods[method])
|
|
152
|
+
throw new TypeError(`Duplicate API method: ${endpoint.clientKey}.${method}`);
|
|
153
|
+
methods[method] = async (input = {}) => {
|
|
154
|
+
const path = pathWithParams(endpoint.pathname, input.params) + queryString(input.query);
|
|
155
|
+
const url = endpointUrl(path, options);
|
|
156
|
+
const fetcher = options.fetch ?? fetch;
|
|
157
|
+
const response = await fetcher(url, {
|
|
158
|
+
method: endpoint.method, headers: headersFor(options, endpoint.method, input.body !== undefined),
|
|
159
|
+
credentials: "same-origin", ...(input.body === undefined ? {} : { body: JSON.stringify(input.body) }),
|
|
160
|
+
...(input.signal ? { signal: input.signal } : {}),
|
|
161
|
+
});
|
|
162
|
+
return responseValue(response, endpoint.contentType);
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return root;
|
|
166
|
+
}
|
|
167
|
+
/** Creates browser RPC procedures from public endpoint paths. */
|
|
168
|
+
export function createRpcClient(manifest, options = {}) {
|
|
169
|
+
const root = Object.create(null);
|
|
170
|
+
for (const endpoint of manifest.rpc) {
|
|
171
|
+
insert(root, endpoint.id, async (input, request = {}) => {
|
|
172
|
+
const fetcher = options.fetch ?? fetch;
|
|
173
|
+
const response = await fetcher(endpointUrl(endpoint.pathname, options), {
|
|
174
|
+
method: "POST", headers: headersFor(options, "POST", true), credentials: "same-origin",
|
|
175
|
+
body: JSON.stringify(input), ...(request.signal ? { signal: request.signal } : {}),
|
|
176
|
+
});
|
|
177
|
+
return responseValue(response);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return root;
|
|
181
|
+
}
|
|
182
|
+
//# sourceMappingURL=clients.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clients.js","sourceRoot":"","sources":["../../src/framework/clients.ts"],"names":[],"mappings":"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","sourcesContent":["/** Public descriptors intentionally omit handler files and server schemas. */\nexport interface PublicApiEndpoint {\n readonly clientKey: string;\n readonly pathname: string;\n readonly method: \"GET\" | \"POST\" | \"PUT\" | \"PATCH\" | \"DELETE\";\n readonly contentType?: \"json\" | \"response\";\n}\n\nexport interface PublicRpcEndpoint {\n readonly id: string;\n readonly pathname: string;\n}\n\nexport interface PublicServerManifest {\n readonly api: readonly PublicApiEndpoint[];\n readonly rpc: readonly PublicRpcEndpoint[];\n}\n\n/** Strips private handler paths before serializing a discovered server manifest. */\nexport function createPublicServerManifest(manifest: PublicServerManifest): PublicServerManifest {\n return {\n api: manifest.api.map(({ clientKey, pathname, method, contentType }) => ({\n clientKey, pathname, method, ...(contentType ? { contentType } : {}),\n })),\n rpc: manifest.rpc.map(({ id, pathname }) => ({ id, pathname })),\n };\n}\n\n/** Type-only operation declarations can be inferred from public shared schemas. */\nexport interface ApiOperation<Params = undefined, Query = undefined, Body = undefined, Output = unknown> {\n readonly params: Params;\n readonly query: Query;\n readonly body: Body;\n readonly output: Output;\n}\n\nexport interface RpcOperation<Input = unknown, Output = unknown> {\n readonly input: Input;\n readonly output: Output;\n}\n\ntype AnyApiOperation = ApiOperation<unknown, unknown, unknown, unknown>;\n\ntype SearchValue = string | number | boolean | null | undefined;\ntype QueryValue = SearchValue | readonly SearchValue[];\ntype RequestOptions = { readonly signal?: AbortSignal };\ntype ParamOption<T> = T extends undefined ? { readonly params?: never } : { readonly params: T };\ntype QueryOption<T> = T extends undefined ? { readonly query?: never } : { readonly query: T };\ntype BodyOption<T> = T extends undefined ? { readonly body?: never } : { readonly body: T };\ntype ApiArguments<O extends AnyApiOperation> = O[\"params\"] extends undefined\n ? O[\"query\"] extends undefined\n ? O[\"body\"] extends undefined ? [options?: ParamOption<O[\"params\"]> & QueryOption<O[\"query\"]> & BodyOption<O[\"body\"]> & RequestOptions] : [options: ParamOption<O[\"params\"]> & QueryOption<O[\"query\"]> & BodyOption<O[\"body\"]> & RequestOptions]\n : [options: ParamOption<O[\"params\"]> & QueryOption<O[\"query\"]> & BodyOption<O[\"body\"]> & RequestOptions]\n : [options: ParamOption<O[\"params\"]> & QueryOption<O[\"query\"]> & BodyOption<O[\"body\"]> & RequestOptions];\ntype Nest<Path extends string, Value> = Path extends `${infer Head}.${infer Tail}` ? { [K in Head]: Nest<Tail, Value> } : { [K in Path]: Value };\ntype Intersection<Union> = (Union extends unknown ? (value: Union) => void : never) extends (value: infer Result) => void ? Result : never;\ntype ApiMethods<Operations> = { [Method in keyof Operations]: Operations[Method] extends AnyApiOperation\n ? (...args: ApiArguments<Operations[Method]>) => Promise<Operations[Method][\"output\"]>\n : never };\nexport type ApiClient<Contract extends Record<string, Record<string, AnyApiOperation>>> = Intersection<{\n [Key in keyof Contract & string]: Nest<Key, ApiMethods<Contract[Key]>>\n}[keyof Contract & string]>;\nexport type RpcClient<Contract extends Record<string, RpcOperation>> = Intersection<{\n [Key in keyof Contract & string]: Nest<Key, (input: Contract[Key][\"input\"], options?: RequestOptions) => Promise<Contract[Key][\"output\"]>>\n}[keyof Contract & string]>;\n\nexport class ClientError extends Error {\n constructor(public readonly code: string, public readonly status: number, public readonly requestId?: string) {\n super(code);\n this.name = \"ClientError\";\n }\n}\n\nexport interface ClientOptions {\n readonly baseUrl?: string | (() => string);\n readonly fetch?: typeof fetch;\n readonly csrfToken?: string | (() => string | undefined);\n readonly headers?: HeadersInit | (() => HeadersInit);\n}\n\n/** Reads the current document token at call time, including after client navigation. */\nexport function createBrowserClientOptions(): ClientOptions {\n return {\n csrfToken: () => typeof document === \"undefined\"\n ? undefined\n : document.querySelector('meta[name=\"meeko-csrf-token\"]')?.getAttribute(\"content\") ?? undefined,\n fetch: async (input, init) => {\n const response = await fetch(input, init);\n const token = response.headers.get(\"x-meeko-csrf-token\");\n if (token && typeof document !== \"undefined\") {\n let meta = document.querySelector('meta[name=\"meeko-csrf-token\"]');\n if (!meta) {\n meta = document.createElement(\"meta\");\n meta.setAttribute(\"name\", \"meeko-csrf-token\");\n document.head.append(meta);\n }\n meta.setAttribute(\"content\", token);\n }\n return response;\n },\n };\n}\n\nfunction pathWithParams(pathname: string, params: Readonly<Record<string, string>> = {}): string {\n const used = new Set<string>();\n const path = pathname.replace(/\\[(\\.\\.\\.)?([A-Za-z_$][\\w$]*)\\]/g, (_match, rest: string | undefined, name: string) => {\n const value = params[name];\n if (!value) throw new TypeError(`Missing route parameter: ${name}`);\n used.add(name);\n const parts = rest ? value.split(\"/\") : [value];\n if (parts.some((part) => !part || part === \".\" || part === \"..\" || part.includes(\"/\") || part.includes(\"\\\\\"))) throw new TypeError(`Invalid route parameter: ${name}`);\n return parts.map(encodeURIComponent).join(\"/\");\n });\n for (const key of Object.keys(params)) if (!used.has(key)) throw new TypeError(`Unknown route parameter: ${key}`);\n return path;\n}\n\nfunction queryString(query: Readonly<Record<string, QueryValue>> | undefined): string {\n if (!query) return \"\";\n const search = new URLSearchParams();\n for (const [key, raw] of Object.entries(query)) {\n for (const value of Array.isArray(raw) ? raw : [raw]) {\n if (value !== null && value !== undefined) search.append(key, String(value));\n }\n }\n const result = search.toString();\n return result ? `?${result}` : \"\";\n}\n\nasync function responseValue(response: Response, contentType: \"json\" | \"response\" = \"json\"): Promise<unknown> {\n if (!response.ok) {\n let payload: unknown;\n try { payload = await response.json(); } catch { /* Bad error bodies still produce a normalized error. */ }\n const record = payload && typeof payload === \"object\" ? payload as Record<string, unknown> : {};\n const code = typeof record.code === \"string\" ? record.code : \"HTTP_ERROR\";\n const requestId = typeof record.requestId === \"string\" ? record.requestId : response.headers.get(\"x-request-id\") ?? undefined;\n throw new ClientError(code, response.status, requestId);\n }\n if (contentType === \"response\") return response;\n if (response.status === 204) return undefined;\n try { return await response.json(); }\n catch { throw new ClientError(\"INVALID_RESPONSE\", response.status, response.headers.get(\"x-request-id\") ?? undefined); }\n}\n\nfunction endpointUrl(pathname: string, options: ClientOptions): string {\n const baseUrl = typeof options.baseUrl === \"function\" ? options.baseUrl() : options.baseUrl;\n return baseUrl ? new URL(pathname, baseUrl).toString() : pathname;\n}\n\nfunction headersFor(options: ClientOptions, method: string, json: boolean): Headers {\n const headers = new Headers(typeof options.headers === \"function\" ? options.headers() : options.headers);\n if (json) headers.set(\"content-type\", \"application/json\");\n if (method !== \"GET\" && method !== \"HEAD\") {\n const csrf = typeof options.csrfToken === \"function\" ? options.csrfToken() : options.csrfToken;\n if (csrf) headers.set(\"x-meeko-csrf\", csrf);\n }\n return headers;\n}\n\nfunction insert(root: Record<string, unknown>, key: string, value: unknown): void {\n const parts = key.split(\".\");\n if (parts.some((part) => !/^[A-Za-z_$][\\w$]*$/.test(part))) throw new TypeError(`Invalid client key: ${key}`);\n let node = root;\n for (const part of parts.slice(0, -1)) {\n const existing = node[part];\n if (existing === undefined) node[part] = Object.create(null) as Record<string, unknown>;\n else if (!existing || typeof existing !== \"object\") throw new TypeError(`Client key collision: ${key}`);\n node = node[part] as Record<string, unknown>;\n }\n const last = parts.at(-1)!;\n if (node[last] !== undefined) throw new TypeError(`Client key collision: ${key}`);\n node[last] = value;\n}\n\nfunction ensureObject(root: Record<string, unknown>, key: string): Record<string, unknown> {\n const parts = key.split(\".\");\n if (parts.some((part) => !/^[A-Za-z_$][\\w$]*$/.test(part))) throw new TypeError(`Invalid client key: ${key}`);\n let node = root;\n for (const part of parts) {\n const existing = node[part];\n if (existing === undefined) node[part] = Object.create(null) as Record<string, unknown>;\n else if (!existing || typeof existing !== \"object\") throw new TypeError(`Client key collision: ${key}`);\n node = node[part] as Record<string, unknown>;\n }\n return node;\n}\n\n/** Creates browser API methods from public endpoint paths, without server imports. */\nexport function createApiClient<Contract extends Record<string, Record<string, AnyApiOperation>>>(manifest: Pick<PublicServerManifest, \"api\">, options: ClientOptions = {}): ApiClient<Contract> {\n const root: Record<string, unknown> = Object.create(null);\n for (const endpoint of manifest.api) {\n const methods = ensureObject(root, endpoint.clientKey);\n const method = endpoint.method.toLowerCase();\n if (methods[method]) throw new TypeError(`Duplicate API method: ${endpoint.clientKey}.${method}`);\n methods[method] = async (input: { params?: Record<string, string>; query?: Record<string, QueryValue>; body?: unknown; signal?: AbortSignal } = {}) => {\n const path = pathWithParams(endpoint.pathname, input.params) + queryString(input.query);\n const url = endpointUrl(path, options);\n const fetcher = options.fetch ?? fetch;\n const response = await fetcher(url, {\n method: endpoint.method, headers: headersFor(options, endpoint.method, input.body !== undefined),\n credentials: \"same-origin\", ...(input.body === undefined ? {} : { body: JSON.stringify(input.body) }),\n ...(input.signal ? { signal: input.signal } : {}),\n });\n return responseValue(response, endpoint.contentType);\n };\n }\n return root as ApiClient<Contract>;\n}\n\n/** Creates browser RPC procedures from public endpoint paths. */\nexport function createRpcClient<Contract extends Record<string, RpcOperation>>(manifest: Pick<PublicServerManifest, \"rpc\">, options: ClientOptions = {}): RpcClient<Contract> {\n const root: Record<string, unknown> = Object.create(null);\n for (const endpoint of manifest.rpc) {\n insert(root, endpoint.id, async (input: unknown, request: RequestOptions = {}) => {\n const fetcher = options.fetch ?? fetch;\n const response = await fetcher(endpointUrl(endpoint.pathname, options), {\n method: \"POST\", headers: headersFor(options, \"POST\", true), credentials: \"same-origin\",\n body: JSON.stringify(input), ...(request.signal ? { signal: request.signal } : {}),\n });\n return responseValue(response);\n });\n }\n return root as RpcClient<Contract>;\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface MeekoConfig {
|
|
2
|
+
/** Layers are applied after the application, in declaration order. Collisions fail. */
|
|
3
|
+
readonly extends?: readonly string[];
|
|
4
|
+
/** Modules use the same app/pages convention as layers. */
|
|
5
|
+
readonly modules?: readonly string[];
|
|
6
|
+
readonly imports?: {
|
|
7
|
+
readonly serverDirs?: readonly string[];
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export declare function defineMeekoConfig<const T extends MeekoConfig>(config: T): T;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-type.js","sourceRoot":"","sources":["../../src/framework/config-type.ts"],"names":[],"mappings":"AAQA,MAAM,UAAU,iBAAiB,CAA8B,MAAS;IACtE,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["export interface MeekoConfig {\n /** Layers are applied after the application, in declaration order. Collisions fail. */\n readonly extends?: readonly string[];\n /** Modules use the same app/pages convention as layers. */\n readonly modules?: readonly string[];\n readonly imports?: { readonly serverDirs?: readonly string[] };\n}\n\nexport function defineMeekoConfig<const T extends MeekoConfig>(config: T): T {\n return config;\n}\n"]}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RouteRecord } from "./router.js";
|
|
2
|
+
import type { MeekoConfig } from "./config-type.js";
|
|
3
|
+
export type { MeekoConfig } from "./config-type.js";
|
|
4
|
+
export interface ConfigSource {
|
|
5
|
+
readonly name: string;
|
|
6
|
+
readonly root: string;
|
|
7
|
+
readonly kind: "app" | "layer" | "module";
|
|
8
|
+
}
|
|
9
|
+
/** Loads the optional project configuration using Node 24's TypeScript support. */
|
|
10
|
+
export declare function loadMeekoConfig(projectRoot: string): Promise<MeekoConfig>;
|
|
11
|
+
/** Resolves sources and rejects accidental escape or duplicate names before discovery. */
|
|
12
|
+
/** Reads static server import directories for synchronous editor discovery. */
|
|
13
|
+
export declare function readMeekoConfigSync(projectRoot: string): MeekoConfig;
|
|
14
|
+
export declare function resolveConfigSources(projectRoot: string, config?: MeekoConfig): Promise<readonly ConfigSource[]>;
|
|
15
|
+
export declare function discoverConfiguredRoutes(projectRoot: string): Promise<readonly RouteRecord[]>;
|