@backtickjs/hono 0.2.8 → 0.3.1

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,53 @@ 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
 
20
22
  const app = new Hono();
21
23
 
22
- app.use(backtick());
24
+ app.use(backtickApp(app));
25
+ // The root layout, around every screen: the navigator.
26
+ app.get("/_layout", (context) => context.render(cs`() => <$Stack />`));
23
27
  app.get("/", (context) => context.render(<Home />));
24
28
 
25
29
  export default app;
26
30
  ```
27
31
 
32
+ The Backtick app opens an app from one answer, which `backtickApp(app)` answers: the client code that fetches each screen and keeps it while it shows. Navigation works as in Next.js, whose folders are routes here. `/_layout` is the root layout, a client component that draws the navigator; unlike Next.js's, it takes no `children`, as the navigator renders the screens. An app without one doesn't open. A screen is each segment's `_layout` (`/day/:id/_layout`, rendered for its path, answering a client component given `children`), around its `_loading` while what's under it loads, around the page. A loading state takes no params, as in Next.js: it's fetched once for its pattern, and reading a param throws; what depends on the path, such as the title, goes in a `_layout`. Every link fetches ahead what its screen needs to show, its layouts down to its first loading state and that loading state, as Next.js's `Link` prefetches; `<Link prefetch>` adds the page, and `router.prefetch(href)` fetches as a link does. A navigation pushes once those are in, at once if they were fetched ahead; with no loading state, it holds the current screen until all of it is in, as expo-router's `unstable_useIsNavigating()` says, for a progress bar. Going back shows a screen as it was. A client with a navigator of its own uses `backtickRenderer()` alone.
33
+
34
+ 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:
35
+
36
+ ```tsx
37
+ import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query" with {
38
+ backtick: "client",
39
+ };
40
+
41
+ const Part = cs`(props: { path: string }) =>
42
+ $useSuspenseQuery({ queryKey: [props.path] }).data as ReactNode`;
43
+
44
+ const Cart = cs`() => {
45
+ const queryClient = $useQueryClient();
46
+ return (
47
+ <>
48
+ <$Suspense fallback={<$ActivityIndicator />}>
49
+ <$Part path="/cart/items" />
50
+ </$Suspense>
51
+ <$Button
52
+ title="Refresh"
53
+ onPress={() =>
54
+ queryClient.invalidateQueries({ queryKey: ["/cart/items"] })
55
+ }
56
+ />
57
+ </>
58
+ );
59
+ }`;
60
+ app.get("/cart", (context) => context.render(cs`<$Cart />`));
61
+ ```
62
+
28
63
  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
64
 
30
65
  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,38 +1,45 @@
1
1
  import type { Spliceable, SplicesAs } from "@backtickjs/core";
2
- import type { MiddlewareHandler } from "hono";
2
+ import type { Hono, MiddlewareHandler } from "hono";
3
+ export type { Runtime } from "./runtime.js";
3
4
  declare module "hono" {
4
5
  interface ContextRenderer {
5
6
  <T extends [T] extends [SplicesAs<T>] ? unknown : Spliceable>(input: T): Promise<Response>;
6
7
  }
7
8
  }
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
- }
20
9
  /**
21
10
  * Hono's renderer for Backtick screens: under it, `context.render(input)`
22
11
  * answers with `input` bundled for the modules the client sent in
23
12
  * `x-backtick-modules`, so each route is a screen.
24
13
  *
25
- * app.use(backtick());
14
+ * app.use(backtickRenderer());
26
15
  * app.get("/", (context) => context.render(<Home />));
27
16
  *
28
17
  * The status is the context's, so `context.status(404)` makes a not-found
29
- * screen, and screens are `no-store`: each opens as its latest copy. A client
18
+ * screen, and screens are `no-store`, kept out of HTTP caches: a reload or
19
+ * a new open always gets the latest copy. (The Backtick app fetches a page anew
20
+ * for each navigation, as Next.js's router does.) A client
30
21
  * without a module the screen needs gets a 426, and any other refusal of the
31
- * bundler's a 400, each with `{ name, message }`.
22
+ * bundler's a 400, each with `{ name, message }`. An app the Backtick app
23
+ * opens uses `backtickApp(app)` instead.
24
+ */
25
+ export declare function backtickRenderer(): MiddlewareHandler;
26
+ /**
27
+ * Hono's renderer for an app the Backtick app opens, as `backtickRenderer()`,
28
+ * which also answers the runtime the Backtick app fetches when it opens
29
+ * `app`: the client code that fetches, keeps and shows its screens.
30
+ *
31
+ * app.use(backtickApp(app));
32
+ * app.get("/_layout", (context) => context.render(cs`() => <$Stack />`));
33
+ * app.get("/", (context) => context.render(<Home />));
32
34
  *
33
- * `manifest` is the app's web app manifest, sent in `x-backtick-manifest` on
34
- * every screen, so it's as fresh as they are.
35
+ * Routes nest as Next.js's folders. `/_layout` is the root layout, a client
36
+ * component drawing the navigator; an app without one doesn't open. A screen
37
+ * is each segment's `_layout` (`/city/:id/_layout`, rendered for its path,
38
+ * answering a client component given `children`), around its `_loading`
39
+ * while what's under it loads, around the page. A loading state takes no
40
+ * params, as in Next.js: it's fetched once for its pattern, and reading a
41
+ * param throws. A navigation pushes once its layouts down to its first
42
+ * loading state, and that loading state, are in; with none, once all of it
43
+ * is. Layout and loading routes written with a trailing slash are refused.
35
44
  */
36
- export declare function backtick(options?: {
37
- manifest?: Manifest;
38
- }): MiddlewareHandler;
45
+ export declare function backtickApp(app: Hono<any, any, any>): MiddlewareHandler;
package/dist/index.js CHANGED
@@ -1,51 +1,96 @@
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 fetches a page anew
14
+ * for each navigation, 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 }`.
14
- *
15
- * `manifest` is the app's web app manifest, sent in `x-backtick-manifest` on
16
- * every screen, so it's as fresh as they are.
16
+ * bundler's a 400, each with `{ name, message }`. An app the Backtick app
17
+ * opens uses `backtickApp(app)` instead.
17
18
  */
18
- export function backtick(options = {}) {
19
- const { manifest } = options;
20
- const json = JSON.stringify(manifest);
19
+ export function backtickRenderer() {
21
20
  return async (context, next) => {
22
- context.setRenderer(async (input) => {
23
- let modules;
24
- try {
25
- modules = JSON.parse(context.req.header("x-backtick-modules") ?? "{}");
26
- }
27
- catch (error) {
28
- const { name, message } = error;
29
- return context.json({ name, message: `x-backtick-modules isn't JSON: ${message}` }, 400);
30
- }
31
- try {
32
- const build = await bundler.build({ input, modules });
33
- const { code } = build.generate({ format: "cjs" });
34
- context.header("content-type", "text/javascript");
35
- context.header("cache-control", "no-store");
36
- if (manifest !== undefined) {
37
- context.header("x-backtick-manifest", encodeURIComponent(json));
38
- }
39
- return context.body(code);
40
- }
41
- catch (error) {
42
- if (error instanceof BundlerError) {
43
- return context.json({ name: error.name, message: error.message }, error.name === "ClientVersionMismatch" ? 426 : 400);
44
- }
45
- throw error;
46
- }
47
- });
21
+ context.setRenderer(async (input) => bundled(context, input));
48
22
  await next();
49
23
  };
50
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.
29
+ *
30
+ * app.use(backtickApp(app));
31
+ * app.get("/_layout", (context) => context.render(cs`() => <$Stack />`));
32
+ * app.get("/", (context) => context.render(<Home />));
33
+ *
34
+ * Routes nest as Next.js's folders. `/_layout` is the root layout, a client
35
+ * component drawing the navigator; an app without one doesn't open. A screen
36
+ * is each segment's `_layout` (`/city/:id/_layout`, rendered for its path,
37
+ * answering a client component given `children`), around its `_loading`
38
+ * while what's under it loads, around the page. A loading state takes no
39
+ * params, as in Next.js: it's fetched once for its pattern, and reading a
40
+ * param throws. A navigation pushes once its layouts down to its first
41
+ * loading state, and that loading state, are in; with none, once all of it
42
+ * is. Layout and loading routes written with a trailing slash are refused.
43
+ */
44
+ export function backtickApp(app) {
45
+ const renderer = backtickRenderer();
46
+ return async (context, next) => {
47
+ if (context.req.path.endsWith("/_loading")) {
48
+ context.req.param = () => {
49
+ throw new Error(`${context.req.routePath} reads a param: a loading state takes none, as in Next.js. What depends on the path goes in a _layout.`);
50
+ };
51
+ }
52
+ if (context.req.method !== "GET" || context.req.path !== runtimePath) {
53
+ return renderer(context, next);
54
+ }
55
+ const patterns = (name) => [
56
+ ...new Set(app.routes
57
+ .filter(({ method, path }) => method === "GET" && path.endsWith(name))
58
+ .map(({ path }) => path)),
59
+ ];
60
+ const misplaced = app.routes.find(({ path }) => path.endsWith("/_layout/") || path.endsWith("/_loading/"));
61
+ if (misplaced !== undefined) {
62
+ throw new Error(`The app's ${misplaced.path} route ends with a slash: write it ${misplaced.path.slice(0, -1)}.`);
63
+ }
64
+ if (!patterns("/_layout").includes("/_layout")) {
65
+ throw new Error("The app has no /_layout route: it's the root layout, a client component drawing the navigator, as cs`() => <$Stack />`.");
66
+ }
67
+ return bundled(context, clientRuntime(new URL(context.req.url).origin, context.req.header("x-backtick-modules") ?? "{}", { layouts: patterns("/_layout"), loadings: patterns("/_loading") }));
68
+ };
69
+ }
70
+ // Where the Backtick app fetches the runtime it opens an app with.
71
+ const runtimePath = "/.well-known/backtick/runtime";
72
+ // `input` bundled for the modules the client sent, as the answer.
73
+ async function bundled(context, input) {
74
+ let modules;
75
+ try {
76
+ modules = JSON.parse(context.req.header("x-backtick-modules") ?? "{}");
77
+ }
78
+ catch (error) {
79
+ const { name, message } = error;
80
+ return context.json({ name, message: `x-backtick-modules isn't JSON: ${message}` }, 400);
81
+ }
82
+ try {
83
+ const build = await bundler.build({ input, modules });
84
+ const { code } = build.generate({ format: "cjs" });
85
+ context.header("content-type", "text/javascript");
86
+ context.header("cache-control", "no-store");
87
+ return context.body(code);
88
+ }
89
+ catch (error) {
90
+ if (error instanceof BundlerError) {
91
+ return context.json({ name: error.name, message: error.message }, error.name === "ClientVersionMismatch" ? 426 : 400);
92
+ }
93
+ throw error;
94
+ }
95
+ }
51
96
  //# 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;AAuB1E;;;;;;;;;;;;;;;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,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,54 @@
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")`): once its layouts down to its
12
+ * first loading state, and that loading state, are in, at once if they
13
+ * were prefetched; with no loading state, once all of it is in. Suspends
14
+ * until the first screen can show.
15
+ */
16
+ Navigator?: ComponentType<{
17
+ href: string;
18
+ }>;
19
+ /**
20
+ * A route's screen, nested as Next.js nests a page: each segment's layout,
21
+ * around its loading state while what's under it loads. One not `active`
22
+ * yet shows nothing.
23
+ */
24
+ Screen?: ComponentType<{
25
+ href: string;
26
+ active: boolean;
27
+ }>;
28
+ /**
29
+ * Fetches an href's screen again, which stays until the new one is in:
30
+ * resolves once it's in, either way.
31
+ */
32
+ reload?(href: string): Promise<void>;
33
+ /**
34
+ * Fetches an href ahead, as Next.js's `Link` prefetches: its layouts down
35
+ * to its first loading state, and that loading state (nothing if it has
36
+ * none); with `page`, its page too, as `<Link prefetch>`.
37
+ */
38
+ prefetch?(href: string, page?: boolean): void;
39
+ }
40
+ /**
41
+ * A route's answer, run. Any bundle is an answer, whatever its status: an
42
+ * app's not-found screen is one. A refusal ({ name, message }) says why. For
43
+ * a route that may not exist (`notFoundIsNull`), a 404 is null.
44
+ */
45
+ export declare const fetchAnswer: Client<unknown>;
46
+ /**
47
+ * The runtime of an open: for the phone at `origin` with `modules`, with the
48
+ * patterns of the app's `_layout` and `_loading` routes (`/city/:id/_layout`),
49
+ * so a screen asks only for those it has.
50
+ */
51
+ export declare function clientRuntime(origin: string, modules: string, routes: {
52
+ layouts: readonly string[];
53
+ loadings: readonly string[];
54
+ }): 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: "3t7t976h189wg:61: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\":\";;;kBA4D8BA,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: "3t7t976h189wg:97: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, $splice19, $splice20, $splice21, $splice22) => {\n var segmentStaleTime = 300000;\n var currentNavigation = {\n href: \"\",\n startedAt: 0\n };\n var fetchedAhead = new Set();\n var pageStaleTime = query => query.state.dataUpdatedAt >= currentNavigation.startedAt ? Infinity : fetchedAhead.has(query.queryKey[0]) ? segmentStaleTime : 0;\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 pageQuery = href => ({\n queryKey: [href]\n });\n var layoutQuery = path => ({\n queryKey: [path],\n staleTime: segmentStaleTime\n });\n var loadingQuery = pattern => ({\n queryKey: [pattern],\n staleTime: segmentStaleTime,\n queryFn: async () => ({\n value: await $splice4()($splice5() + pattern, $splice6(), false)\n })\n });\n var layoutRouter = new ($splice7())();\n for (var pattern of $splice8().layouts) {\n layoutRouter.add(\"GET\", pattern, pattern);\n }\n var loadingRouter = new ($splice9())();\n for (var _pattern of $splice10().loadings) {\n loadingRouter.add(\"GET\", _pattern, _pattern);\n }\n var segmentsOf = href => {\n var searchStart = href.indexOf(\"?\");\n var path = searchStart === -1 ? href : href.slice(0, searchStart);\n var names = path.split(\"/\").filter(Boolean);\n var bases = [\"\", ...names.map((_, i) => \"/\" + names.slice(0, i + 1).join(\"/\"))];\n return bases.map(base => {\n var layout = base + \"/_layout\";\n return {\n layout: base !== \"\" && layoutRouter.match(\"GET\", layout)[0].length > 0 ? layout : undefined,\n loading: loadingRouter.match(\"GET\", base + \"/_loading\")[0][0]?.[0]\n };\n });\n };\n var shellOf = href => {\n var queries = [];\n for (var segment of segmentsOf(href)) {\n if (segment.layout !== undefined) {\n queries.push(layoutQuery(segment.layout));\n }\n if (segment.loading !== undefined) {\n queries.push(loadingQuery(segment.loading));\n return {\n queries,\n loads: true\n };\n }\n }\n queries.push(pageQuery(href));\n return {\n queries,\n loads: false\n };\n };\n var dropStaleAnswer = (query, startedAt) => {\n var cached = queryClient.getQueryCache().find({\n queryKey: query.queryKey,\n exact: true\n });\n if (cached !== undefined && cached.getObserversCount() === 0 && cached.state.data !== undefined && cached.state.dataUpdatedAt < startedAt && cached.isStaleByTime(query.staleTime ?? pageStaleTime(cached))) {\n queryClient.removeQueries({\n queryKey: query.queryKey,\n exact: true\n });\n }\n };\n var Page = props => $splice11()(pageQuery(props.href)).data;\n var Loading = props => $splice12()(loadingQuery(props.pattern)).data.value;\n var Layout = props => {\n var answer = $splice13()(layoutQuery(props.path)).data;\n if (typeof answer !== \"function\") {\n return answer;\n }\n var Answer = answer;\n return (0, jsx_runtime_1.jsx)(Answer, {\n children: props.children\n });\n };\n var Segments = props => {\n $splice14()(pageQuery(props.href));\n var node = (0, jsx_runtime_1.jsx)(Page, {\n href: props.href\n });\n var _loop = function (segment) {\n if (segment.loading !== undefined) {\n node = ($Suspense => (0, jsx_runtime_1.jsx)($Suspense, {\n fallback: (0, jsx_runtime_1.jsx)(Loading, {\n pattern: segment.loading\n }),\n children: node\n }))($splice15());\n }\n if (segment.layout !== undefined) {\n node = (0, jsx_runtime_1.jsx)(Layout, {\n path: segment.layout,\n children: node\n });\n }\n };\n for (var segment of segmentsOf(props.href).reverse()) {\n _loop(segment);\n }\n return node;\n };\n var prefetch = (href, page = false) => {\n var shell = shellOf(href);\n if (page) {\n fetchedAhead.add(href);\n }\n var queries = page ? [...shell.queries, pageQuery(href)] : shell.loads ? shell.queries : [];\n for (var query of queries) {\n void queryClient.query(query).catch(() => { });\n }\n };\n for (var _pattern2 of $splice16().loadings) {\n void queryClient.query(loadingQuery(_pattern2)).catch(() => { });\n }\n var Screen = props => {\n var href = $splice17()(props.href);\n return props.active ? ($Suspense => (0, jsx_runtime_1.jsx)($Suspense, {\n fallback: null,\n children: (0, jsx_runtime_1.jsx)(Segments, {\n href: href\n })\n }))($splice18()) : null;\n };\n var AwaitQueries = props => {\n $splice19()({\n queries: props.queries.map(query => ({\n ...query,\n refetchOnMount: false\n }))\n });\n return null;\n };\n var NavigationHold = props => {\n if (currentNavigation.href !== props.href) {\n currentNavigation = {\n href: props.href,\n startedAt: Date.now()\n };\n }\n var shell = shellOf(props.href);\n for (var query of [...shell.queries, pageQuery(props.href)]) {\n dropStaleAnswer(query, currentNavigation.startedAt);\n }\n var shown = (queryClient.getQueryCache().find({\n queryKey: [props.href],\n exact: true\n })?.getObserversCount() ?? 0) > 0;\n if (!shown) {\n void queryClient.query(pageQuery(props.href)).catch(() => { });\n }\n return (0, jsx_runtime_1.jsx)(AwaitQueries, {\n queries: shell.queries\n });\n };\n class SilentErrorBoundary extends $splice20() {\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 RootLayout = () => {\n var answer = $splice21()({\n queryKey: [\"/_layout\"],\n staleTime: Infinity\n }).data;\n if (typeof answer !== \"function\") {\n throw new Error(\"The app's /_layout answers no component: it's the root layout, a client component drawing the navigator (a Stack).\");\n }\n var Answer = answer;\n return (0, jsx_runtime_1.jsx)(Answer, {});\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), (0, jsx_runtime_1.jsx)(RootLayout, {})]\n }))($splice22());\n var runtime = {\n Navigator,\n Screen,\n prefetch,\n reload: async (href) => {\n var paths = [href, ...segmentsOf(href).flatMap(segment => segment.layout === undefined ? [] : [segment.layout])];\n await Promise.allSettled(paths.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\",\"$splice19\",\"$splice20\",\"$splice21\",\"$splice22\",\"segmentStaleTime\",\"currentNavigation\",\"href\",\"startedAt\",\"fetchedAhead\",\"Set\",\"pageStaleTime\",\"query\",\"state\",\"dataUpdatedAt\",\"Infinity\",\"has\",\"queryKey\",\"queryClient\",\"defaultOptions\",\"queries\",\"queryFn\",\"context\",\"staleTime\",\"structuralSharing\",\"retry\",\"pageQuery\",\"layoutQuery\",\"path\",\"loadingQuery\",\"pattern\",\"value\",\"layoutRouter\",\"layouts\",\"add\",\"loadingRouter\",\"loadings\",\"segmentsOf\",\"searchStart\",\"indexOf\",\"slice\",\"names\",\"split\",\"filter\",\"Boolean\",\"bases\",\"map\",\"_\",\"i\",\"join\",\"base\",\"layout\",\"match\",\"length\",\"undefined\",\"loading\",\"shellOf\",\"segment\",\"push\",\"loads\",\"dropStaleAnswer\",\"cached\",\"getQueryCache\",\"find\",\"exact\",\"getObserversCount\",\"data\",\"isStaleByTime\",\"removeQueries\",\"Page\",\"props\",\"Loading\",\"Layout\",\"answer\",\"Answer\",\"_jsx\",\"children\",\"Segments\",\"node\",\"_loop\",\"$Suspense\",\"fallback\",\"reverse\",\"prefetch\",\"page\",\"shell\",\"catch\",\"Screen\",\"active\",\"AwaitQueries\",\"refetchOnMount\",\"NavigationHold\",\"Date\",\"now\",\"shown\",\"SilentErrorBoundary\",\"failed\",\"getDerivedStateFromError\",\"render\",\"RootLayout\",\"Error\",\"Navigator\",\"$QueryClientProvider\",\"_jsxs\",\"client\",\"runtime\",\"reload\",\"paths\",\"flatMap\",\"Promise\",\"allSettled\",\"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: [] }, { 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`, with the
38
+ * patterns of the app's `_layout` and `_loading` routes (`/city/:id/_layout`),
39
+ * so a screen asks only for those it has.
40
+ */
41
+ export function clientRuntime(origin, modules, routes) {
42
+ return cs.create($module1, [QueryClient, fetchAnswer, origin, modules, fetchAnswer, origin, modules, PatternRouter, routes, PatternRouter, routes, useSuspenseQuery, useSuspenseQuery, useSuspenseQuery, usePrefetchQuery, Suspense, routes, useDeferredValue, Suspense, useSuspenseQueries, Component, useSuspenseQuery, QueryClientProvider]);
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;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDzD;;;;GAIG;AACH,MAAM,CAAC,MAAM,WAAW,8BAwBtB,CAAC;AAEH;;;;GAIG;AACH,MAAM,UAAU,aAAa,CAC3B,MAAc,EACd,OAAe,EACf,MAAmE;IAEnE,gVAkSG;AACL,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backtickjs/hono",
3
- "version": "0.2.8",
3
+ "version": "0.3.1",
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.8",
41
- "@backtickjs/core": "0.2.8"
40
+ "@backtickjs/bundler": "0.3.1",
41
+ "@backtickjs/core": "0.3.1"
42
42
  },
43
43
  "peerDependencies": {
44
44
  "hono": "^4.13.13"
45
45
  },
46
46
  "devDependencies": {
47
+ "@backtickjs/expo": "0.3.1",
48
+ "@backtickjs/jsx-react": "0.3.1",
49
+ "@backtickjs/prettier-plugin": "0.3.1",
50
+ "@backtickjs/tsc": "0.3.1",
51
+ "@backtickjs/tspatch-plugin": "0.3.1",
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,12 @@
1
1
  import { bundler, BundlerError, type Modules } from "@backtickjs/bundler";
2
2
  import type { Spliceable, SplicesAs } from "@backtickjs/core";
3
- import type { MiddlewareHandler } from "hono";
3
+ import type { Context, Hono, MiddlewareHandler } from "hono";
4
+ import { clientRuntime } from "./runtime.js";
5
+
6
+ export type { Runtime } from "./runtime.js";
4
7
 
5
8
  declare module "hono" {
6
- // What `context.render` takes under `backtick()`, as
9
+ // What `context.render` takes under `backtickRenderer()`, as
7
10
  // `jsxRenderer()` types its own.
8
11
  interface ContextRenderer {
9
12
  <T extends [T] extends [SplicesAs<T>] ? unknown : Spliceable>(
@@ -12,67 +15,120 @@ declare module "hono" {
12
15
  }
13
16
  }
14
17
 
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
- }
23
-
24
18
  /**
25
19
  * Hono's renderer for Backtick screens: under it, `context.render(input)`
26
20
  * answers with `input` bundled for the modules the client sent in
27
21
  * `x-backtick-modules`, so each route is a screen.
28
22
  *
29
- * app.use(backtick());
23
+ * app.use(backtickRenderer());
30
24
  * app.get("/", (context) => context.render(<Home />));
31
25
  *
32
26
  * The status is the context's, so `context.status(404)` makes a not-found
33
- * screen, and screens are `no-store`: each opens as its latest copy. A client
27
+ * screen, and screens are `no-store`, kept out of HTTP caches: a reload or
28
+ * a new open always gets the latest copy. (The Backtick app fetches a page anew
29
+ * for each navigation, as Next.js's router does.) A client
34
30
  * without a module the screen needs gets a 426, and any other refusal of the
35
- * bundler's a 400, each with `{ name, message }`.
31
+ * bundler's a 400, each with `{ name, message }`. An app the Backtick app
32
+ * opens uses `backtickApp(app)` instead.
33
+ */
34
+ export function backtickRenderer(): MiddlewareHandler {
35
+ return async (context, next) => {
36
+ context.setRenderer(async (input) => bundled(context, input as Spliceable));
37
+ await next();
38
+ };
39
+ }
40
+
41
+ /**
42
+ * Hono's renderer for an app the Backtick app opens, as `backtickRenderer()`,
43
+ * which also answers the runtime the Backtick app fetches when it opens
44
+ * `app`: the client code that fetches, keeps and shows its screens.
45
+ *
46
+ * app.use(backtickApp(app));
47
+ * app.get("/_layout", (context) => context.render(cs`() => <$Stack />`));
48
+ * app.get("/", (context) => context.render(<Home />));
36
49
  *
37
- * `manifest` is the app's web app manifest, sent in `x-backtick-manifest` on
38
- * every screen, so it's as fresh as they are.
50
+ * Routes nest as Next.js's folders. `/_layout` is the root layout, a client
51
+ * component drawing the navigator; an app without one doesn't open. A screen
52
+ * is each segment's `_layout` (`/city/:id/_layout`, rendered for its path,
53
+ * answering a client component given `children`), around its `_loading`
54
+ * while what's under it loads, around the page. A loading state takes no
55
+ * params, as in Next.js: it's fetched once for its pattern, and reading a
56
+ * param throws. A navigation pushes once its layouts down to its first
57
+ * loading state, and that loading state, are in; with none, once all of it
58
+ * is. Layout and loading routes written with a trailing slash are refused.
39
59
  */
40
- export function backtick(
41
- options: { manifest?: Manifest } = {},
42
- ): MiddlewareHandler {
43
- const { manifest } = options;
44
- const json = JSON.stringify(manifest);
60
+ export function backtickApp(app: Hono<any, any, any>): MiddlewareHandler {
61
+ const renderer = backtickRenderer();
45
62
  return async (context, next) => {
46
- context.setRenderer(async (input) => {
47
- let modules: Modules;
48
- try {
49
- modules = JSON.parse(context.req.header("x-backtick-modules") ?? "{}");
50
- } catch (error) {
51
- const { name, message } = error as SyntaxError;
52
- return context.json(
53
- { name, message: `x-backtick-modules isn't JSON: ${message}` },
54
- 400,
63
+ if (context.req.path.endsWith("/_loading")) {
64
+ context.req.param = () => {
65
+ throw new Error(
66
+ `${context.req.routePath} reads a param: a loading state takes none, as in Next.js. What depends on the path goes in a _layout.`,
55
67
  );
56
- }
57
- try {
58
- const build = await bundler.build({ input, modules });
59
- const { code } = build.generate({ format: "cjs" });
60
- context.header("content-type", "text/javascript");
61
- context.header("cache-control", "no-store");
62
- if (manifest !== undefined) {
63
- context.header("x-backtick-manifest", encodeURIComponent(json));
64
- }
65
- return context.body(code);
66
- } catch (error) {
67
- if (error instanceof BundlerError) {
68
- return context.json(
69
- { name: error.name, message: error.message },
70
- error.name === "ClientVersionMismatch" ? 426 : 400,
71
- );
72
- }
73
- throw error;
74
- }
75
- });
76
- await next();
68
+ };
69
+ }
70
+ if (context.req.method !== "GET" || context.req.path !== runtimePath) {
71
+ return renderer(context, next);
72
+ }
73
+ const patterns = (name: string) => [
74
+ ...new Set(
75
+ app.routes
76
+ .filter(({ method, path }) => method === "GET" && path.endsWith(name))
77
+ .map(({ path }) => path),
78
+ ),
79
+ ];
80
+ const misplaced = app.routes.find(
81
+ ({ path }) => path.endsWith("/_layout/") || path.endsWith("/_loading/"),
82
+ );
83
+ if (misplaced !== undefined) {
84
+ throw new Error(
85
+ `The app's ${misplaced.path} route ends with a slash: write it ${misplaced.path.slice(0, -1)}.`,
86
+ );
87
+ }
88
+ if (!patterns("/_layout").includes("/_layout")) {
89
+ throw new Error(
90
+ "The app has no /_layout route: it's the root layout, a client component drawing the navigator, as cs`() => <$Stack />`.",
91
+ );
92
+ }
93
+ return bundled(
94
+ context,
95
+ clientRuntime(
96
+ new URL(context.req.url).origin,
97
+ context.req.header("x-backtick-modules") ?? "{}",
98
+ { layouts: patterns("/_layout"), loadings: patterns("/_loading") },
99
+ ),
100
+ );
77
101
  };
78
102
  }
103
+
104
+ // Where the Backtick app fetches the runtime it opens an app with.
105
+ const runtimePath = "/.well-known/backtick/runtime";
106
+
107
+ // `input` bundled for the modules the client sent, as the answer.
108
+ async function bundled(context: Context, input: Spliceable): Promise<Response> {
109
+ let modules: Modules;
110
+ try {
111
+ modules = JSON.parse(context.req.header("x-backtick-modules") ?? "{}");
112
+ } catch (error) {
113
+ const { name, message } = error as SyntaxError;
114
+ return context.json(
115
+ { name, message: `x-backtick-modules isn't JSON: ${message}` },
116
+ 400,
117
+ );
118
+ }
119
+ try {
120
+ const build = await bundler.build({ input, modules });
121
+ const { code } = build.generate({ format: "cjs" });
122
+ context.header("content-type", "text/javascript");
123
+ context.header("cache-control", "no-store");
124
+ return context.body(code);
125
+ } catch (error) {
126
+ if (error instanceof BundlerError) {
127
+ return context.json(
128
+ { name: error.name, message: error.message },
129
+ error.name === "ClientVersionMismatch" ? 426 : 400,
130
+ );
131
+ }
132
+ throw error;
133
+ }
134
+ }
@@ -0,0 +1,388 @@
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")`): once its layouts down to its
32
+ * first loading state, and that loading state, are in, at once if they
33
+ * were prefetched; with no loading state, once all of it is in. Suspends
34
+ * until the first screen can show.
35
+ */
36
+ Navigator?: ComponentType<{ href: string }>;
37
+ /**
38
+ * A route's screen, nested as Next.js nests a page: each segment's layout,
39
+ * around its loading state while what's under it loads. One not `active`
40
+ * yet shows nothing.
41
+ */
42
+ Screen?: ComponentType<{ href: string; active: boolean }>;
43
+ /**
44
+ * Fetches an href's screen again, which stays until the new one is in:
45
+ * resolves once it's in, either way.
46
+ */
47
+ reload?(href: string): Promise<void>;
48
+ /**
49
+ * Fetches an href ahead, as Next.js's `Link` prefetches: its layouts down
50
+ * to its first loading state, and that loading state (nothing if it has
51
+ * none); with `page`, its page too, as `<Link prefetch>`.
52
+ */
53
+ prefetch?(href: string, page?: boolean): void;
54
+ }
55
+
56
+ /**
57
+ * A route's answer, run. Any bundle is an answer, whatever its status: an
58
+ * app's not-found screen is one. A refusal ({ name, message }) says why. For
59
+ * a route that may not exist (`notFoundIsNull`), a 404 is null.
60
+ */
61
+ export const fetchAnswer = cs`async (
62
+ url: string,
63
+ modules: string,
64
+ notFoundIsNull: boolean,
65
+ ) => {
66
+ const response = await fetch(url, {
67
+ headers: { "x-backtick-modules": modules },
68
+ });
69
+ if (notFoundIsNull && response.status === 404) {
70
+ return null;
71
+ }
72
+ const body = await response.text();
73
+ if (
74
+ !(response.headers.get("content-type") ?? "").startsWith("text/javascript")
75
+ ) {
76
+ let reason = body;
77
+ try {
78
+ reason = JSON.parse(body).message ?? body;
79
+ } catch {}
80
+ throw new Error(
81
+ "The screen couldn't be opened (" + response.status + "): " + reason,
82
+ );
83
+ }
84
+ return (await $exec(body as never)) as unknown;
85
+ }`;
86
+
87
+ /**
88
+ * The runtime of an open: for the phone at `origin` with `modules`, with the
89
+ * patterns of the app's `_layout` and `_loading` routes (`/city/:id/_layout`),
90
+ * so a screen asks only for those it has.
91
+ */
92
+ export function clientRuntime(
93
+ origin: string,
94
+ modules: string,
95
+ routes: { layouts: readonly string[]; loadings: readonly string[] },
96
+ ): Client<Runtime> {
97
+ return cs`{
98
+ // Every answer is a query, keyed by where it was fetched, and reused in
99
+ // memory as Next.js's router cache does: a layout or a loading state for 5
100
+ // minutes; a page only for the navigation it was fetched for (0 s), or, fetched
101
+ // ahead by <Link prefetch>, for 5 minutes. A screen's no-store keeps it
102
+ // out of HTTP caches, not this one.
103
+ const segmentStaleTime = 300000;
104
+ // When the navigation to the href on top started: kept outside React, as
105
+ // a hold that suspends keeps no state.
106
+ let currentNavigation = { href: "", startedAt: 0 };
107
+ const fetchedAhead = new Set<string>();
108
+ const pageStaleTime = (query: {
109
+ queryKey: readonly unknown[];
110
+ state: { dataUpdatedAt: number };
111
+ }) =>
112
+ query.state.dataUpdatedAt >= currentNavigation.startedAt
113
+ ? Infinity
114
+ : fetchedAhead.has(query.queryKey[0] as string)
115
+ ? segmentStaleTime
116
+ : 0;
117
+ const queryClient = new $QueryClient({
118
+ defaultOptions: {
119
+ queries: {
120
+ queryFn: async (context: { queryKey: readonly unknown[] }) =>
121
+ $fetchAnswer(
122
+ $origin + (context.queryKey[0] as string),
123
+ $modules,
124
+ false,
125
+ ),
126
+ staleTime: pageStaleTime,
127
+ structuralSharing: false,
128
+ retry: false,
129
+ },
130
+ },
131
+ });
132
+ type Query = {
133
+ queryKey: string[];
134
+ staleTime?: number;
135
+ queryFn?: () => Promise<unknown>;
136
+ };
137
+ const pageQuery = (href: string): Query => ({ queryKey: [href] });
138
+ const layoutQuery = (path: string): Query => ({
139
+ queryKey: [path],
140
+ staleTime: segmentStaleTime,
141
+ });
142
+ // A loading state takes no params, as in Next.js: it's fetched at its
143
+ // pattern (/city/:id/_loading), once for every href it applies to.
144
+ const loadingQuery = (pattern: string) => ({
145
+ queryKey: [pattern],
146
+ staleTime: segmentStaleTime,
147
+ queryFn: async () => ({
148
+ value: (await $fetchAnswer(
149
+ $origin + pattern,
150
+ $modules,
151
+ false,
152
+ )) as ReactNode,
153
+ }),
154
+ });
155
+
156
+ // The app's layouts and loading states, matched by Hono's own router.
157
+ const layoutRouter = new $PatternRouter<string>();
158
+ for (const pattern of $routes.layouts) {
159
+ layoutRouter.add("GET", pattern, pattern);
160
+ }
161
+ const loadingRouter = new $PatternRouter<string>();
162
+ for (const pattern of $routes.loadings) {
163
+ loadingRouter.add("GET", pattern, pattern);
164
+ }
165
+ // An href's segments, as Next.js's folders: each path prefix, with its
166
+ // layout's path and its loading state's pattern, where the app has them.
167
+ // The root's layout is the app's RootLayout.
168
+ const segmentsOf = (href: string) => {
169
+ const searchStart = href.indexOf("?");
170
+ const path = searchStart === -1 ? href : href.slice(0, searchStart);
171
+ const names = path.split("/").filter(Boolean);
172
+ const bases = [
173
+ "",
174
+ ...names.map((_, i) => "/" + names.slice(0, i + 1).join("/")),
175
+ ];
176
+ return bases.map((base) => {
177
+ const layout = base + "/_layout";
178
+ return {
179
+ layout:
180
+ base !== "" && layoutRouter.match("GET", layout)[0].length > 0
181
+ ? layout
182
+ : undefined,
183
+ loading: loadingRouter.match("GET", base + "/_loading")[0][0]?.[0],
184
+ };
185
+ });
186
+ };
187
+ // What an href's screen needs before it can show, as Next.js prefetches
188
+ // and holds: its layouts down to the first loading state, and that
189
+ // loading state; or, with none, all its layouts and its page.
190
+ const shellOf = (href: string) => {
191
+ const queries: Query[] = [];
192
+ for (const segment of segmentsOf(href)) {
193
+ if (segment.layout !== undefined) {
194
+ queries.push(layoutQuery(segment.layout));
195
+ }
196
+ if (segment.loading !== undefined) {
197
+ queries.push(loadingQuery(segment.loading));
198
+ return { queries, loads: true };
199
+ }
200
+ }
201
+ queries.push(pageQuery(href));
202
+ return { queries, loads: false };
203
+ };
204
+ // A stale answer no screen shows, in before a navigation started, is
205
+ // dropped as the navigation reads it, so the navigation waits for a fresh
206
+ // one, as Next.js's does; one a screen still shows, going back to it, is
207
+ // kept.
208
+ const dropStaleAnswer = (query: Query, startedAt: number) => {
209
+ const cached = queryClient
210
+ .getQueryCache()
211
+ .find({ queryKey: query.queryKey, exact: true });
212
+ if (
213
+ cached !== undefined &&
214
+ cached.getObserversCount() === 0 &&
215
+ cached.state.data !== undefined &&
216
+ cached.state.dataUpdatedAt < startedAt &&
217
+ cached.isStaleByTime(query.staleTime ?? pageStaleTime(cached))
218
+ ) {
219
+ queryClient.removeQueries({ queryKey: query.queryKey, exact: true });
220
+ }
221
+ };
222
+
223
+ const Page = (props: { href: string }) =>
224
+ $useSuspenseQuery(pageQuery(props.href)).data as ReactNode;
225
+ const Loading = (props: { pattern: string }) =>
226
+ $useSuspenseQuery(loadingQuery(props.pattern)).data.value;
227
+ // A layout answers a client component, given what's under it; any other
228
+ // answer (its not-found screen) is what the screen shows.
229
+ const Layout = (props: { path: string; children: ReactNode }) => {
230
+ const answer = $useSuspenseQuery(layoutQuery(props.path)).data;
231
+ if (typeof answer !== "function") {
232
+ return answer as ReactNode;
233
+ }
234
+ const Answer = answer as ComponentType<{ children: ReactNode }>;
235
+ return <Answer>{props.children}</Answer>;
236
+ };
237
+ // A screen as Next.js nests a page: each segment's layout, around a
238
+ // Suspense showing its loading state, around what's under it. The page
239
+ // is fetched meanwhile.
240
+ const Segments = (props: { href: string }) => {
241
+ $usePrefetchQuery(pageQuery(props.href));
242
+ let node: ReactNode = <Page href={props.href} />;
243
+ for (const segment of segmentsOf(props.href).reverse()) {
244
+ if (segment.loading !== undefined) {
245
+ node = (
246
+ <$Suspense fallback={<Loading pattern={segment.loading} />}>
247
+ {node}
248
+ </$Suspense>
249
+ );
250
+ }
251
+ if (segment.layout !== undefined) {
252
+ node = <Layout path={segment.layout}>{node}</Layout>;
253
+ }
254
+ }
255
+ return node;
256
+ };
257
+
258
+ // Fetches an href ahead, as Next.js's Link: what its screen needs to
259
+ // show, if that ends at a loading state, and its page too when asked
260
+ // (<Link prefetch>).
261
+ const prefetch = (href: string, page = false) => {
262
+ const shell = shellOf(href);
263
+ if (page) {
264
+ fetchedAhead.add(href);
265
+ }
266
+ const queries = page
267
+ ? [...shell.queries, pageQuery(href)]
268
+ : shell.loads
269
+ ? shell.queries
270
+ : [];
271
+ for (const query of queries) {
272
+ void queryClient.query(query).catch(() => {});
273
+ }
274
+ };
275
+ // Every loading state, as the app opens: one per pattern, for any href.
276
+ for (const pattern of $routes.loadings) {
277
+ void queryClient.query(loadingQuery(pattern)).catch(() => {});
278
+ }
279
+
280
+ // An href changed in place (router.setParams) shows once it's in: the
281
+ // screen stays meanwhile.
282
+ const Screen = (props: { href: string; active: boolean }) => {
283
+ const href = $useDeferredValue(props.href);
284
+ return props.active ? (
285
+ <$Suspense fallback={null}>
286
+ <Segments href={href} />
287
+ </$Suspense>
288
+ ) : null;
289
+ };
290
+
291
+ // Suspends until each query is in. One that is, however old, holds
292
+ // nothing: stale ones were dropped above.
293
+ const AwaitQueries = (props: { queries: Query[] }) => {
294
+ $useSuspenseQueries({
295
+ queries: props.queries.map((query) => ({
296
+ ...query,
297
+ refetchOnMount: false,
298
+ })),
299
+ });
300
+ return null;
301
+ };
302
+ // Suspends a navigation's render, above the Stack, until the href's
303
+ // screen can show (shellOf). As navigations are transitions, the screen on
304
+ // top stays meanwhile. The page is fetched alongside. One that fails ends
305
+ // the hold, for the screen to show why.
306
+ const NavigationHold = (props: { href: string }) => {
307
+ if (currentNavigation.href !== props.href) {
308
+ currentNavigation = { href: props.href, startedAt: Date.now() };
309
+ }
310
+ const shell = shellOf(props.href);
311
+ for (const query of [...shell.queries, pageQuery(props.href)]) {
312
+ dropStaleAnswer(query, currentNavigation.startedAt);
313
+ }
314
+ // Going back, the screen shows its page as it was: only a page no
315
+ // screen shows is fetched.
316
+ const shown =
317
+ (queryClient
318
+ .getQueryCache()
319
+ .find({ queryKey: [props.href], exact: true })
320
+ ?.getObserversCount() ?? 0) > 0;
321
+ if (!shown) {
322
+ void queryClient.query(pageQuery(props.href)).catch(() => {});
323
+ }
324
+ return <AwaitQueries queries={shell.queries} />;
325
+ };
326
+ class SilentErrorBoundary extends $Component<
327
+ { children: ReactNode },
328
+ { failed: boolean }
329
+ > {
330
+ state = { failed: false };
331
+ static getDerivedStateFromError() {
332
+ return { failed: true };
333
+ }
334
+ render() {
335
+ return this.state.failed ? null : this.props.children;
336
+ }
337
+ }
338
+
339
+ // The root layout, /_layout, which draws the navigator: fetched once per
340
+ // open, as a new one would remount the navigator, and its screens with it.
341
+ const RootLayout = () => {
342
+ const answer = $useSuspenseQuery({
343
+ queryKey: ["/_layout"],
344
+ staleTime: Infinity,
345
+ }).data;
346
+ if (typeof answer !== "function") {
347
+ throw new Error(
348
+ "The app's /_layout answers no component: it's the root layout, a client component drawing the navigator (a Stack).",
349
+ );
350
+ }
351
+ const Answer = answer as ComponentType;
352
+ return <Answer />;
353
+ };
354
+
355
+ const Navigator = (props: { href: string }) => (
356
+ <$QueryClientProvider client={queryClient}>
357
+ <SilentErrorBoundary key={props.href}>
358
+ <NavigationHold href={props.href} />
359
+ </SilentErrorBoundary>
360
+ <RootLayout />
361
+ </$QueryClientProvider>
362
+ );
363
+
364
+ const runtime: Runtime = {
365
+ Navigator,
366
+ Screen,
367
+ prefetch,
368
+ // Its page and layouts again: one that failed, for its screen to wait
369
+ // for it; the others kept until the new answer is in.
370
+ reload: async (href) => {
371
+ const paths = [
372
+ href,
373
+ ...segmentsOf(href).flatMap((segment) =>
374
+ segment.layout === undefined ? [] : [segment.layout],
375
+ ),
376
+ ];
377
+ await Promise.allSettled(
378
+ paths.map((path) =>
379
+ queryClient.getQueryState([path])?.status === "error"
380
+ ? queryClient.resetQueries({ queryKey: [path], exact: true })
381
+ : queryClient.refetchQueries({ queryKey: [path], exact: true }),
382
+ ),
383
+ );
384
+ },
385
+ };
386
+ return runtime;
387
+ }`;
388
+ }