create-backtick-app 0.1.9 → 0.1.10

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
@@ -41,7 +41,7 @@ Every template has:
41
41
 
42
42
  - `server/Home.tsx`: the welcome screen, a server component. It reads values on the server and splices them into a client script.
43
43
  - `server/HelloWave.tsx`: a client component, a waving hand animated where the screen runs. Tap or click it.
44
- - `server/index.tsx`: the Backtick server on http://localhost:3000.
44
+ - `server/index.tsx`: the Backtick server on http://localhost:3000, a [Hono](https://hono.dev) app that runs on Node or Bun. Add a route there for each screen or page; a path can take parameters, as `/stories/:id`.
45
45
  - `scripts/start.mjs`: starts the server in development, restarting it whenever you save. The client polls `/live`, which exists only in development, and redraws when the server restarts.
46
46
  - `npm run reset-project`: replaces `server/Home.tsx` with a blank screen and removes `HelloWave.tsx`.
47
47
  - `npm run typecheck`: runs `backtick-tsc` over the server, scripts and splices included. In `react-native`, it also runs `tsc` over the Expo app.
@@ -49,7 +49,7 @@ Every template has:
49
49
 
50
50
  ### `react-native`
51
51
 
52
- An Expo app (SDK 57) with the Backtick server beside it in `server/`. `App.tsx` fetches the screen at `/home` and runs it with [`@backtickjs/react-native-client`](https://github.com/backtickjs/backtick/tree/main/packages/react-native-client), offering screens what `moduleMap.ts` lists.
52
+ An Expo app (SDK 57) with the Backtick server beside it in `server/`. `App.tsx` fetches the screen at `/` and runs it with [`@backtickjs/react-native-client`](https://github.com/backtickjs/backtick/tree/main/packages/react-native-client), offering screens what `moduleMap.ts` lists.
53
53
 
54
54
  Each build of the app sends an id that names the modules it carries, which `app.config.ts` works out from `moduleMap.ts`. The server bundles each screen for that build's modules, with [`@backtickjs/builds`](https://github.com/backtickjs/backtick/tree/main/packages/builds), so a build in users' hands keeps working as the app moves on:
55
55
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-backtick-app",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Creates a Backtick app: React Native, React or Solid, with screens from your server on Node or Bun",
5
5
  "keywords": [
6
6
  "backtick",
@@ -11,15 +11,17 @@
11
11
  "format:check": "prettier --check --ignore-unknown ."
12
12
  },
13
13
  "dependencies": {
14
- "@backtickjs/bundler": "^0.1.9",
15
- "@backtickjs/core": "^0.1.9",
16
- "@backtickjs/node-plugin": "^0.1.9",
14
+ "@backtickjs/bundler": "^0.1.10",
15
+ "@backtickjs/core": "^0.1.10",
16
+ "@backtickjs/node-plugin": "^0.1.10",
17
17
  "@backtickjs/react": "^19.2.3",
18
+ "@hono/node-server": "^2.1.4",
19
+ "hono": "^4.13.13",
18
20
  "typescript": "^6.0.3"
19
21
  },
20
22
  "devDependencies": {
21
- "@backtickjs/prettier-plugin": "^0.1.9",
22
- "@backtickjs/tsc": "^0.1.9",
23
+ "@backtickjs/prettier-plugin": "^0.1.10",
24
+ "@backtickjs/tsc": "^0.1.10",
23
25
  "@types/node": "^25.9.1",
24
26
  "prettier": "^3.6.2"
25
27
  },
@@ -18,3 +18,9 @@ const server = spawn(runtime, ["--watch", ...loader, "server/index.tsx"], {
18
18
  });
19
19
 
20
20
  server.on("exit", (code) => process.exit(code ?? 0));
21
+
22
+ // Stopped as a whole: a signal to this script alone, as a task runner sends
23
+ // rather than Ctrl-C to the terminal, stops the server with it.
24
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
25
+ process.on(signal, () => server.kill(signal));
26
+ }
@@ -1,33 +1,27 @@
1
- import { createServer } from "node:http";
1
+ import { serve } from "@hono/node-server";
2
+ import { Hono } from "hono";
2
3
  import { Home } from "./Home.js";
3
4
  import { development, toHtml } from "./html.js";
4
5
 
6
+ const app = new Hono();
7
+
8
+ // Your pages: add a route, and the browser can open it. A path can take
9
+ // parameters, `/stories/:id`, read with `context.req.param("id")`.
10
+ app.get("/", async (context) => context.html(await toHtml(<Home />)));
11
+
5
12
  // In development, this run of the server: `--watch` starts a new one whenever
6
13
  // you save, and the page, seeing a new one at `/live`, reloads.
7
- const run = crypto.randomUUID();
14
+ if (development) {
15
+ const run = crypto.randomUUID();
16
+ app.get("/live", (context) => context.text(run));
17
+ }
8
18
 
9
- const server = createServer(async (request, response) => {
10
- if (development && request.url === "/live") {
11
- response.end(run);
12
- return;
13
- }
14
- if (request.url === "/") {
15
- try {
16
- const html = await toHtml(<Home />);
17
- response.setHeader("content-type", "text/html");
18
- response.end(html);
19
- } catch (error) {
20
- // Answered rather than crashed on, so the browser can show why.
21
- console.error(error);
22
- response.statusCode = 500;
23
- response.end(String(error));
24
- }
25
- return;
26
- }
27
- response.statusCode = 404;
28
- response.end();
19
+ // Answered rather than crashed on, so the browser can show why.
20
+ app.onError((error, context) => {
21
+ console.error(error);
22
+ return context.text(String(error), 500);
29
23
  });
30
24
 
31
- server.listen(3000, () => {
25
+ serve({ fetch: app.fetch, port: 3000 }, () => {
32
26
  console.log("Backtick server on http://localhost:3000");
33
27
  });
@@ -26,11 +26,6 @@ async function fetchScreen(path: string): Promise<ReactNode> {
26
26
  const response = await fetch(`${server}${path}`, {
27
27
  headers: { "x-build-id": buildId },
28
28
  });
29
- if (response.status === 426) {
30
- throw new Error(
31
- "This version of the app is no longer supported: update it.",
32
- );
33
- }
34
29
  if (!response.ok) {
35
30
  throw new Error(`${path} answered ${response.status}.`);
36
31
  }
@@ -43,7 +38,7 @@ export default function App() {
43
38
  // error the last one showed.
44
39
  const [request, setRequest] = useState(() => ({
45
40
  number: 0,
46
- screen: fetchScreen("/home"),
41
+ screen: fetchScreen("/"),
47
42
  }));
48
43
  useEffect(() => {
49
44
  // In development, the screen is fetched again each time the server
@@ -52,7 +47,7 @@ export default function App() {
52
47
  return onServerRestart(() =>
53
48
  setRequest(({ number }) => ({
54
49
  number: number + 1,
55
- screen: fetchScreen("/home"),
50
+ screen: fetchScreen("/"),
56
51
  })),
57
52
  );
58
53
  }
@@ -1,12 +1,15 @@
1
1
  import type { ModuleMap } from "@backtickjs/react-native-client";
2
+ import * as React from "react";
3
+ import * as JSXRuntime from "react/jsx-runtime";
4
+ import * as ReactNative from "react-native";
2
5
 
3
- // What a screen may require, and where each comes from: the packages this
4
- // app is built with. Add one when a screen uses another package. A table and
6
+ // What a screen may require, as your app imports it: the packages it's built
7
+ // with. Add one when a screen uses another package. Imports and a table,
5
8
  // nothing else: the server reads it, without running it, to know what each
6
9
  // build of your app carries. A platform that differs gets its own file, as
7
10
  // Metro picks files: moduleMap.web.ts, moduleMap.ios.ts.
8
11
  export const moduleMap = {
9
- react: () => require("react"),
10
- "react/jsx-runtime": () => require("react/jsx-runtime"),
11
- "react-native": () => require("react-native"),
12
+ react: React,
13
+ "react/jsx-runtime": JSXRuntime,
14
+ "react-native": ReactNative,
12
15
  } satisfies ModuleMap;
@@ -17,16 +17,18 @@
17
17
  "format:check": "prettier --check --ignore-unknown ."
18
18
  },
19
19
  "dependencies": {
20
- "@backtickjs/builds": "^0.1.9",
21
- "@backtickjs/bundler": "^0.1.9",
22
- "@backtickjs/core": "^0.1.9",
23
- "@backtickjs/node-plugin": "^0.1.9",
20
+ "@backtickjs/builds": "^0.1.10",
21
+ "@backtickjs/bundler": "^0.1.10",
22
+ "@backtickjs/core": "^0.1.10",
23
+ "@backtickjs/node-plugin": "^0.1.10",
24
24
  "@backtickjs/react": "^19.2.3",
25
25
  "@backtickjs/react-native": "^0.86.3",
26
- "@backtickjs/react-native-client": "^0.1.9",
26
+ "@backtickjs/react-native-client": "^0.1.10",
27
+ "@hono/node-server": "^2.1.4",
27
28
  "expo": "~57.0.26",
28
29
  "expo-constants": "~57.0.20",
29
30
  "expo-status-bar": "~57.0.1",
31
+ "hono": "^4.13.13",
30
32
  "react": "19.2.3",
31
33
  "react-dom": "19.2.3",
32
34
  "react-native": "0.86.3",
@@ -34,8 +36,8 @@
34
36
  "typescript": "^6.0.3"
35
37
  },
36
38
  "devDependencies": {
37
- "@backtickjs/prettier-plugin": "^0.1.9",
38
- "@backtickjs/tsc": "^0.1.9",
39
+ "@backtickjs/prettier-plugin": "^0.1.10",
40
+ "@backtickjs/tsc": "^0.1.10",
39
41
  "@testing-library/react": "^16.3.3",
40
42
  "@testing-library/user-event": "^14.6.7",
41
43
  "@types/node": "^25.9.1",
@@ -32,6 +32,15 @@ expo.on("exit", (code) => {
32
32
  process.exit(code ?? 0);
33
33
  });
34
34
 
35
+ // Stopped as a whole: a signal to this script alone, as a task runner sends
36
+ // rather than Ctrl-C to the terminal, stops the server and Expo with it.
37
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
38
+ process.on(signal, () => {
39
+ server.kill(signal);
40
+ expo.kill(signal);
41
+ });
42
+ }
43
+
35
44
  // Expo shows how to open the app, its QR code, only in an interactive
36
45
  // terminal. Run elsewhere, as an agent runs it, this prints the address once
37
46
  // Metro is up.
@@ -0,0 +1,15 @@
1
+ import { cs } from "@backtickjs/core";
2
+ import { Text, View } from "@backtickjs/react-native";
3
+
4
+ // Every path, for a build you retired: drawn with React Native alone, which
5
+ // every build has, and bundled for the retired build's own modules. Point
6
+ // your users at your store listing here.
7
+ export function AppUpdateRequired() {
8
+ return cs`(
9
+ <$View style={{ flex: 1, justifyContent: "center", padding: 24 }}>
10
+ <$Text style={{ fontSize: 16, textAlign: "center" }}>
11
+ This version of the app is no longer supported. Update it to keep going.
12
+ </$Text>
13
+ </$View>
14
+ )`;
15
+ }
@@ -1,61 +1,33 @@
1
- import { createServer } from "node:http";
2
- import { fileURLToPath } from "node:url";
3
- import { createBuilds, DeprecatedBuild } from "@backtickjs/builds";
4
- import { bundler } from "@backtickjs/bundler";
1
+ import { serve } from "@hono/node-server";
2
+ import { Hono } from "hono";
3
+ import { cors } from "hono/cors";
5
4
  import { Home } from "./Home.js";
5
+ import { screen } from "./screen.js";
6
+
7
+ const app = new Hono();
8
+
9
+ // `npm run web` serves the app from another origin.
10
+ app.use(cors({ origin: "*", allowHeaders: ["x-build-id"] }));
11
+
12
+ // Your app's screens: add a route, and the app can fetch it. A path can
13
+ // take parameters, `/stories/:id`, read with `context.req.param("id")`.
14
+ app.get("/", (context) => screen(context, <Home />));
6
15
 
7
16
  // Development is only what `npm start` runs, which sets NODE_ENV: any other
8
17
  // run, a deploy included, is production. In development, this run of the
9
18
  // server: `--watch` starts a new one whenever you save, and the app, seeing a
10
19
  // new one at `/live`, draws its screen again.
11
- const development = process.env.NODE_ENV === "development";
12
- const run = crypto.randomUUID();
13
-
14
- // Your app's builds: each sends its id, and a screen is bundled for the
15
- // modules that build carries, from moduleMap.ts. A build in users' hands
16
- // keeps working as your app moves on, once registered: see builds/.
17
- const builds = createBuilds({
18
- projectRoot: fileURLToPath(new URL("..", import.meta.url)),
19
- });
20
+ if (process.env.NODE_ENV === "development") {
21
+ const run = crypto.randomUUID();
22
+ app.get("/live", (context) => context.text(run));
23
+ }
20
24
 
21
- const server = createServer(async (request, response) => {
22
- // `npm run web` serves the app from another origin.
23
- response.setHeader("access-control-allow-origin", "*");
24
- response.setHeader("access-control-allow-headers", "x-build-id");
25
- if (request.method === "OPTIONS") {
26
- response.end();
27
- return;
28
- }
29
- if (development && request.url === "/live") {
30
- response.end(run);
31
- return;
32
- }
33
- if (request.url === "/home") {
34
- try {
35
- const buildId = request.headers["x-build-id"]?.toString() || null;
36
- const modules = builds.modulesOf(buildId);
37
- const bundle = await bundler.build({ input: <Home />, modules });
38
- const { code } = bundle.generate({ format: "cjs" });
39
- response.setHeader("content-type", "text/javascript");
40
- response.end(code);
41
- } catch (error) {
42
- // A build you retired: the app asks its user to update.
43
- if (error instanceof DeprecatedBuild) {
44
- response.statusCode = 426;
45
- response.end(String(error));
46
- return;
47
- }
48
- // Answered rather than crashed on, so the app can show why.
49
- console.error(error);
50
- response.statusCode = 500;
51
- response.end(String(error));
52
- }
53
- return;
54
- }
55
- response.statusCode = 404;
56
- response.end();
25
+ // Answered rather than crashed on, so the app can show why.
26
+ app.onError((error, context) => {
27
+ console.error(error);
28
+ return context.text(String(error), 500);
57
29
  });
58
30
 
59
- server.listen(3000, () => {
31
+ serve({ fetch: app.fetch, port: 3000 }, () => {
60
32
  console.log("Backtick server on http://localhost:3000");
61
33
  });
@@ -0,0 +1,33 @@
1
+ import { fileURLToPath } from "node:url";
2
+ import { createBuilds, DeprecatedBuild } from "@backtickjs/builds";
3
+ import { bundler } from "@backtickjs/bundler";
4
+ import type { JSX } from "@backtickjs/react/jsx-runtime";
5
+ import type { Context } from "hono";
6
+ import { AppUpdateRequired } from "./AppUpdateRequired.js";
7
+
8
+ // Your app's builds: each sends its id, and a screen is bundled for the
9
+ // modules that build carries, from moduleMap.ts. A build in users' hands
10
+ // keeps working as your app moves on, once registered: see builds/.
11
+ const builds = createBuilds({
12
+ projectRoot: fileURLToPath(new URL("..", import.meta.url)),
13
+ });
14
+
15
+ /**
16
+ * `element` as the response to a request from your app: bundled for the
17
+ * modules the requesting build carries, or, for a build you retired, a
18
+ * screen asking to update, bundled for what it has.
19
+ */
20
+ export async function screen(context: Context, element: JSX.Element) {
21
+ let input = element;
22
+ let modules;
23
+ try {
24
+ modules = builds.modulesOf(context.req.header("x-build-id") || null);
25
+ } catch (error) {
26
+ if (!(error instanceof DeprecatedBuild)) throw error;
27
+ input = <AppUpdateRequired />;
28
+ modules = error.modules;
29
+ }
30
+ const bundle = await bundler.build({ input, modules });
31
+ const { code } = bundle.generate({ format: "cjs" });
32
+ return context.body(code, 200, { "content-type": "text/javascript" });
33
+ }
@@ -14,9 +14,9 @@ const modules = createBuilds({
14
14
  projectRoot: fileURLToPath(new URL("../..", import.meta.url)),
15
15
  }).installed("web");
16
16
  const moduleMap = {
17
- react: () => React,
18
- "react/jsx-runtime": () => JSXRuntime,
19
- "react-native": () => ReactNativeWeb,
17
+ react: React,
18
+ "react/jsx-runtime": JSXRuntime,
19
+ "react-native": ReactNativeWeb,
20
20
  };
21
21
 
22
22
  // A screen, bundled as your server bundles it and run as your app runs it,
@@ -11,15 +11,17 @@
11
11
  "format:check": "prettier --check --ignore-unknown ."
12
12
  },
13
13
  "dependencies": {
14
- "@backtickjs/bundler": "^0.1.9",
15
- "@backtickjs/core": "^0.1.9",
16
- "@backtickjs/node-plugin": "^0.1.9",
14
+ "@backtickjs/bundler": "^0.1.10",
15
+ "@backtickjs/core": "^0.1.10",
16
+ "@backtickjs/node-plugin": "^0.1.10",
17
17
  "@backtickjs/solid-js": "^1.9.14",
18
+ "@hono/node-server": "^2.1.4",
19
+ "hono": "^4.13.13",
18
20
  "typescript": "^6.0.3"
19
21
  },
20
22
  "devDependencies": {
21
- "@backtickjs/prettier-plugin": "^0.1.9",
22
- "@backtickjs/tsc": "^0.1.9",
23
+ "@backtickjs/prettier-plugin": "^0.1.10",
24
+ "@backtickjs/tsc": "^0.1.10",
23
25
  "@types/node": "^25.9.1",
24
26
  "prettier": "^3.6.2"
25
27
  },
@@ -18,3 +18,9 @@ const server = spawn(runtime, ["--watch", ...loader, "server/index.tsx"], {
18
18
  });
19
19
 
20
20
  server.on("exit", (code) => process.exit(code ?? 0));
21
+
22
+ // Stopped as a whole: a signal to this script alone, as a task runner sends
23
+ // rather than Ctrl-C to the terminal, stops the server with it.
24
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
25
+ process.on(signal, () => server.kill(signal));
26
+ }
@@ -1,33 +1,27 @@
1
- import { createServer } from "node:http";
1
+ import { serve } from "@hono/node-server";
2
+ import { Hono } from "hono";
2
3
  import { Home } from "./Home.js";
3
4
  import { development, toHtml } from "./html.js";
4
5
 
6
+ const app = new Hono();
7
+
8
+ // Your pages: add a route, and the browser can open it. A path can take
9
+ // parameters, `/stories/:id`, read with `context.req.param("id")`.
10
+ app.get("/", async (context) => context.html(await toHtml(<Home />)));
11
+
5
12
  // In development, this run of the server: `--watch` starts a new one whenever
6
13
  // you save, and the page, seeing a new one at `/live`, reloads.
7
- const run = crypto.randomUUID();
14
+ if (development) {
15
+ const run = crypto.randomUUID();
16
+ app.get("/live", (context) => context.text(run));
17
+ }
8
18
 
9
- const server = createServer(async (request, response) => {
10
- if (development && request.url === "/live") {
11
- response.end(run);
12
- return;
13
- }
14
- if (request.url === "/") {
15
- try {
16
- const html = await toHtml(<Home />);
17
- response.setHeader("content-type", "text/html");
18
- response.end(html);
19
- } catch (error) {
20
- // Answered rather than crashed on, so the browser can show why.
21
- console.error(error);
22
- response.statusCode = 500;
23
- response.end(String(error));
24
- }
25
- return;
26
- }
27
- response.statusCode = 404;
28
- response.end();
19
+ // Answered rather than crashed on, so the browser can show why.
20
+ app.onError((error, context) => {
21
+ console.error(error);
22
+ return context.text(String(error), 500);
29
23
  });
30
24
 
31
- server.listen(3000, () => {
25
+ serve({ fetch: app.fetch, port: 3000 }, () => {
32
26
  console.log("Backtick server on http://localhost:3000");
33
27
  });