@backtickjs/hono 0.2.7 → 0.3.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @backtickjs/hono
2
2
 
3
- [Hono](https://hono.dev) routes as [Backtick](https://backtickjs.com) screens: under `backtick()`, `context.render(<Home />)` answers a request with your server components, bundled for the modules the client that asked provides, as Hono's own `jsxRenderer()` answers with HTML. Each route is a screen, and a path on the client is the same path on the server. Hono is a peer dependency: this package is built against Hono 4.13.13, and works with any later 4.x.
3
+ [Hono](https://hono.dev) routes as [Backtick](https://backtickjs.com) screens: under `backtickRenderer()`, `context.render(<Home />)` answers a request with your server components, bundled for the modules the client that asked provides, as Hono's own `jsxRenderer()` answers with HTML. Each route is a screen, and a path on the client is the same path on the server. Hono is a peer dependency: this package is built against Hono 4.13.13, and works with any later 4.x.
4
4
 
5
5
  ## Install
6
6
 
@@ -13,18 +13,54 @@ The [repository's README](https://github.com/backtickjs/backtick#packages) says
13
13
  ## Usage
14
14
 
15
15
  ```tsx
16
- import { backtick } from "@backtickjs/hono";
16
+ import { cs } from "@backtickjs/core";
17
+ import { backtickApp } from "@backtickjs/hono";
18
+ import { Stack } from "expo-router" with { backtick: "client" };
17
19
  import { Hono } from "hono";
18
20
  import { Home } from "./Home.js";
19
21
 
22
+ // The root layout, around every screen: the navigator.
23
+ const RootLayout = cs`() => <$Stack />`;
24
+
20
25
  const app = new Hono();
21
26
 
22
- app.use(backtick());
27
+ app.use(backtickApp(app, RootLayout));
23
28
  app.get("/", (context) => context.render(<Home />));
24
29
 
25
30
  export default app;
26
31
  ```
27
32
 
33
+ The Backtick app opens an app from one answer, which `backtickApp(app, RootLayout)` answers: the client code that fetches each screen and keeps it while it shows, around the app's root layout, a client component that draws its navigator, as Hono's `jsxRenderer()` takes the page's. Navigation works as in Next.js. Every route answers with what it draws, and the runtime fetches each as a query keyed by its path. A route with a `_loading` (`/day/_loading`, rendered for the path, so it can use its params) pushes as soon as that's in, at once if it was prefetched, and shows it until the route's answer is in; a navigation to one without holds the current screen until its answer is in, as expo-router's `unstable_useIsNavigating()` says, for a progress bar. Going back shows a screen as it was. `<Link prefetch>` fetches a route's loading state ahead, or its answer, without one. The root layout is the only one: what screens share is a component they draw. A client with a navigator of its own uses `backtickRenderer()` alone.
34
+
35
+ The runtime gives the app a [TanStack Query](https://tanstack.com/query) client whose query keys are paths: a query of `"/cart/items"` fetches that route, and its answer is what the route renders. For a screen whose parts load on their own, each in its own `Suspense`, and refetch on their own:
36
+
37
+ ```tsx
38
+ import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query" with {
39
+ backtick: "client",
40
+ };
41
+
42
+ const Part = cs`(props: { path: string }) =>
43
+ $useSuspenseQuery({ queryKey: [props.path] }).data as ReactNode`;
44
+
45
+ const Cart = cs`() => {
46
+ const queryClient = $useQueryClient();
47
+ return (
48
+ <>
49
+ <$Suspense fallback={<$ActivityIndicator />}>
50
+ <$Part path="/cart/items" />
51
+ </$Suspense>
52
+ <$Button
53
+ title="Refresh"
54
+ onPress={() =>
55
+ queryClient.invalidateQueries({ queryKey: ["/cart/items"] })
56
+ }
57
+ />
58
+ </>
59
+ );
60
+ }`;
61
+ app.get("/cart", (context) => context.render(cs`<$Cart />`));
62
+ ```
63
+
28
64
  The client sends its modules, each at its package's version, in `x-backtick-modules` (in an Expo app, `modulesOf` from `@backtickjs/core` writes them). The answer is the screen's bundle as CommonJS, `text/javascript`, for the client to run with `execRoot`.
29
65
 
30
66
  A client without a module the screen needs gets a `426`, and any other refusal of the bundler's a `400`, each with `{ name, message }` as JSON: the client can show why. An error your server components throw passes through to Hono.
package/dist/index.d.ts CHANGED
@@ -1,39 +1,46 @@
1
- import type { Spliceable, SplicesAs } from "@backtickjs/core";
2
- import type { MiddlewareHandler } from "hono";
1
+ import type { Client, Spliceable, SplicesAs } from "@backtickjs/core";
2
+ import type { ComponentType } from "react";
3
+ import type { Hono, MiddlewareHandler } from "hono";
4
+ export type { Runtime } from "./runtime.js";
3
5
  declare module "hono" {
4
6
  interface ContextRenderer {
5
7
  <T extends [T] extends [SplicesAs<T>] ? unknown : Spliceable>(input: T): Promise<Response>;
6
8
  }
7
9
  }
8
- /** A web app manifest: what the phone uses of an app before and around it. */
9
- export interface Manifest {
10
- name?: string;
11
- short_name?: string;
12
- icons?: {
13
- src: string;
14
- sizes?: string;
15
- type?: string;
16
- }[];
17
- theme_color?: string;
18
- background_color?: string;
19
- display?: "fullscreen" | "standalone" | "minimal-ui" | "browser";
20
- }
21
10
  /**
22
11
  * Hono's renderer for Backtick screens: under it, `context.render(input)`
23
12
  * answers with `input` bundled for the modules the client sent in
24
13
  * `x-backtick-modules`, so each route is a screen.
25
14
  *
26
- * app.use(backtick());
15
+ * app.use(backtickRenderer());
27
16
  * app.get("/", (context) => context.render(<Home />));
28
17
  *
29
18
  * The status is the context's, so `context.status(404)` makes a not-found
30
- * screen, and screens are `no-store`: each opens as its latest copy. A client
19
+ * screen, and screens are `no-store`, kept out of HTTP caches: a reload or
20
+ * a new open always gets the latest copy. (The Backtick app reuses a screen
21
+ * it has, in memory, for 30 seconds, as Next.js's router does.) A client
31
22
  * without a module the screen needs gets a 426, and any other refusal of the
32
- * bundler's a 400, each with `{ name, message }`.
23
+ * bundler's a 400, each with `{ name, message }`. An app the Backtick app
24
+ * opens uses `backtickApp(app, RootLayout)` instead.
25
+ */
26
+ export declare function backtickRenderer(): MiddlewareHandler;
27
+ /**
28
+ * Hono's renderer for an app the Backtick app opens, as `backtickRenderer()`,
29
+ * which also answers the runtime the Backtick app fetches when it opens
30
+ * `app`: the client code that fetches, keeps and shows its screens, around
31
+ * its root layout, a client component that draws its `Stack`.
32
+ *
33
+ * const RootLayout = cs`() => <$Stack />`;
34
+ * app.use(backtickApp(app, RootLayout));
35
+ * app.get("/", (context) => context.render(<Home />));
33
36
  *
34
- * `manifest` is the app's web app manifest, at `/manifest.webmanifest` and in
35
- * `x-backtick-manifest` on every screen, so it's as fresh as they are.
37
+ * Every route answers with what it draws. As in Next.js, a route with a
38
+ * `_loading` route (`/city/:id/_loading`, rendered for the path, so it can
39
+ * show the city's name) pushes as soon as that's in, at once if it was
40
+ * prefetched, and shows it until the route's answer is in; a navigation to
41
+ * one without holds the current screen until its answer is in. The runtime
42
+ * asks only for the loading states `app` has routes for. One with a layout
43
+ * route (a screen has none: what screens share is a component they draw), or
44
+ * with a loading route written with a trailing slash, doesn't open.
36
45
  */
37
- export declare function backtick(options?: {
38
- manifest?: Manifest;
39
- }): MiddlewareHandler;
46
+ export declare function backtickApp(app: Hono<any, any, any>, RootLayout: Client<ComponentType>): MiddlewareHandler;
package/dist/index.js CHANGED
@@ -1,57 +1,89 @@
1
1
  import { bundler, BundlerError } from "@backtickjs/bundler";
2
+ import { clientRuntime } from "./runtime.js";
2
3
  /**
3
4
  * Hono's renderer for Backtick screens: under it, `context.render(input)`
4
5
  * answers with `input` bundled for the modules the client sent in
5
6
  * `x-backtick-modules`, so each route is a screen.
6
7
  *
7
- * app.use(backtick());
8
+ * app.use(backtickRenderer());
8
9
  * app.get("/", (context) => context.render(<Home />));
9
10
  *
10
11
  * The status is the context's, so `context.status(404)` makes a not-found
11
- * screen, and screens are `no-store`: each opens as its latest copy. A client
12
+ * screen, and screens are `no-store`, kept out of HTTP caches: a reload or
13
+ * a new open always gets the latest copy. (The Backtick app reuses a screen
14
+ * it has, in memory, for 30 seconds, as Next.js's router does.) A client
12
15
  * without a module the screen needs gets a 426, and any other refusal of the
13
- * bundler's a 400, each with `{ name, message }`.
16
+ * bundler's a 400, each with `{ name, message }`. An app the Backtick app
17
+ * opens uses `backtickApp(app, RootLayout)` instead.
18
+ */
19
+ export function backtickRenderer() {
20
+ return async (context, next) => {
21
+ context.setRenderer(async (input) => bundled(context, input));
22
+ await next();
23
+ };
24
+ }
25
+ /**
26
+ * Hono's renderer for an app the Backtick app opens, as `backtickRenderer()`,
27
+ * which also answers the runtime the Backtick app fetches when it opens
28
+ * `app`: the client code that fetches, keeps and shows its screens, around
29
+ * its root layout, a client component that draws its `Stack`.
14
30
  *
15
- * `manifest` is the app's web app manifest, at `/manifest.webmanifest` and in
16
- * `x-backtick-manifest` on every screen, so it's as fresh as they are.
31
+ * const RootLayout = cs`() => <$Stack />`;
32
+ * app.use(backtickApp(app, RootLayout));
33
+ * app.get("/", (context) => context.render(<Home />));
34
+ *
35
+ * Every route answers with what it draws. As in Next.js, a route with a
36
+ * `_loading` route (`/city/:id/_loading`, rendered for the path, so it can
37
+ * show the city's name) pushes as soon as that's in, at once if it was
38
+ * prefetched, and shows it until the route's answer is in; a navigation to
39
+ * one without holds the current screen until its answer is in. The runtime
40
+ * asks only for the loading states `app` has routes for. One with a layout
41
+ * route (a screen has none: what screens share is a component they draw), or
42
+ * with a loading route written with a trailing slash, doesn't open.
17
43
  */
18
- export function backtick(options = {}) {
19
- const { manifest } = options;
20
- const json = JSON.stringify(manifest);
44
+ export function backtickApp(app, RootLayout) {
45
+ const renderer = backtickRenderer();
21
46
  return async (context, next) => {
22
- if (manifest !== undefined &&
23
- context.req.path === "/manifest.webmanifest") {
24
- return context.body(json, 200, {
25
- "content-type": "application/manifest+json",
26
- });
47
+ if (context.req.method !== "GET" || context.req.path !== runtimePath) {
48
+ return renderer(context, next);
27
49
  }
28
- context.setRenderer(async (input) => {
29
- let modules;
30
- try {
31
- modules = JSON.parse(context.req.header("x-backtick-modules") ?? "{}");
32
- }
33
- catch (error) {
34
- const { name, message } = error;
35
- return context.json({ name, message: `x-backtick-modules isn't JSON: ${message}` }, 400);
36
- }
37
- try {
38
- const build = await bundler.build({ input, modules });
39
- const { code } = build.generate({ format: "cjs" });
40
- context.header("content-type", "text/javascript");
41
- context.header("cache-control", "no-store");
42
- if (manifest !== undefined) {
43
- context.header("x-backtick-manifest", encodeURIComponent(json));
44
- }
45
- return context.body(code);
46
- }
47
- catch (error) {
48
- if (error instanceof BundlerError) {
49
- return context.json({ name: error.name, message: error.message }, error.name === "ClientVersionMismatch" ? 426 : 400);
50
- }
51
- throw error;
52
- }
53
- });
54
- await next();
50
+ const misplaced = app.routes.find(({ path }) => path.endsWith("/_loading/") || /\/_layout\/?$/.test(path));
51
+ if (misplaced !== undefined) {
52
+ throw new Error(misplaced.path.endsWith("/_loading/")
53
+ ? `The app's ${misplaced.path} route ends with a slash: write it ${misplaced.path.slice(0, -1)}.`
54
+ : `The app's ${misplaced.path} route is a layout, which a screen doesn't have: the root layout is backtickApp(app, RootLayout)'s, and what screens share is a component they draw.`);
55
+ }
56
+ // The app's loading states, by pattern.
57
+ const loadings = app.routes
58
+ .filter(({ method, path }) => method === "GET" && path.endsWith("/_loading"))
59
+ .map(({ path }) => path);
60
+ return bundled(context, clientRuntime(new URL(context.req.url).origin, context.req.header("x-backtick-modules") ?? "{}", RootLayout, [...new Set(loadings)]));
55
61
  };
56
62
  }
63
+ // Where the Backtick app fetches the runtime it opens an app with.
64
+ const runtimePath = "/.well-known/backtick/runtime";
65
+ // `input` bundled for the modules the client sent, as the answer.
66
+ async function bundled(context, input) {
67
+ let modules;
68
+ try {
69
+ modules = JSON.parse(context.req.header("x-backtick-modules") ?? "{}");
70
+ }
71
+ catch (error) {
72
+ const { name, message } = error;
73
+ return context.json({ name, message: `x-backtick-modules isn't JSON: ${message}` }, 400);
74
+ }
75
+ try {
76
+ const build = await bundler.build({ input, modules });
77
+ const { code } = build.generate({ format: "cjs" });
78
+ context.header("content-type", "text/javascript");
79
+ context.header("cache-control", "no-store");
80
+ return context.body(code);
81
+ }
82
+ catch (error) {
83
+ if (error instanceof BundlerError) {
84
+ return context.json({ name: error.name, message: error.message }, error.name === "ClientVersionMismatch" ? 426 : 400);
85
+ }
86
+ throw error;
87
+ }
88
+ }
57
89
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,YAAY,EAAgB,MAAM,qBAAqB,CAAC;AAwB1E;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,QAAQ,CACtB,UAAmC,EAAE;IAErC,MAAM,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC;IAC7B,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;IACtC,OAAO,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;QAC7B,IACE,QAAQ,KAAK,SAAS;YACtB,OAAO,CAAC,GAAG,CAAC,IAAI,KAAK,uBAAuB,EAC5C,CAAC;YACD,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,GAAG,EAAE;gBAC7B,cAAc,EAAE,2BAA2B;aAC5C,CAAC,CAAC;QACL,CAAC;QACD,OAAO,CAAC,WAAW,CAAC,KAAK,EAAE,KAAK,EAAE,EAAE;YAClC,IAAI,OAAgB,CAAC;YACrB,IAAI,CAAC;gBACH,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,oBAAoB,CAAC,IAAI,IAAI,CAAC,CAAC;YACzE,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,KAAoB,CAAC;gBAC/C,OAAO,OAAO,CAAC,IAAI,CACjB,EAAE,IAAI,EAAE,OAAO,EAAE,kCAAkC,OAAO,EAAE,EAAE,EAC9D,GAAG,CACJ,CAAC;YACJ,CAAC;YACD,IAAI,CAAC;gBACH,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,CAAC;gBACtD,MAAM,EAAE,IAAI,EAAE,GAAG,KAAK,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,CAAC;gBACnD,OAAO,CAAC,MAAM,CAAC,cAAc,EAAE,iBAAiB,CAAC,CAAC;gBAClD,OAAO,CAAC,MAAM,CAAC,eAAe,EAAE,UAAU,CAAC,CAAC;gBAC5C,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;oBAC3B,OAAO,CAAC,MAAM,CAAC,qBAAqB,EAAE,kBAAkB,CAAC,IAAI,CAAC,CAAC,CAAC;gBAClE,CAAC;gBACD,OAAO,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC5B,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,IAAI,KAAK,YAAY,YAAY,EAAE,CAAC;oBAClC,OAAO,OAAO,CAAC,IAAI,CACjB,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,EAC5C,KAAK,CAAC,IAAI,KAAK,uBAAuB,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,CACnD,CAAC;gBACJ,CAAC;gBACD,MAAM,KAAK,CAAC;YACd,CAAC;QACH,CAAC,CAAC,CAAC;QACH,MAAM,IAAI,EAAE,CAAC;IACf,CAAC,CAAC;AACJ,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ import { type Client } from "@backtickjs/core";
2
+ import { type ComponentType } from "react";
3
+ /**
4
+ * What the phone runs an app with. A shell checks for a member before using
5
+ * it, so a later runtime can drop one.
6
+ */
7
+ export interface Runtime {
8
+ /**
9
+ * The app's root layout, given the href on top. A navigation holds the
10
+ * current screen until the next can show, as navigations are transitions
11
+ * (`router.setTransitionMode("always")`): a route with a `_loading` once
12
+ * that's in, at once if it was prefetched; one without, once its page is
13
+ * in. Suspends until the first screen can show.
14
+ */
15
+ Navigator?: ComponentType<{
16
+ href: string;
17
+ }>;
18
+ /**
19
+ * A route's screen: what it renders, and meanwhile what its `_loading`
20
+ * renders. One not `active` yet shows nothing.
21
+ */
22
+ Screen?: ComponentType<{
23
+ href: string;
24
+ active: boolean;
25
+ }>;
26
+ /**
27
+ * Fetches an href's screen again, which stays until the new one is in:
28
+ * resolves once it's in, either way.
29
+ */
30
+ reload?(href: string): Promise<void>;
31
+ /**
32
+ * Fetches an href ahead, as Next.js prefetches a link: its loading state,
33
+ * or, without one, its page. For each `<Link prefetch>`, which the shell
34
+ * sees as it's prefetched.
35
+ */
36
+ prefetch?(href: string): void;
37
+ }
38
+ /**
39
+ * A route's answer, run. Any bundle is an answer, whatever its status: an
40
+ * app's not-found screen is one. A refusal ({ name, message }) says why. For
41
+ * a route that may not exist (`notFoundIsNull`), a 404 is null.
42
+ */
43
+ export declare const fetchAnswer: Client<unknown>;
44
+ /**
45
+ * The runtime of an open: for the phone at `origin` with `modules`, around
46
+ * the app's root layout, with the patterns of its `_loading` routes
47
+ * (`/city/:id/_loading`), so a screen asks only for those it has.
48
+ */
49
+ export declare function clientRuntime(origin: string, modules: string, RootLayout: Client<ComponentType>, loadingPatterns: readonly string[]): Client<Runtime>;
@@ -0,0 +1,44 @@
1
+ // The phone's half of `backtickApp()`: how an app's screens are fetched,
2
+ // kept and shown, written as client code, which `backtickApp()` sends from
3
+ // `/.well-known/backtick/runtime`.
4
+ import { cs, exec } from "@backtickjs/core";
5
+ const QueryClient = { "@backtickjs": "ClientImport", name: "QueryClient", from: "@tanstack/react-query", version: "5.104.0" };
6
+ const QueryClientProvider = { "@backtickjs": "ClientImport", name: "QueryClientProvider", from: "@tanstack/react-query", version: "5.104.0" };
7
+ const usePrefetchQuery = { "@backtickjs": "ClientImport", name: "usePrefetchQuery", from: "@tanstack/react-query", version: "5.104.0" };
8
+ const useSuspenseQueries = { "@backtickjs": "ClientImport", name: "useSuspenseQueries", from: "@tanstack/react-query", version: "5.104.0" };
9
+ const useSuspenseQuery = { "@backtickjs": "ClientImport", name: "useSuspenseQuery", from: "@tanstack/react-query", version: "5.104.0" };
10
+ const PatternRouter = { "@backtickjs": "ClientImport", name: "PatternRouter", from: "hono/router/pattern-router", version: "4.13.13" };
11
+ const Component = { "@backtickjs": "ClientImport", name: "Component", from: "react", version: "19.3.0" };
12
+ const Suspense = { "@backtickjs": "ClientImport", name: "Suspense", from: "react", version: "19.3.0" };
13
+ const useDeferredValue = { "@backtickjs": "ClientImport", name: "useDeferredValue", from: "react", version: "19.3.0" };
14
+ const $module0 = {
15
+ id: "2hmayizp567y2:59:27",
16
+ code: "(module, exports, require) => {\n\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nexports.default = $splice0 => async (url, modules, notFoundIsNull) => {\n var response = await fetch(url, {\n headers: {\n \"x-backtick-modules\": modules\n }\n });\n if (notFoundIsNull && response.status === 404) {\n return null;\n }\n var body = await response.text();\n if (!(response.headers.get(\"content-type\") ?? \"\").startsWith(\"text/javascript\")) {\n var reason = body;\n try {\n reason = JSON.parse(body).message ?? body;\n }\n catch { }\n throw new Error(\"The screen couldn't be opened (\" + response.status + \"): \" + reason);\n }\n return await $splice0()(body);\n};\n}",
17
+ map: "{\"version\":3,\"file\":\"module.jsx\",\"mappings\":\";;;kBA0D8BA,QAAA,WAC5BC,GAAW,EACXC,OAAe,EACfC,cAAuB;IAEvB,IAAMC,QAAQ,GAAG,MAAMC,KAAK,CAACJ,GAAG,EAAE;QAChCK,OAAO,EAAE;YAAE,oBAAoB,EAAEJ;SAAO;KACzC,CAAC;IACF,IAAIC,cAAc,IAAIC,QAAQ,CAACG,MAAM,KAAK,GAAG,EAAE;QAC7C,OAAO,IAAI;IACb;IACA,IAAMC,IAAI,GAAG,MAAMJ,QAAQ,CAACK,IAAI,EAAE;IAClC,IACE,CAAC,CAACL,QAAQ,CAACE,OAAO,CAACI,GAAG,CAAC,cAAc,CAAC,IAAI,EAAE,EAAEC,UAAU,CAAC,iBAAiB,CAAC,EAC3E;QACA,IAAIC,MAAM,GAAGJ,IAAI;QACjB,IAAI;YACFI,MAAM,GAAGC,IAAI,CAACC,KAAK,CAACN,IAAI,CAAC,CAACO,OAAO,IAAIP,IAAI;QAC3C,CAAC;QAAC,MAAM,CAAC;QACT,MAAM,IAAIQ,KAAK,CACb,iCAAiC,GAAGZ,QAAQ,CAACG,MAAM,GAAG,KAAK,GAAGK,MAAM,CACrE;IACH;IACA,OAAQ,MAAMZ,QAAA,EAAK,CAACQ,IAAa,CAAC;AACpC,CAAC\",\"names\":[\"$splice0\",\"url\",\"modules\",\"notFoundIsNull\",\"response\",\"fetch\",\"headers\",\"status\",\"body\",\"text\",\"get\",\"startsWith\",\"reason\",\"JSON\",\"parse\",\"message\",\"Error\"],\"ignoreList\":[],\"sources\":[\"@backtickjs/hono/src/runtime.tsx\"]}",
18
+ dependencies: [],
19
+ params: [{ kind: "splice", bindings: [] }],
20
+ kind: "function"
21
+ };
22
+ const $module1 = {
23
+ id: "2hmayizp567y2:96:9",
24
+ code: "(module, exports, require) => {\n\"use strict\";\nObject.defineProperty(exports, \"__esModule\", { value: true });\nconst jsx_runtime_1 = require(\"react/jsx-runtime\");\nexports.default = ($splice0, $splice1, $splice2, $splice3, $splice4, $splice5, $splice6, $splice7, $splice8, $splice9, $splice10, $splice11, $splice12, $splice13, $splice14, $splice15, $splice16, $splice17, $splice18) => {\n var pageStaleTime = 30000;\n var loadingStaleTime = 300000;\n var queryClient = new ($splice0())({\n defaultOptions: {\n queries: {\n queryFn: async (context) => $splice1()($splice2() + context.queryKey[0], $splice3(), false),\n staleTime: pageStaleTime,\n structuralSharing: false,\n retry: false\n }\n }\n });\n var loadingQueryOptions = path => ({\n queryKey: [path],\n staleTime: loadingStaleTime,\n queryFn: async () => ({\n value: await $splice4()($splice5() + path, $splice6(), true)\n })\n });\n var loadingRouter = new ($splice7())();\n for (var pattern of $splice8()) {\n loadingRouter.add(\"GET\", pattern, true);\n }\n var loadingPathOf = href => {\n var searchStart = href.indexOf(\"?\");\n var path = searchStart === -1 ? href : href.slice(0, searchStart);\n var loading = (path === \"/\" ? \"\" : path) + \"/_loading\";\n return loadingRouter.match(\"GET\", loading)[0].length > 0 ? loading : undefined;\n };\n var dropStaleAnswer = (path, startedAt) => {\n var query = queryClient.getQueryCache().find({\n queryKey: [path],\n exact: true\n });\n if (query !== undefined && query.getObserversCount() === 0 && query.state.data !== undefined && query.state.dataUpdatedAt < startedAt && query.isStaleByTime(pageStaleTime)) {\n queryClient.removeQueries({\n queryKey: [path],\n exact: true\n });\n }\n };\n var Page = props => $splice9()({\n queryKey: [props.href]\n }).data;\n var PageWithLoading = props => {\n var loading = loadingPathOf(props.href);\n $splice10()({\n queryKey: [props.href]\n });\n var queries = loading === undefined ? [] : [loadingQueryOptions(loading)];\n var loadingResult = $splice11()({\n queries\n })[0];\n return loadingResult === undefined ? (0, jsx_runtime_1.jsx)(Page, {\n href: props.href\n }) : ($Suspense => (0, jsx_runtime_1.jsx)($Suspense, {\n fallback: loadingResult.data.value,\n children: (0, jsx_runtime_1.jsx)(Page, {\n href: props.href\n })\n }))($splice12());\n };\n var prefetch = href => {\n var loading = loadingPathOf(href);\n if (loading === undefined) {\n void queryClient.query({\n queryKey: [href]\n }).catch(() => { });\n return;\n }\n void queryClient.query(loadingQueryOptions(loading)).then(answer => answer.value === null ? queryClient.query({\n queryKey: [href]\n }) : undefined).catch(() => { });\n };\n var Screen = props => {\n var href = $splice13()(props.href);\n return props.active ? ($Suspense => (0, jsx_runtime_1.jsx)($Suspense, {\n fallback: null,\n children: (0, jsx_runtime_1.jsx)(PageWithLoading, {\n href: href\n })\n }))($splice14()) : null;\n };\n var AwaitQuery = props => {\n $splice15()({\n ...props.query,\n refetchOnMount: false\n });\n return null;\n };\n var currentNavigation = {\n href: \"\",\n startedAt: 0\n };\n var NavigationHold = props => {\n if (currentNavigation.href !== props.href) {\n currentNavigation = {\n href: props.href,\n startedAt: Date.now()\n };\n }\n dropStaleAnswer(props.href, currentNavigation.startedAt);\n var loading = loadingPathOf(props.href);\n if (loading !== undefined) {\n void queryClient.query({\n queryKey: [props.href]\n }).catch(() => { });\n return (0, jsx_runtime_1.jsx)(AwaitQuery, {\n query: loadingQueryOptions(loading)\n });\n }\n return (0, jsx_runtime_1.jsx)(AwaitQuery, {\n query: {\n queryKey: [props.href]\n }\n });\n };\n class SilentErrorBoundary extends $splice16() {\n state = {\n failed: false\n };\n static getDerivedStateFromError() {\n return {\n failed: true\n };\n }\n render() {\n return this.state.failed ? null : this.props.children;\n }\n }\n var Navigator = props => ($QueryClientProvider => (0, jsx_runtime_1.jsxs)($QueryClientProvider, {\n client: queryClient,\n children: [(0, jsx_runtime_1.jsx)(SilentErrorBoundary, {\n children: (0, jsx_runtime_1.jsx)(NavigationHold, {\n href: props.href\n })\n }, props.href), ($RootLayout => (0, jsx_runtime_1.jsx)($RootLayout, {}))($splice18())]\n }))($splice17());\n var runtime = {\n Navigator,\n Screen,\n prefetch,\n reload: async (href) => {\n var loading = loadingPathOf(href);\n await Promise.allSettled((loading === undefined ? [href] : [href, loading]).map(path => queryClient.getQueryState([path])?.status === \"error\" ? queryClient.resetQueries({\n queryKey: [path],\n exact: true\n }) : queryClient.refetchQueries({\n queryKey: [path],\n exact: true\n })));\n }\n };\n return runtime;\n};\n}",
25
+ map: "{\"version\":3,\"file\":\"module.jsx\",\"mappings\":\"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\",\"names\":[\"$splice0\",\"$splice1\",\"$splice2\",\"$splice3\",\"$splice4\",\"$splice5\",\"$splice6\",\"$splice7\",\"$splice8\",\"$splice9\",\"$splice10\",\"$splice11\",\"$splice12\",\"$splice13\",\"$splice14\",\"$splice15\",\"$splice16\",\"$splice17\",\"$splice18\",\"pageStaleTime\",\"loadingStaleTime\",\"queryClient\",\"defaultOptions\",\"queries\",\"queryFn\",\"context\",\"queryKey\",\"staleTime\",\"structuralSharing\",\"retry\",\"loadingQueryOptions\",\"path\",\"value\",\"loadingRouter\",\"pattern\",\"add\",\"loadingPathOf\",\"href\",\"searchStart\",\"indexOf\",\"slice\",\"loading\",\"match\",\"length\",\"undefined\",\"dropStaleAnswer\",\"startedAt\",\"query\",\"getQueryCache\",\"find\",\"exact\",\"getObserversCount\",\"state\",\"data\",\"dataUpdatedAt\",\"isStaleByTime\",\"removeQueries\",\"Page\",\"props\",\"PageWithLoading\",\"loadingResult\",\"_jsx\",\"$Suspense\",\"fallback\",\"children\",\"prefetch\",\"catch\",\"then\",\"answer\",\"Screen\",\"active\",\"AwaitQuery\",\"refetchOnMount\",\"currentNavigation\",\"NavigationHold\",\"Date\",\"now\",\"SilentErrorBoundary\",\"failed\",\"getDerivedStateFromError\",\"render\",\"Navigator\",\"$QueryClientProvider\",\"_jsxs\",\"client\",\"$RootLayout\",\"runtime\",\"reload\",\"Promise\",\"allSettled\",\"map\",\"getQueryState\",\"status\",\"resetQueries\",\"refetchQueries\"],\"ignoreList\":[],\"sources\":[\"@backtickjs/hono/src/runtime.tsx\"]}",
26
+ dependencies: ["react/jsx-runtime"],
27
+ params: [{ kind: "splice", bindings: [] }, { kind: "splice", bindings: [] }, { kind: "splice", bindings: [] }, { kind: "splice", bindings: [] }, { kind: "splice", bindings: [] }, { kind: "splice", bindings: [] }, { kind: "splice", bindings: [] }, { kind: "splice", bindings: [] }, { kind: "splice", bindings: [] }, { kind: "splice", bindings: [] }, { kind: "splice", bindings: [] }, { kind: "splice", bindings: [] }, { kind: "splice", bindings: [] }, { kind: "splice", bindings: [] }, { kind: "splice", bindings: [] }, { kind: "splice", bindings: [] }, { kind: "splice", bindings: [] }, { kind: "splice", bindings: [] }, { kind: "splice", bindings: [] }],
28
+ kind: "block"
29
+ };
30
+ /**
31
+ * A route's answer, run. Any bundle is an answer, whatever its status: an
32
+ * app's not-found screen is one. A refusal ({ name, message }) says why. For
33
+ * a route that may not exist (`notFoundIsNull`), a 404 is null.
34
+ */
35
+ export const fetchAnswer = cs.create($module0, [exec]);
36
+ /**
37
+ * The runtime of an open: for the phone at `origin` with `modules`, around
38
+ * the app's root layout, with the patterns of its `_loading` routes
39
+ * (`/city/:id/_loading`), so a screen asks only for those it has.
40
+ */
41
+ export function clientRuntime(origin, modules, RootLayout, loadingPatterns) {
42
+ return cs.create($module1, [QueryClient, fetchAnswer, origin, modules, fetchAnswer, origin, modules, PatternRouter, loadingPatterns, useSuspenseQuery, usePrefetchQuery, useSuspenseQueries, Suspense, useDeferredValue, Suspense, useSuspenseQuery, Component, QueryClientProvider, RootLayout]);
43
+ }
44
+ //# sourceMappingURL=runtime.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime.js","sourceRoot":"","sources":["../src/runtime.tsx"],"names":[],"mappings":"AAAA,yEAAyE;AACzE,2EAA2E;AAC3E,mCAAmC;AACnC,OAAO,EAAe,EAAE,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;AAkDzD;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,8BAwBtB,CAAC;AAEH;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAC3B,MAAc,EACd,OAAe,EACf,UAAiC,EACjC,eAAkC;IAElC,kSAyLG;AACL,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backtickjs/hono",
3
- "version": "0.2.7",
3
+ "version": "0.3.0",
4
4
  "description": "Hono routes as Backtick screens: a route's answer is its server components, bundled for the client that asked",
5
5
  "keywords": [
6
6
  "backtick",
@@ -37,20 +37,36 @@
37
37
  "access": "public"
38
38
  },
39
39
  "dependencies": {
40
- "@backtickjs/bundler": "0.2.7",
41
- "@backtickjs/core": "0.2.7"
40
+ "@backtickjs/bundler": "0.3.0",
41
+ "@backtickjs/core": "0.3.0"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "hono": "^4.13.13"
45
45
  },
46
46
  "devDependencies": {
47
+ "@backtickjs/expo": "0.3.0",
48
+ "@backtickjs/jsx-react": "0.3.0",
49
+ "@backtickjs/prettier-plugin": "0.3.0",
50
+ "@backtickjs/tsc": "0.3.0",
51
+ "@backtickjs/tspatch-plugin": "0.3.0",
52
+ "@tanstack/react-query": "5.104.0",
53
+ "@types/react": "19.3.0",
47
54
  "hono": "^4.13.13",
48
55
  "prettier": "^3.6.2",
56
+ "react": "19.3.0",
57
+ "ts-patch": "4.0.1",
49
58
  "typescript": "^6.0.3"
50
59
  },
60
+ "backtick": {
61
+ "plugins": [
62
+ "@backtickjs/jsx-react/plugin",
63
+ "@backtickjs/expo/plugin"
64
+ ]
65
+ },
51
66
  "scripts": {
52
- "build": "tsc -b",
53
- "dev": "tsc -b --watch",
67
+ "build": "tspc -b --noCheck --force",
68
+ "dev": "tspc -b --noCheck --watch",
69
+ "typecheck": "backtick-tsc -p tsconfig.build.json --noEmit",
54
70
  "format": "prettier --write --ignore-unknown .",
55
71
  "format:check": "prettier --check --ignore-unknown .",
56
72
  "test": "node --test \"test/**/*.test.ts\"",
package/src/index.ts CHANGED
@@ -1,9 +1,13 @@
1
1
  import { bundler, BundlerError, type Modules } from "@backtickjs/bundler";
2
- import type { Spliceable, SplicesAs } from "@backtickjs/core";
3
- import type { MiddlewareHandler } from "hono";
2
+ import type { Client, Spliceable, SplicesAs } from "@backtickjs/core";
3
+ import type { ComponentType } from "react";
4
+ import type { Context, Hono, MiddlewareHandler } from "hono";
5
+ import { clientRuntime } from "./runtime.js";
6
+
7
+ export type { Runtime } from "./runtime.js";
4
8
 
5
9
  declare module "hono" {
6
- // What `context.render` takes under `backtick()`, as
10
+ // What `context.render` takes under `backtickRenderer()`, as
7
11
  // `jsxRenderer()` types its own.
8
12
  interface ContextRenderer {
9
13
  <T extends [T] extends [SplicesAs<T>] ? unknown : Spliceable>(
@@ -12,76 +16,113 @@ declare module "hono" {
12
16
  }
13
17
  }
14
18
 
15
- /** A web app manifest: what the phone uses of an app before and around it. */
16
- export interface Manifest {
17
- name?: string;
18
- short_name?: string;
19
- icons?: { src: string; sizes?: string; type?: string }[];
20
- theme_color?: string;
21
- background_color?: string;
22
- display?: "fullscreen" | "standalone" | "minimal-ui" | "browser";
23
- }
24
-
25
19
  /**
26
20
  * Hono's renderer for Backtick screens: under it, `context.render(input)`
27
21
  * answers with `input` bundled for the modules the client sent in
28
22
  * `x-backtick-modules`, so each route is a screen.
29
23
  *
30
- * app.use(backtick());
24
+ * app.use(backtickRenderer());
31
25
  * app.get("/", (context) => context.render(<Home />));
32
26
  *
33
27
  * The status is the context's, so `context.status(404)` makes a not-found
34
- * screen, and screens are `no-store`: each opens as its latest copy. A client
28
+ * screen, and screens are `no-store`, kept out of HTTP caches: a reload or
29
+ * a new open always gets the latest copy. (The Backtick app reuses a screen
30
+ * it has, in memory, for 30 seconds, as Next.js's router does.) A client
35
31
  * without a module the screen needs gets a 426, and any other refusal of the
36
- * bundler's a 400, each with `{ name, message }`.
32
+ * bundler's a 400, each with `{ name, message }`. An app the Backtick app
33
+ * opens uses `backtickApp(app, RootLayout)` instead.
34
+ */
35
+ export function backtickRenderer(): MiddlewareHandler {
36
+ return async (context, next) => {
37
+ context.setRenderer(async (input) => bundled(context, input as Spliceable));
38
+ await next();
39
+ };
40
+ }
41
+
42
+ /**
43
+ * Hono's renderer for an app the Backtick app opens, as `backtickRenderer()`,
44
+ * which also answers the runtime the Backtick app fetches when it opens
45
+ * `app`: the client code that fetches, keeps and shows its screens, around
46
+ * its root layout, a client component that draws its `Stack`.
37
47
  *
38
- * `manifest` is the app's web app manifest, at `/manifest.webmanifest` and in
39
- * `x-backtick-manifest` on every screen, so it's as fresh as they are.
48
+ * const RootLayout = cs`() => <$Stack />`;
49
+ * app.use(backtickApp(app, RootLayout));
50
+ * app.get("/", (context) => context.render(<Home />));
51
+ *
52
+ * Every route answers with what it draws. As in Next.js, a route with a
53
+ * `_loading` route (`/city/:id/_loading`, rendered for the path, so it can
54
+ * show the city's name) pushes as soon as that's in, at once if it was
55
+ * prefetched, and shows it until the route's answer is in; a navigation to
56
+ * one without holds the current screen until its answer is in. The runtime
57
+ * asks only for the loading states `app` has routes for. One with a layout
58
+ * route (a screen has none: what screens share is a component they draw), or
59
+ * with a loading route written with a trailing slash, doesn't open.
40
60
  */
41
- export function backtick(
42
- options: { manifest?: Manifest } = {},
61
+ export function backtickApp(
62
+ app: Hono<any, any, any>,
63
+ RootLayout: Client<ComponentType>,
43
64
  ): MiddlewareHandler {
44
- const { manifest } = options;
45
- const json = JSON.stringify(manifest);
65
+ const renderer = backtickRenderer();
46
66
  return async (context, next) => {
47
- if (
48
- manifest !== undefined &&
49
- context.req.path === "/manifest.webmanifest"
50
- ) {
51
- return context.body(json, 200, {
52
- "content-type": "application/manifest+json",
53
- });
67
+ if (context.req.method !== "GET" || context.req.path !== runtimePath) {
68
+ return renderer(context, next);
54
69
  }
55
- context.setRenderer(async (input) => {
56
- let modules: Modules;
57
- try {
58
- modules = JSON.parse(context.req.header("x-backtick-modules") ?? "{}");
59
- } catch (error) {
60
- const { name, message } = error as SyntaxError;
61
- return context.json(
62
- { name, message: `x-backtick-modules isn't JSON: ${message}` },
63
- 400,
64
- );
65
- }
66
- try {
67
- const build = await bundler.build({ input, modules });
68
- const { code } = build.generate({ format: "cjs" });
69
- context.header("content-type", "text/javascript");
70
- context.header("cache-control", "no-store");
71
- if (manifest !== undefined) {
72
- context.header("x-backtick-manifest", encodeURIComponent(json));
73
- }
74
- return context.body(code);
75
- } catch (error) {
76
- if (error instanceof BundlerError) {
77
- return context.json(
78
- { name: error.name, message: error.message },
79
- error.name === "ClientVersionMismatch" ? 426 : 400,
80
- );
81
- }
82
- throw error;
83
- }
84
- });
85
- await next();
70
+ const misplaced = app.routes.find(
71
+ ({ path }) => path.endsWith("/_loading/") || /\/_layout\/?$/.test(path),
72
+ );
73
+ if (misplaced !== undefined) {
74
+ throw new Error(
75
+ misplaced.path.endsWith("/_loading/")
76
+ ? `The app's ${misplaced.path} route ends with a slash: write it ${misplaced.path.slice(0, -1)}.`
77
+ : `The app's ${misplaced.path} route is a layout, which a screen doesn't have: the root layout is backtickApp(app, RootLayout)'s, and what screens share is a component they draw.`,
78
+ );
79
+ }
80
+ // The app's loading states, by pattern.
81
+ const loadings = app.routes
82
+ .filter(
83
+ ({ method, path }) => method === "GET" && path.endsWith("/_loading"),
84
+ )
85
+ .map(({ path }) => path);
86
+ return bundled(
87
+ context,
88
+ clientRuntime(
89
+ new URL(context.req.url).origin,
90
+ context.req.header("x-backtick-modules") ?? "{}",
91
+ RootLayout,
92
+ [...new Set(loadings)],
93
+ ),
94
+ );
86
95
  };
87
96
  }
97
+
98
+ // Where the Backtick app fetches the runtime it opens an app with.
99
+ const runtimePath = "/.well-known/backtick/runtime";
100
+
101
+ // `input` bundled for the modules the client sent, as the answer.
102
+ async function bundled(context: Context, input: Spliceable): Promise<Response> {
103
+ let modules: Modules;
104
+ try {
105
+ modules = JSON.parse(context.req.header("x-backtick-modules") ?? "{}");
106
+ } catch (error) {
107
+ const { name, message } = error as SyntaxError;
108
+ return context.json(
109
+ { name, message: `x-backtick-modules isn't JSON: ${message}` },
110
+ 400,
111
+ );
112
+ }
113
+ try {
114
+ const build = await bundler.build({ input, modules });
115
+ const { code } = build.generate({ format: "cjs" });
116
+ context.header("content-type", "text/javascript");
117
+ context.header("cache-control", "no-store");
118
+ return context.body(code);
119
+ } catch (error) {
120
+ if (error instanceof BundlerError) {
121
+ return context.json(
122
+ { name: error.name, message: error.message },
123
+ error.name === "ClientVersionMismatch" ? 426 : 400,
124
+ );
125
+ }
126
+ throw error;
127
+ }
128
+ }
@@ -0,0 +1,282 @@
1
+ // The phone's half of `backtickApp()`: how an app's screens are fetched,
2
+ // kept and shown, written as client code, which `backtickApp()` sends from
3
+ // `/.well-known/backtick/runtime`.
4
+ import { type Client, cs, exec } from "@backtickjs/core";
5
+ import {
6
+ QueryClient,
7
+ QueryClientProvider,
8
+ usePrefetchQuery,
9
+ useSuspenseQueries,
10
+ useSuspenseQuery,
11
+ } from "@tanstack/react-query" with { backtick: "client" };
12
+ import { PatternRouter } from "hono/router/pattern-router" with {
13
+ backtick: "client",
14
+ };
15
+ import {
16
+ Component,
17
+ type ComponentType,
18
+ type ReactNode,
19
+ Suspense,
20
+ useDeferredValue,
21
+ } from "react" with { backtick: "client" };
22
+
23
+ /**
24
+ * What the phone runs an app with. A shell checks for a member before using
25
+ * it, so a later runtime can drop one.
26
+ */
27
+ export interface Runtime {
28
+ /**
29
+ * The app's root layout, given the href on top. A navigation holds the
30
+ * current screen until the next can show, as navigations are transitions
31
+ * (`router.setTransitionMode("always")`): a route with a `_loading` once
32
+ * that's in, at once if it was prefetched; one without, once its page is
33
+ * in. Suspends until the first screen can show.
34
+ */
35
+ Navigator?: ComponentType<{ href: string }>;
36
+ /**
37
+ * A route's screen: what it renders, and meanwhile what its `_loading`
38
+ * renders. One not `active` yet shows nothing.
39
+ */
40
+ Screen?: ComponentType<{ href: string; active: boolean }>;
41
+ /**
42
+ * Fetches an href's screen again, which stays until the new one is in:
43
+ * resolves once it's in, either way.
44
+ */
45
+ reload?(href: string): Promise<void>;
46
+ /**
47
+ * Fetches an href ahead, as Next.js prefetches a link: its loading state,
48
+ * or, without one, its page. For each `<Link prefetch>`, which the shell
49
+ * sees as it's prefetched.
50
+ */
51
+ prefetch?(href: string): void;
52
+ }
53
+
54
+ /**
55
+ * A route's answer, run. Any bundle is an answer, whatever its status: an
56
+ * app's not-found screen is one. A refusal ({ name, message }) says why. For
57
+ * a route that may not exist (`notFoundIsNull`), a 404 is null.
58
+ */
59
+ export const fetchAnswer = cs`async (
60
+ url: string,
61
+ modules: string,
62
+ notFoundIsNull: boolean,
63
+ ) => {
64
+ const response = await fetch(url, {
65
+ headers: { "x-backtick-modules": modules },
66
+ });
67
+ if (notFoundIsNull && response.status === 404) {
68
+ return null;
69
+ }
70
+ const body = await response.text();
71
+ if (
72
+ !(response.headers.get("content-type") ?? "").startsWith("text/javascript")
73
+ ) {
74
+ let reason = body;
75
+ try {
76
+ reason = JSON.parse(body).message ?? body;
77
+ } catch {}
78
+ throw new Error(
79
+ "The screen couldn't be opened (" + response.status + "): " + reason,
80
+ );
81
+ }
82
+ return (await $exec(body as never)) as unknown;
83
+ }`;
84
+
85
+ /**
86
+ * The runtime of an open: for the phone at `origin` with `modules`, around
87
+ * the app's root layout, with the patterns of its `_loading` routes
88
+ * (`/city/:id/_loading`), so a screen asks only for those it has.
89
+ */
90
+ export function clientRuntime(
91
+ origin: string,
92
+ modules: string,
93
+ RootLayout: Client<ComponentType>,
94
+ loadingPatterns: readonly string[],
95
+ ): Client<Runtime> {
96
+ return cs`{
97
+ // Every answer is a query, keyed by its path: what the route renders. A
98
+ // screen is its page's, and its loading state's while the page loads.
99
+ // Each is reused, in memory, as Next.js's router cache does: a page for
100
+ // 30 seconds, a loading state for 5 minutes. A screen's no-store keeps it
101
+ // out of HTTP caches, not this one.
102
+ const pageStaleTime = 30000;
103
+ const loadingStaleTime = 300000;
104
+ const queryClient = new $QueryClient({
105
+ defaultOptions: {
106
+ queries: {
107
+ queryFn: async (context: { queryKey: readonly unknown[] }) =>
108
+ $fetchAnswer(
109
+ $origin + (context.queryKey[0] as string),
110
+ $modules,
111
+ false,
112
+ ),
113
+ staleTime: pageStaleTime,
114
+ structuralSharing: false,
115
+ retry: false,
116
+ },
117
+ },
118
+ });
119
+ // A loading state: none if it answers 404, for a param it has none for.
120
+ const loadingQueryOptions = (path: string) => ({
121
+ queryKey: [path],
122
+ staleTime: loadingStaleTime,
123
+ queryFn: async () => ({
124
+ value: (await $fetchAnswer(
125
+ $origin + path,
126
+ $modules,
127
+ true,
128
+ )) as ReactNode,
129
+ }),
130
+ });
131
+ // An href's loading state, if the app has a _loading route for it,
132
+ // matched by Hono's own router.
133
+ const loadingRouter = new $PatternRouter<true>();
134
+ for (const pattern of $loadingPatterns) {
135
+ loadingRouter.add("GET", pattern, true);
136
+ }
137
+ const loadingPathOf = (href: string) => {
138
+ const searchStart = href.indexOf("?");
139
+ const path = searchStart === -1 ? href : href.slice(0, searchStart);
140
+ const loading = (path === "/" ? "" : path) + "/_loading";
141
+ return loadingRouter.match("GET", loading)[0].length > 0
142
+ ? loading
143
+ : undefined;
144
+ };
145
+ // A stale answer no screen shows, in before a navigation started, is
146
+ // dropped as the navigation reads it, so the navigation waits for a fresh
147
+ // one, as Next.js's does; one a screen still shows, going back to it, is
148
+ // kept.
149
+ const dropStaleAnswer = (path: string, startedAt: number) => {
150
+ const query = queryClient
151
+ .getQueryCache()
152
+ .find({ queryKey: [path], exact: true });
153
+ if (
154
+ query !== undefined &&
155
+ query.getObserversCount() === 0 &&
156
+ query.state.data !== undefined &&
157
+ query.state.dataUpdatedAt < startedAt &&
158
+ query.isStaleByTime(pageStaleTime)
159
+ ) {
160
+ queryClient.removeQueries({ queryKey: [path], exact: true });
161
+ }
162
+ };
163
+
164
+ const Page = (props: { href: string }) =>
165
+ $useSuspenseQuery({ queryKey: [props.href] }).data as ReactNode;
166
+
167
+ // A screen as Next.js shows a page: with a loading state, inside a
168
+ // Suspense showing it; the page fetched meanwhile.
169
+ const PageWithLoading = (props: { href: string }) => {
170
+ const loading = loadingPathOf(props.href);
171
+ $usePrefetchQuery({ queryKey: [props.href] });
172
+ const queries: ReturnType<typeof loadingQueryOptions>[] =
173
+ loading === undefined ? [] : [loadingQueryOptions(loading)];
174
+ const loadingResult = $useSuspenseQueries({ queries })[0];
175
+ return loadingResult === undefined ? (
176
+ <Page href={props.href} />
177
+ ) : (
178
+ <$Suspense fallback={loadingResult.data.value}>
179
+ <Page href={props.href} />
180
+ </$Suspense>
181
+ );
182
+ };
183
+
184
+ // Fetches an href ahead, as Next.js prefetches: its loading state, or,
185
+ // without one, its page.
186
+ const prefetch = (href: string) => {
187
+ const loading = loadingPathOf(href);
188
+ if (loading === undefined) {
189
+ void queryClient.query({ queryKey: [href] }).catch(() => {});
190
+ return;
191
+ }
192
+ void queryClient
193
+ .query(loadingQueryOptions(loading))
194
+ .then((answer) =>
195
+ answer.value === null
196
+ ? queryClient.query({ queryKey: [href] })
197
+ : undefined,
198
+ )
199
+ .catch(() => {});
200
+ };
201
+
202
+ // An href changed in place (router.setParams) shows once it's in: the
203
+ // screen stays meanwhile.
204
+ const Screen = (props: { href: string; active: boolean }) => {
205
+ const href = $useDeferredValue(props.href);
206
+ return props.active ? (
207
+ <$Suspense fallback={null}>
208
+ <PageWithLoading href={href} />
209
+ </$Suspense>
210
+ ) : null;
211
+ };
212
+
213
+ // Suspends until a query is in. One that is, however old, holds nothing:
214
+ // a stale page was dropped above, and a stale loading state is refreshed
215
+ // by the screen, as it's only shown until the page is in.
216
+ const AwaitQuery = (props: { query: { queryKey: string[] } }) => {
217
+ $useSuspenseQuery({ ...props.query, refetchOnMount: false });
218
+ return null;
219
+ };
220
+ // When the navigation to the href on top started: kept outside React, as
221
+ // a hold that suspends keeps no state.
222
+ let currentNavigation = { href: "", startedAt: 0 };
223
+ // Suspends a navigation's render, above the Stack, until the href's
224
+ // loading state is in, or, without one, its page. As navigations are
225
+ // transitions, the screen on top stays. One that fails ends the hold, for
226
+ // the screen to show why.
227
+ const NavigationHold = (props: { href: string }) => {
228
+ if (currentNavigation.href !== props.href) {
229
+ currentNavigation = { href: props.href, startedAt: Date.now() };
230
+ }
231
+ dropStaleAnswer(props.href, currentNavigation.startedAt);
232
+ // A route with a loading state pushes as soon as its loading state is
233
+ // in, at once if it was prefetched; its page is fetched meanwhile.
234
+ const loading = loadingPathOf(props.href);
235
+ if (loading !== undefined) {
236
+ void queryClient.query({ queryKey: [props.href] }).catch(() => {});
237
+ return <AwaitQuery query={loadingQueryOptions(loading)} />;
238
+ }
239
+ return <AwaitQuery query={{ queryKey: [props.href] }} />;
240
+ };
241
+ class SilentErrorBoundary extends $Component<
242
+ { children: ReactNode },
243
+ { failed: boolean }
244
+ > {
245
+ state = { failed: false };
246
+ static getDerivedStateFromError() {
247
+ return { failed: true };
248
+ }
249
+ render() {
250
+ return this.state.failed ? null : this.props.children;
251
+ }
252
+ }
253
+
254
+ const Navigator = (props: { href: string }) => (
255
+ <$QueryClientProvider client={queryClient}>
256
+ <SilentErrorBoundary key={props.href}>
257
+ <NavigationHold href={props.href} />
258
+ </SilentErrorBoundary>
259
+ <$RootLayout />
260
+ </$QueryClientProvider>
261
+ );
262
+
263
+ const runtime: Runtime = {
264
+ Navigator,
265
+ Screen,
266
+ prefetch,
267
+ // Each of its queries again: one that failed, for its screen to wait
268
+ // for it; the others kept until the new answer is in.
269
+ reload: async (href) => {
270
+ const loading = loadingPathOf(href);
271
+ await Promise.allSettled(
272
+ (loading === undefined ? [href] : [href, loading]).map((path) =>
273
+ queryClient.getQueryState([path])?.status === "error"
274
+ ? queryClient.resetQueries({ queryKey: [path], exact: true })
275
+ : queryClient.refetchQueries({ queryKey: [path], exact: true }),
276
+ ),
277
+ );
278
+ },
279
+ };
280
+ return runtime;
281
+ }`;
282
+ }