@ilha/router 0.8.13 → 0.9.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/README.md +1 -1
- package/dist/codegen.d.ts +3 -0
- package/dist/index.d.ts +28 -3
- package/dist/index.js +2231 -2
- package/dist/{plugin-DdquB3Nf.js → plugin-DPGzrIVj.js} +510 -15
- package/dist/plugin.d.ts +9 -1
- package/dist/request-scope-D6_4rqMb.js +27 -0
- package/dist/request-scope.d.ts +20 -0
- package/dist/rolldown.d.ts +1 -0
- package/dist/rolldown.js +2 -2
- package/dist/rspack.d.ts +1 -0
- package/dist/rspack.js +2 -2
- package/dist/server-island-registry.d.ts +77 -0
- package/dist/server-island-registry.js +130 -0
- package/dist/server-island.d.ts +44 -0
- package/dist/server-island.js +229 -0
- package/dist/server-islands.d.ts +72 -0
- package/dist/ssr.d.ts +17 -88
- package/dist/ssr.js +132 -130
- package/dist/vite.d.ts +1 -0
- package/dist/vite.js +2 -2
- package/package.json +12 -5
- package/dist/src-BKWkRtMx.js +0 -2119
package/dist/ssr.d.ts
CHANGED
|
@@ -1,94 +1,23 @@
|
|
|
1
|
-
import "ilha:loaders";
|
|
2
|
-
import type { HeadInput, HydratableRenderOptions, SerializedHead } from "./index";
|
|
3
|
-
export interface AssetAttrs {
|
|
4
|
-
href: string;
|
|
5
|
-
"data-vite-dev-id"?: string;
|
|
6
|
-
}
|
|
7
|
-
/** Manifest slice without `merge` — matches Nitro/Vite `?assets` raw shape. */
|
|
8
|
-
export interface AssetsRaw {
|
|
9
|
-
entry?: string;
|
|
10
|
-
js: AssetAttrs[];
|
|
11
|
-
css: AssetAttrs[];
|
|
12
|
-
}
|
|
13
1
|
/**
|
|
14
|
-
*
|
|
15
|
-
* imports. `merge` combines several descriptors, de-duping by href.
|
|
2
|
+
* Production SSR endpoints for server-owned islands and regular-page loads.
|
|
16
3
|
*
|
|
17
|
-
*
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
merge(...others: AssetsRaw[]): Assets;
|
|
21
|
-
};
|
|
22
|
-
/** Client + SSR manifests from `?assets=client` / `?assets=ssr` imports. */
|
|
23
|
-
export interface IlhaAssetsPair {
|
|
24
|
-
client: Assets;
|
|
25
|
-
server: Assets;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Merge client and SSR asset manifests (same as `client.merge(server)` from Nitro).
|
|
29
|
-
*/
|
|
30
|
-
export declare function mergeAssets({ client, server }: IlhaAssetsPair): Assets;
|
|
31
|
-
/**
|
|
32
|
-
* App-wide `<head>` defaults for `IlhaHandler`. Same shape as route `head()`;
|
|
33
|
-
* use so entry files read clearly: `head: appHead({ title, script: [...] })`.
|
|
34
|
-
*/
|
|
35
|
-
export declare function appHead(head: HeadInput): HeadInput;
|
|
36
|
-
export interface IlhaHandlerOptions {
|
|
37
|
-
/**
|
|
38
|
-
* Merged asset manifest — use `mergeAssets({ client, server })` with imports
|
|
39
|
-
* from `?assets=client` and `?assets=ssr`.
|
|
40
|
-
*/
|
|
41
|
-
assets: Assets;
|
|
42
|
-
/** `<html lang>` value. Default: `"en"`. */
|
|
43
|
-
lang?: string;
|
|
44
|
-
/** Id of the hydration mount container. Default: `"app"`. */
|
|
45
|
-
appId?: string;
|
|
46
|
-
/** Fallback client entry used when the merged assets have no `entry`. */
|
|
47
|
-
clientEntry?: string;
|
|
48
|
-
/**
|
|
49
|
-
* App-wide `<head>` defaults — same shape as the `head()` API. Route-level
|
|
50
|
-
* head (from loaders/pages) is merged on top, so anything here acts as a
|
|
51
|
-
* base. Use it for the default `title`, app meta, and inline scripts.
|
|
52
|
-
*
|
|
53
|
-
* @example
|
|
54
|
-
* head: {
|
|
55
|
-
* title: "Ilha + Oxide",
|
|
56
|
-
* script: [{ children: themeScript }],
|
|
57
|
-
* }
|
|
58
|
-
*/
|
|
59
|
-
head?: HeadInput;
|
|
60
|
-
/** Options forwarded to `pageRouter.renderHydratable` / `renderResponse`. */
|
|
61
|
-
renderOptions?: HydratableRenderOptions;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* SSR host helper for Ilha apps. Wires the generated `pageRouter` /
|
|
65
|
-
* `registry` and the asset manifest into a single `fetch`-style handler so a
|
|
66
|
-
* host entry collapses to:
|
|
4
|
+
* Default export is an oxidejs-style fetch middleware:
|
|
5
|
+
* `(request) => Response | undefined`. Returns `undefined` for any request it
|
|
6
|
+
* does not own, so hosts can chain it ahead of their own handler:
|
|
67
7
|
*
|
|
68
8
|
* ```ts
|
|
69
|
-
*
|
|
70
|
-
* import client from "./entry-client.ts?assets=client";
|
|
71
|
-
* import server from "./entry-server.ts?assets=ssr";
|
|
72
|
-
*
|
|
73
|
-
* const handler = new IlhaHandler({
|
|
74
|
-
* assets: mergeAssets({ client, server }),
|
|
75
|
-
* head: appHead({ title: "My app", script: [{ children: "..." }] }),
|
|
76
|
-
* });
|
|
77
|
-
*
|
|
78
|
-
* export default { fetch: (request: Request) => handler.handle(request) };
|
|
9
|
+
* oxide({ middleware: ["@ilha/router/ssr"] });
|
|
79
10
|
* ```
|
|
11
|
+
*
|
|
12
|
+
* Serves:
|
|
13
|
+
* - `POST /__ilha/frame` — re-renders a server island (JSON `{ id, path }` in,
|
|
14
|
+
* `{ html }` out). Renderers come from the process-global registry
|
|
15
|
+
* populated by self-registration code appended to `.server` modules.
|
|
16
|
+
* - `GET /__ilha/loader?path=…` — regular-page server loads via the loader
|
|
17
|
+
* runner (`setFrameLoaderRunner`, wired by the generated server module).
|
|
80
18
|
*/
|
|
81
|
-
export declare
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
private readonly head;
|
|
87
|
-
private readonly renderOptions;
|
|
88
|
-
constructor(options: IlhaHandlerOptions);
|
|
89
|
-
/** Render the document shell around an already-rendered island body. */
|
|
90
|
-
document(body: string, head?: SerializedHead): string;
|
|
91
|
-
/** Handle an incoming request and return a full HTML / redirect Response. */
|
|
92
|
-
handle(request: Request): Promise<Response>;
|
|
93
|
-
private handleInner;
|
|
94
|
-
}
|
|
19
|
+
export declare const FRAME_ENDPOINT = "/__ilha/frame";
|
|
20
|
+
/** Regular-page server loads: served through the loader-runner slot. */
|
|
21
|
+
export declare const LOADER_ENDPOINT = "/__ilha/loader";
|
|
22
|
+
declare function ssr(request: Request): Promise<Response | undefined>;
|
|
23
|
+
export default ssr;
|
package/dist/ssr.js
CHANGED
|
@@ -1,152 +1,154 @@
|
|
|
1
|
-
import { t as
|
|
2
|
-
import {
|
|
3
|
-
import "ilha:loaders";
|
|
1
|
+
import { t as runWithIslandRequest } from "./request-scope-D6_4rqMb.js";
|
|
2
|
+
import { FrameError, getFrameGuard, getFrameLoaderRunner, renderServerIsland } from "./server-island-registry.js";
|
|
4
3
|
|
|
5
4
|
//#region src/ssr.ts
|
|
6
5
|
/**
|
|
7
|
-
*
|
|
8
|
-
*/
|
|
9
|
-
function mergeAssets({ client, server }) {
|
|
10
|
-
return client.merge(server);
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* App-wide `<head>` defaults for `IlhaHandler`. Same shape as route `head()`;
|
|
14
|
-
* use so entry files read clearly: `head: appHead({ title, script: [...] })`.
|
|
15
|
-
*/
|
|
16
|
-
function appHead(head) {
|
|
17
|
-
return head;
|
|
18
|
-
}
|
|
19
|
-
const ATTR_ESC = {
|
|
20
|
-
"&": "&",
|
|
21
|
-
"<": "<",
|
|
22
|
-
">": ">",
|
|
23
|
-
"\"": """,
|
|
24
|
-
"'": "'"
|
|
25
|
-
};
|
|
26
|
-
function escapeAttr(value) {
|
|
27
|
-
return value.replace(/[&<>"']/g, (c) => ATTR_ESC[c]);
|
|
28
|
-
}
|
|
29
|
-
function stylesheetTag(attrs) {
|
|
30
|
-
const devId = attrs["data-vite-dev-id"] ? ` data-vite-dev-id="${escapeAttr(attrs["data-vite-dev-id"])}"` : "";
|
|
31
|
-
return `<link rel="stylesheet" href="${escapeAttr(attrs.href)}"${devId} />`;
|
|
32
|
-
}
|
|
33
|
-
/**
|
|
34
|
-
* SSR host helper for Ilha apps. Wires the generated `pageRouter` /
|
|
35
|
-
* `registry` and the asset manifest into a single `fetch`-style handler so a
|
|
36
|
-
* host entry collapses to:
|
|
6
|
+
* Production SSR endpoints for server-owned islands and regular-page loads.
|
|
37
7
|
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
* import server from "./entry-server.ts?assets=ssr";
|
|
8
|
+
* Default export is an oxidejs-style fetch middleware:
|
|
9
|
+
* `(request) => Response | undefined`. Returns `undefined` for any request it
|
|
10
|
+
* does not own, so hosts can chain it ahead of their own handler:
|
|
42
11
|
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
* head: appHead({ title: "My app", script: [{ children: "..." }] }),
|
|
46
|
-
* });
|
|
47
|
-
*
|
|
48
|
-
* export default { fetch: (request: Request) => handler.handle(request) };
|
|
12
|
+
* ```ts
|
|
13
|
+
* oxide({ middleware: ["@ilha/router/ssr"] });
|
|
49
14
|
* ```
|
|
15
|
+
*
|
|
16
|
+
* Serves:
|
|
17
|
+
* - `POST /__ilha/frame` — re-renders a server island (JSON `{ id, path }` in,
|
|
18
|
+
* `{ html }` out). Renderers come from the process-global registry
|
|
19
|
+
* populated by self-registration code appended to `.server` modules.
|
|
20
|
+
* - `GET /__ilha/loader?path=…` — regular-page server loads via the loader
|
|
21
|
+
* runner (`setFrameLoaderRunner`, wired by the generated server module).
|
|
50
22
|
*/
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const routeHead = head?.headTags ? `\n ${head.headTags}` : "";
|
|
72
|
-
const htmlAttrsStr = head?.htmlAttrs ?? "";
|
|
73
|
-
const langFromHead = htmlAttrsStr.match(/\blang="([^"]*)"/)?.[1];
|
|
74
|
-
return `<!doctype html>
|
|
75
|
-
<html lang="${langFromHead == null ? this.lang : langFromHead}"${htmlAttrsStr.replace(/\s*lang="[^"]*"/, "")}>
|
|
76
|
-
<head>
|
|
77
|
-
<meta charset="UTF-8" />
|
|
78
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
79
|
-
${titleTag}<link rel="icon" href="/favicon.svg" />
|
|
80
|
-
${styles}${routeHead}
|
|
81
|
-
</head>
|
|
82
|
-
<body${head?.bodyAttrs ?? ""}>
|
|
83
|
-
<div id="${escapeAttr(this.appId)}">${body}</div>
|
|
84
|
-
<script type="module" src="${escapeAttr(clientEntry)}"><\/script>
|
|
85
|
-
</body>
|
|
86
|
-
</html>`;
|
|
23
|
+
const FRAME_ENDPOINT = "/__ilha/frame";
|
|
24
|
+
/** Regular-page server loads: served through the loader-runner slot. */
|
|
25
|
+
const LOADER_ENDPOINT = "/__ilha/loader";
|
|
26
|
+
/** Max request body size — matches the dev middleware cap. */
|
|
27
|
+
const MAX_BODY = 16384;
|
|
28
|
+
function json(status, body) {
|
|
29
|
+
return new Response(JSON.stringify(body), {
|
|
30
|
+
status,
|
|
31
|
+
headers: {
|
|
32
|
+
"cache-control": "no-store",
|
|
33
|
+
"content-type": "application/json;charset=utf-8"
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
async function ssr(request) {
|
|
38
|
+
let pathname;
|
|
39
|
+
try {
|
|
40
|
+
pathname = new URL(request.url).pathname;
|
|
41
|
+
} catch {
|
|
42
|
+
return json(400, { error: "frame failed" });
|
|
87
43
|
}
|
|
88
|
-
|
|
89
|
-
|
|
44
|
+
if (pathname !== "/__ilha/frame" && pathname !== "/__ilha/loader") return;
|
|
45
|
+
const origin = request.headers.get("origin");
|
|
46
|
+
const host = request.headers.get("host");
|
|
47
|
+
if (origin && host && origin !== `http://${host}` && origin !== `https://${host}`) return json(403, { error: "frame failed" });
|
|
48
|
+
if (pathname === "/__ilha/loader") {
|
|
49
|
+
if (request.method !== "GET") return json(405, { error: "method not allowed" });
|
|
90
50
|
try {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return
|
|
95
|
-
status: 500,
|
|
96
|
-
headers: { "content-type": "text/plain;charset=utf-8" }
|
|
97
|
-
});
|
|
51
|
+
const denied = await getFrameGuard()?.(request);
|
|
52
|
+
if (denied) return denied;
|
|
53
|
+
} catch {
|
|
54
|
+
return json(403, { error: "loader failed" });
|
|
98
55
|
}
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
56
|
+
const runner = getFrameLoaderRunner();
|
|
57
|
+
if (!runner) return json(404, {
|
|
58
|
+
kind: "error",
|
|
59
|
+
status: 404,
|
|
60
|
+
message: "not found"
|
|
61
|
+
});
|
|
62
|
+
const cl = request.headers.get("content-length");
|
|
63
|
+
if (cl && Number(cl) > MAX_BODY) return json(413, { error: "frame failed" });
|
|
64
|
+
let target = "/";
|
|
102
65
|
try {
|
|
103
|
-
|
|
66
|
+
target = new URL(request.url).searchParams.get("path") ?? "/";
|
|
104
67
|
} catch {
|
|
105
|
-
return
|
|
68
|
+
return json(400, {
|
|
69
|
+
kind: "error",
|
|
106
70
|
status: 400,
|
|
107
|
-
|
|
71
|
+
message: "bad request"
|
|
108
72
|
});
|
|
109
73
|
}
|
|
110
|
-
if (
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
74
|
+
if (!target.startsWith("/") || target.includes("//") || target.length > 2048) return json(400, {
|
|
75
|
+
kind: "error",
|
|
76
|
+
status: 400,
|
|
77
|
+
message: "bad request"
|
|
78
|
+
});
|
|
79
|
+
try {
|
|
80
|
+
const result = await runner(target);
|
|
81
|
+
if (result.kind === "redirect") return json(result.status || 302, {
|
|
82
|
+
kind: "redirect",
|
|
83
|
+
to: result.to,
|
|
84
|
+
status: result.status
|
|
121
85
|
});
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
86
|
+
if (result.kind !== "data") {
|
|
87
|
+
const status = result.status || 500;
|
|
88
|
+
return json(status, {
|
|
89
|
+
kind: result.kind,
|
|
90
|
+
status,
|
|
91
|
+
message: result.message
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return json(200, result);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error("[ilha-router] loader endpoint failed:", error);
|
|
97
|
+
return json(500, {
|
|
98
|
+
kind: "error",
|
|
99
|
+
status: 500,
|
|
100
|
+
message: "loader failed"
|
|
131
101
|
});
|
|
132
102
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
103
|
+
}
|
|
104
|
+
if (request.method !== "POST") return json(405, { error: "frame failed" });
|
|
105
|
+
if (!(request.headers.get("content-type") ?? "").startsWith("application/json")) return json(415, { error: "frame failed" });
|
|
106
|
+
try {
|
|
107
|
+
const denied = await getFrameGuard()?.(request);
|
|
108
|
+
if (denied) return denied;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
console.error("[ilha-router] frame guard failed:", error);
|
|
111
|
+
return json(403, { error: "frame failed" });
|
|
112
|
+
}
|
|
113
|
+
let id;
|
|
114
|
+
let path = "/";
|
|
115
|
+
try {
|
|
116
|
+
const text = await request.text();
|
|
117
|
+
if (text.length > MAX_BODY) return json(413, { error: "frame failed" });
|
|
118
|
+
const body = JSON.parse(text);
|
|
119
|
+
id = String(body.id ?? "");
|
|
120
|
+
if (typeof body.path === "string" && body.path.startsWith("/") && !body.path.includes("//") && body.path.length <= 2048) path = body.path;
|
|
121
|
+
} catch {
|
|
122
|
+
return json(400, { error: "frame failed" });
|
|
123
|
+
}
|
|
124
|
+
try {
|
|
125
|
+
const headers = new Headers();
|
|
126
|
+
for (const name of [
|
|
127
|
+
"cookie",
|
|
128
|
+
"authorization",
|
|
129
|
+
"user-agent",
|
|
130
|
+
"x-forwarded-for"
|
|
131
|
+
]) {
|
|
132
|
+
const value = request.headers.get(name);
|
|
133
|
+
if (value !== null) headers.set(name, value);
|
|
134
|
+
}
|
|
135
|
+
const scoped = new Request(new URL(path, `http://${host ?? "localhost"}`), {
|
|
136
|
+
method: "POST",
|
|
137
|
+
headers
|
|
147
138
|
});
|
|
139
|
+
return json(200, { html: await renderServerIsland(id, scoped, (scopedRequest, fn) => Promise.resolve(runWithIslandRequest(scopedRequest, fn))) });
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error instanceof FrameError) {
|
|
142
|
+
if (error.redirect) return json(error.status, { redirect: error.redirect });
|
|
143
|
+
if (error.status >= 500) console.error("[ilha-router] frame render failed:", error);
|
|
144
|
+
return json(error.status, { error: "frame failed" });
|
|
145
|
+
}
|
|
146
|
+
console.error("[ilha-router] frame render failed:", error);
|
|
147
|
+
return json(400, { error: "frame failed" });
|
|
148
148
|
}
|
|
149
|
-
}
|
|
149
|
+
}
|
|
150
|
+
/** Side-effect imports required alongside this handler. */
|
|
151
|
+
ssr.imports = ["ilha:pages/server", "ilha:loaders"];
|
|
150
152
|
|
|
151
153
|
//#endregion
|
|
152
|
-
export {
|
|
154
|
+
export { FRAME_ENDPOINT, LOADER_ENDPOINT, ssr as default };
|
package/dist/vite.d.ts
CHANGED
package/dist/vite.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as ilhaPages } from "./plugin-
|
|
1
|
+
import { t as ilhaPages } from "./plugin-DPGzrIVj.js";
|
|
2
2
|
|
|
3
3
|
//#region src/vite.ts
|
|
4
4
|
/** Vite plugin — use via `@ilha/router/vite`. */
|
|
@@ -7,4 +7,4 @@ function pages(options = {}) {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
//#endregion
|
|
10
|
-
export {
|
|
10
|
+
export { pages as default, pages, ilhaPages };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ilha/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "A tiny SPA router for Ilha",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"frontend",
|
|
@@ -39,10 +39,18 @@
|
|
|
39
39
|
"types": "./dist/index.d.ts",
|
|
40
40
|
"import": "./dist/index.js"
|
|
41
41
|
},
|
|
42
|
+
"./server-island": {
|
|
43
|
+
"types": "./dist/server-island.d.ts",
|
|
44
|
+
"import": "./dist/server-island.js"
|
|
45
|
+
},
|
|
42
46
|
"./ssr": {
|
|
43
47
|
"types": "./dist/ssr.d.ts",
|
|
44
48
|
"import": "./dist/ssr.js"
|
|
45
49
|
},
|
|
50
|
+
"./server-island-registry": {
|
|
51
|
+
"types": "./dist/server-island-registry.d.ts",
|
|
52
|
+
"import": "./dist/server-island-registry.js"
|
|
53
|
+
},
|
|
46
54
|
"./vite": {
|
|
47
55
|
"types": "./dist/vite.d.ts",
|
|
48
56
|
"import": "./dist/vite.js"
|
|
@@ -65,14 +73,13 @@
|
|
|
65
73
|
"test": "bun test"
|
|
66
74
|
},
|
|
67
75
|
"dependencies": {
|
|
68
|
-
"rou3": "0.9.1",
|
|
69
76
|
"unplugin": "3.3.0"
|
|
70
77
|
},
|
|
71
78
|
"devDependencies": {
|
|
72
|
-
"ilha": "0.
|
|
73
|
-
"vite": "^8.2.
|
|
79
|
+
"ilha": "0.11.0",
|
|
80
|
+
"vite": "^8.2.2"
|
|
74
81
|
},
|
|
75
82
|
"peerDependencies": {
|
|
76
|
-
"ilha": ">=0.
|
|
83
|
+
"ilha": ">=0.11.0"
|
|
77
84
|
}
|
|
78
85
|
}
|