create-backtick-app 0.1.7 → 0.1.9

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
@@ -49,7 +49,12 @@ 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). The app and the server share a `package.json`, so the server bundles for the React and React Native versions installed there.
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.
53
+
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
+
56
+ - `npm run register-build` records this code's builds in `builds/`. Run it before a build for the stores, and commit what it writes.
57
+ - On EAS, `eas-build-post-install` fails a preview or production build that isn't registered.
53
58
 
54
59
  ```sh
55
60
  npm start # then scan the QR code with your phone, in Expo Go
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-backtick-app",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
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,15 @@
11
11
  "format:check": "prettier --check --ignore-unknown ."
12
12
  },
13
13
  "dependencies": {
14
- "@backtickjs/bundler": "^0.1.7",
15
- "@backtickjs/core": "^0.1.7",
16
- "@backtickjs/node-plugin": "^0.1.7",
14
+ "@backtickjs/bundler": "^0.1.9",
15
+ "@backtickjs/core": "^0.1.9",
16
+ "@backtickjs/node-plugin": "^0.1.9",
17
17
  "@backtickjs/react": "^19.2.3",
18
18
  "typescript": "^6.0.3"
19
19
  },
20
20
  "devDependencies": {
21
- "@backtickjs/prettier-plugin": "^0.1.7",
22
- "@backtickjs/tsc": "^0.1.7",
21
+ "@backtickjs/prettier-plugin": "^0.1.9",
22
+ "@backtickjs/tsc": "^0.1.9",
23
23
  "@types/node": "^25.9.1",
24
24
  "prettier": "^3.6.2"
25
25
  },
@@ -4,7 +4,11 @@ import { createRoot } from "@backtickjs/react/client";
4
4
  import type { JSX } from "@backtickjs/react/jsx-runtime";
5
5
 
6
6
  // The versions of React the page loads, below: what each page is bundled for.
7
- const packageVersions = { react: "19.2.3", "react-dom": "19.2.3" };
7
+ const modules = {
8
+ react: "19.2.3",
9
+ "react/jsx-runtime": "19.2.3",
10
+ "react-dom/client": "19.2.3",
11
+ };
8
12
 
9
13
  // Development is only what `npm start` runs, which sets NODE_ENV: any other
10
14
  // run, a deploy included, is production.
@@ -28,7 +32,7 @@ const reload = `
28
32
  export async function toHtml(element: JSX.Element): Promise<string> {
29
33
  const bundle = await bundler.build({
30
34
  input: cs`$createRoot(document.getElementById("app")!).render($element)`,
31
- packageVersions,
35
+ modules,
32
36
  });
33
37
  // In development, a map back to the files you wrote, for the browser's
34
38
  // devtools.
@@ -1,7 +1,6 @@
1
- import { evaluate } from "@backtickjs/react-native-client";
1
+ import { buildIdFrom, evaluate } from "@backtickjs/react-native-client";
2
2
  import Constants from "expo-constants";
3
3
  import { StatusBar } from "expo-status-bar";
4
- import * as React from "react";
5
4
  import {
6
5
  Component,
7
6
  type ReactNode,
@@ -10,29 +9,32 @@ import {
10
9
  useEffect,
11
10
  useState,
12
11
  } from "react";
13
- import * as JSXRuntime from "react/jsx-runtime";
14
- import * as ReactNative from "react-native";
15
- import { ActivityIndicator, Text } from "react-native";
16
-
17
- // What a screen may require: the packages this app was built with.
18
- const modules = {
19
- react: React,
20
- "react/jsx-runtime": JSXRuntime,
21
- "react-native": ReactNative,
22
- };
12
+ import { ActivityIndicator, Platform, Text } from "react-native";
13
+ import { moduleMap } from "./moduleMap";
23
14
 
24
15
  // Your Backtick server runs on the computer Expo was started from.
25
16
  const host = Constants.expoConfig?.hostUri?.split(":")[0] ?? "localhost";
26
17
  const server = `http://${host}:3000`;
27
18
 
19
+ // This build's id, which names the modules it carries, for the server to
20
+ // bundle screens it can run: see app.config.ts.
21
+ const buildId = buildIdFrom(Constants.expoConfig?.extra, Platform.OS);
22
+
28
23
  // The screen at `path` on your server: its bundle, fetched and run with the
29
- // app's packages. Your fetch to shape: headers, auth, caching.
24
+ // app's modules. Your fetch to shape: headers, auth, caching.
30
25
  async function fetchScreen(path: string): Promise<ReactNode> {
31
- const response = await fetch(`${server}${path}`);
26
+ const response = await fetch(`${server}${path}`, {
27
+ headers: { "x-build-id": buildId },
28
+ });
29
+ if (response.status === 426) {
30
+ throw new Error(
31
+ "This version of the app is no longer supported: update it.",
32
+ );
33
+ }
32
34
  if (!response.ok) {
33
35
  throw new Error(`${path} answered ${response.status}.`);
34
36
  }
35
- return evaluate(await response.text(), modules) as ReactNode;
37
+ return evaluate(await response.text(), moduleMap) as ReactNode;
36
38
  }
37
39
 
38
40
  export default function App() {
@@ -0,0 +1,10 @@
1
+ import { buildIds } from "@backtickjs/builds";
2
+ import type { ConfigContext, ExpoConfig } from "expo/config";
3
+
4
+ // app.json, with this build's id on each platform: it names the modules the
5
+ // build carries, which the app sends and the server bundles screens for.
6
+ export default ({ config, projectRoot }: ConfigContext): ExpoConfig =>
7
+ ({
8
+ ...config,
9
+ extra: { ...config.extra, buildIds: buildIds(projectRoot) },
10
+ }) as ExpoConfig;
@@ -0,0 +1,12 @@
1
+ import type { ModuleMap } from "@backtickjs/react-native-client";
2
+
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
5
+ // nothing else: the server reads it, without running it, to know what each
6
+ // build of your app carries. A platform that differs gets its own file, as
7
+ // Metro picks files: moduleMap.web.ts, moduleMap.ios.ts.
8
+ export const moduleMap = {
9
+ react: () => require("react"),
10
+ "react/jsx-runtime": () => require("react/jsx-runtime"),
11
+ "react-native": () => require("react-native"),
12
+ } satisfies ModuleMap;
@@ -11,16 +11,19 @@
11
11
  "typecheck": "tsc --noEmit && backtick-tsc -p server --noEmit",
12
12
  "test": "node --import ./server/test/setup.mjs --import @backtickjs/node-plugin --test \"server/**/*.test.tsx\"",
13
13
  "reset-project": "node scripts/reset-project.mjs",
14
+ "register-build": "backtick-builds register",
15
+ "eas-build-post-install": "backtick-builds check",
14
16
  "format": "prettier --write --ignore-unknown .",
15
17
  "format:check": "prettier --check --ignore-unknown ."
16
18
  },
17
19
  "dependencies": {
18
- "@backtickjs/bundler": "^0.1.7",
19
- "@backtickjs/core": "^0.1.7",
20
- "@backtickjs/node-plugin": "^0.1.7",
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",
21
24
  "@backtickjs/react": "^19.2.3",
22
25
  "@backtickjs/react-native": "^0.86.3",
23
- "@backtickjs/react-native-client": "^0.1.7",
26
+ "@backtickjs/react-native-client": "^0.1.9",
24
27
  "expo": "~57.0.26",
25
28
  "expo-constants": "~57.0.20",
26
29
  "expo-status-bar": "~57.0.1",
@@ -31,8 +34,8 @@
31
34
  "typescript": "^6.0.3"
32
35
  },
33
36
  "devDependencies": {
34
- "@backtickjs/prettier-plugin": "^0.1.7",
35
- "@backtickjs/tsc": "^0.1.7",
37
+ "@backtickjs/prettier-plugin": "^0.1.9",
38
+ "@backtickjs/tsc": "^0.1.9",
36
39
  "@testing-library/react": "^16.3.3",
37
40
  "@testing-library/user-event": "^14.6.7",
38
41
  "@types/node": "^25.9.1",
@@ -1,5 +1,6 @@
1
1
  import { createServer } from "node:http";
2
- import { createRequire } from "node:module";
2
+ import { fileURLToPath } from "node:url";
3
+ import { createBuilds, DeprecatedBuild } from "@backtickjs/builds";
3
4
  import { bundler } from "@backtickjs/bundler";
4
5
  import { Home } from "./Home.js";
5
6
 
@@ -10,30 +11,40 @@ import { Home } from "./Home.js";
10
11
  const development = process.env.NODE_ENV === "development";
11
12
  const run = crypto.randomUUID();
12
13
 
13
- // The versions of React and React Native the app is built with: this
14
- // project's own, as the app and this server share a package.json. A server
15
- // for apps of many versions would tell them apart, by their user agent for
16
- // one, and bundle each for its own.
17
- const require = createRequire(import.meta.url);
18
- const packageVersions = {
19
- react: require("react/package.json").version,
20
- "react-native": require("react-native/package.json").version,
21
- };
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
+ });
22
20
 
23
21
  const server = createServer(async (request, response) => {
24
22
  // `npm run web` serves the app from another origin.
25
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
+ }
26
29
  if (development && request.url === "/live") {
27
30
  response.end(run);
28
31
  return;
29
32
  }
30
33
  if (request.url === "/home") {
31
34
  try {
32
- const bundle = await bundler.build({ input: <Home />, packageVersions });
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 });
33
38
  const { code } = bundle.generate({ format: "cjs" });
34
39
  response.setHeader("content-type", "text/javascript");
35
40
  response.end(code);
36
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
+ }
37
48
  // Answered rather than crashed on, so the app can show why.
38
49
  console.error(error);
39
50
  response.statusCode = 500;
@@ -1,4 +1,5 @@
1
- import { createRequire } from "node:module";
1
+ import { fileURLToPath } from "node:url";
2
+ import { createBuilds } from "@backtickjs/builds";
2
3
  import { bundler } from "@backtickjs/bundler";
3
4
  import type { Spliceable } from "@backtickjs/core";
4
5
  import { evaluate } from "@backtickjs/react-native-client";
@@ -6,24 +7,22 @@ import * as React from "react";
6
7
  import * as JSXRuntime from "react/jsx-runtime";
7
8
  import * as ReactNativeWeb from "react-native-web";
8
9
 
9
- // What the app provides, as `server/index.tsx` and `App.tsx` say, with React
10
- // Native's web build standing in for React Native. Add a package here when
11
- // your app provides one.
12
- const require = createRequire(import.meta.url);
13
- const packageVersions = {
14
- react: require("react/package.json").version,
15
- "react-native": require("react-native/package.json").version,
16
- };
17
- const modules = {
18
- react: React,
19
- "react/jsx-runtime": JSXRuntime,
20
- "react-native": ReactNativeWeb,
10
+ // What the app's web build provides, as moduleMap.ts says, with React
11
+ // Native's web build standing in for React Native. Add a stand-in here when
12
+ // your module map offers another package.
13
+ const modules = createBuilds({
14
+ projectRoot: fileURLToPath(new URL("../..", import.meta.url)),
15
+ }).installed("web");
16
+ const moduleMap = {
17
+ react: () => React,
18
+ "react/jsx-runtime": () => JSXRuntime,
19
+ "react-native": () => ReactNativeWeb,
21
20
  };
22
21
 
23
22
  // A screen, bundled as your server bundles it and run as your app runs it,
24
23
  // for Testing Library to draw.
25
24
  export async function drawScreen(input: Spliceable): Promise<React.ReactNode> {
26
- const bundle = await bundler.build({ input, packageVersions });
25
+ const bundle = await bundler.build({ input, modules });
27
26
  const { code } = bundle.generate({ format: "cjs" });
28
- return evaluate(code, modules) as React.ReactNode;
27
+ return evaluate(code, moduleMap) as React.ReactNode;
29
28
  }
@@ -13,5 +13,6 @@
13
13
  "jsx": "react-jsx",
14
14
  "jsxImportSource": "@backtickjs/react"
15
15
  },
16
- "include": ["."]
16
+ // with app.config.ts, which runs in Node as the server does
17
+ "include": [".", "../app.config.ts"]
17
18
  }
@@ -3,5 +3,5 @@
3
3
  "compilerOptions": {
4
4
  "strict": true
5
5
  },
6
- "exclude": ["server"]
6
+ "exclude": ["server", "app.config.ts"]
7
7
  }
@@ -11,15 +11,15 @@
11
11
  "format:check": "prettier --check --ignore-unknown ."
12
12
  },
13
13
  "dependencies": {
14
- "@backtickjs/bundler": "^0.1.7",
15
- "@backtickjs/core": "^0.1.7",
16
- "@backtickjs/node-plugin": "^0.1.7",
14
+ "@backtickjs/bundler": "^0.1.9",
15
+ "@backtickjs/core": "^0.1.9",
16
+ "@backtickjs/node-plugin": "^0.1.9",
17
17
  "@backtickjs/solid-js": "^1.9.14",
18
18
  "typescript": "^6.0.3"
19
19
  },
20
20
  "devDependencies": {
21
- "@backtickjs/prettier-plugin": "^0.1.7",
22
- "@backtickjs/tsc": "^0.1.7",
21
+ "@backtickjs/prettier-plugin": "^0.1.9",
22
+ "@backtickjs/tsc": "^0.1.9",
23
23
  "@types/node": "^25.9.1",
24
24
  "prettier": "^3.6.2"
25
25
  },
@@ -4,7 +4,11 @@ import { render } from "@backtickjs/solid-js/web";
4
4
  import type { JSX } from "@backtickjs/solid-js/jsx-runtime";
5
5
 
6
6
  // The version of Solid the page loads, below: what each page is bundled for.
7
- const packageVersions = { "solid-js": "1.9.14" };
7
+ const modules = {
8
+ "solid-js": "1.9.14",
9
+ "solid-js/store": "1.9.14",
10
+ "solid-js/web": "1.9.14",
11
+ };
8
12
 
9
13
  // Development is only what `npm start` runs, which sets NODE_ENV: any other
10
14
  // run, a deploy included, is production.
@@ -28,7 +32,7 @@ const reload = `
28
32
  export async function toHtml(element: JSX.Element): Promise<string> {
29
33
  const bundle = await bundler.build({
30
34
  input: cs`$render(() => $element, document.getElementById("app")!)`,
31
- packageVersions,
35
+ modules,
32
36
  });
33
37
  // In development, a map back to the files you wrote, for the browser's
34
38
  // devtools.