create-backtick-app 0.1.3 → 0.1.5

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
@@ -58,10 +58,13 @@ Every template has:
58
58
  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.
59
59
 
60
60
  ```sh
61
- npm run ios # or android, or web
61
+ npm start # then scan the QR code with your phone, in Expo Go
62
+ npm run ios # or android, or web: a simulator, an emulator or a browser
62
63
  ```
63
64
 
64
- Each starts your Backtick server alongside Expo, in one terminal. Edit `server/Home.tsx` and save: the app fetches the screen again, without rebuilding.
65
+ Each starts your Backtick server alongside Expo, in one terminal. Edit `server/Home.tsx` and save: the app fetches the screen again, without rebuilding. Where Expo can't show its QR code, as when an agent runs it, `npm start` prints the `exp://` address to open in Expo Go instead.
66
+
67
+ `npm test` draws `server/Home.tsx` as the app would, in `server/Home.test.tsx`; [Testing screens](https://backtickjs.com/docs/testing) explains the test.
65
68
 
66
69
  ### `react` and `solid-js`
67
70
 
@@ -78,7 +81,8 @@ Then open http://localhost:3000. Edit `server/Home.tsx` and save to see the page
78
81
  - [`@backtickjs/core`](https://github.com/backtickjs/backtick/tree/main/packages/core): the `cs` tag
79
82
  - [`@backtickjs/bundler`](https://github.com/backtickjs/backtick/tree/main/packages/bundler): runs server components and writes the bundle
80
83
  - [`@backtickjs/react-native-client`](https://github.com/backtickjs/backtick/tree/main/packages/react-native-client): runs a bundle in a React Native app
81
- - [Documentation](https://backtickjs.com/docs)
84
+ - [Quick start](https://backtickjs.com/docs)
85
+ - [Tutorial](https://backtickjs.com/docs/tutorial)
82
86
 
83
87
  ## License
84
88
 
package/dist/index.js CHANGED
@@ -5,24 +5,40 @@ import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { parseArgs } from "node:util";
7
7
  import prompts from "prompts";
8
+ import { isSupportedNode } from "./nodeVersion.js";
9
+ // The web templates' next steps.
10
+ const webNext = (start) => `To run your project:
11
+
12
+ ${start}
13
+
14
+ Then open http://localhost:3000. Edit server/Home.tsx and save to see the page change.`;
8
15
  const TEMPLATES = {
9
16
  "react-native": {
10
17
  title: "React Native",
11
18
  description: "An Expo app, its screens from your server",
12
- scripts: ["android", "ios", "web"],
13
- then: "Each starts your Backtick server alongside Expo. Edit server/Home.tsx and save to see the screen change.",
19
+ next: (start, run) => `To open it on your phone:
20
+
21
+ 1. Install Expo Go on your phone, from the App Store or Google Play.
22
+ 2. Connect your phone to the same Wi-Fi network as this computer.
23
+ 3. Start your project, which starts your Backtick server alongside Expo:
24
+
25
+ ${start}
26
+
27
+ 4. Scan the QR code it shows: with the Camera app on iPhone, or with Expo Go on Android.
28
+
29
+ Then edit server/Home.tsx and save to see the screen change on your phone.
30
+
31
+ No phone at hand? ${run("ios")}, ${run("android")} or ${run("web")} opens it in the iOS Simulator, an Android emulator or a browser instead.`,
14
32
  },
15
33
  react: {
16
34
  title: "React",
17
35
  description: "A web page, rendered by React",
18
- scripts: ["start"],
19
- then: "Then open http://localhost:3000. Edit server/Home.tsx and save to see the page change.",
36
+ next: webNext,
20
37
  },
21
38
  "solid-js": {
22
39
  title: "solid-js",
23
40
  description: "A web page, rendered by Solid",
24
- scripts: ["start"],
25
- then: "Then open http://localhost:3000. Edit server/Home.tsx and save to see the page change.",
41
+ next: webNext,
26
42
  },
27
43
  };
28
44
  // What runs your Backtick server, and the loader that compiles it for that
@@ -67,6 +83,13 @@ if (values.help) {
67
83
  console.log(USAGE);
68
84
  process.exit(0);
69
85
  }
86
+ // Said, not refused: what fails on another version is the app's toolchain,
87
+ // later, and only some of it.
88
+ if (!isSupportedNode(process.versions.node)) {
89
+ console.warn(`Backtick and React Native support Node 22 (22.15 or later) and 24 (24.3 or ` +
90
+ `later), and you're on Node ${process.versions.node}. If something fails, ` +
91
+ `switch to Node 22 or 24.\n`);
92
+ }
70
93
  if (values.template !== undefined && !(values.template in TEMPLATES)) {
71
94
  console.error(`There's no template named ${values.template}. Pick one of: ${Object.keys(TEMPLATES).join(", ")}.`);
72
95
  process.exit(1);
@@ -162,15 +185,16 @@ if (!values["no-install"]) {
162
185
  process.exit(status ?? 1);
163
186
  }
164
187
  }
188
+ // Created where it was run, there's nowhere to go first.
189
+ const here = target === process.cwd();
190
+ const script = (name) => `${run} run ${name}`;
191
+ const start = [...(here ? [] : [`cd ${slug}`]), script("start")]
192
+ .map((command) => ` ${command}`)
193
+ .join("\n");
165
194
  console.log(`
166
195
  ✅ Your project is ready!
167
196
 
168
- To run your project, navigate to the directory and run one of the following ${run} commands.
169
-
170
- - cd ${slug}
171
- ${template.scripts.map((script) => `- ${run} run ${script}`).join("\n")}
172
-
173
- ${template.then}
197
+ ${template.next(start, script)}
174
198
  `);
175
199
  function rewriteJson(file, change) {
176
200
  const json = JSON.parse(readFileSync(file, "utf8"));
@@ -0,0 +1 @@
1
+ export declare function isSupportedNode(version: string): boolean;
@@ -0,0 +1,7 @@
1
+ // Whether a Node version runs a Backtick project: what React Native 0.86 and
2
+ // Backtick's Node plugin both support, 22.15 and later in 22, 24.3 and later
3
+ // in 24, and 25 on. Node 23 runs neither reliably.
4
+ export function isSupportedNode(version) {
5
+ const [major = 0, minor = 0] = version.split(".").map(Number);
6
+ return ((major === 22 && minor >= 15) || (major === 24 && minor >= 3) || major >= 25);
7
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-backtick-app",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
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.3",
15
- "@backtickjs/core": "^0.1.3",
16
- "@backtickjs/node-plugin": "^0.1.3",
14
+ "@backtickjs/bundler": "^0.1.5",
15
+ "@backtickjs/core": "^0.1.5",
16
+ "@backtickjs/node-plugin": "^0.1.5",
17
17
  "@backtickjs/react": "^19.2.3",
18
18
  "typescript": "^6.0.3"
19
19
  },
20
20
  "devDependencies": {
21
- "@backtickjs/prettier-plugin": "^0.1.3",
22
- "@backtickjs/tsc": "^0.1.3",
21
+ "@backtickjs/prettier-plugin": "^0.1.5",
22
+ "@backtickjs/tsc": "^0.1.5",
23
23
  "@types/node": "^25.9.1",
24
24
  "prettier": "^3.6.2"
25
25
  },
@@ -9,17 +9,18 @@
9
9
  "ios": "node scripts/start.mjs --ios",
10
10
  "web": "node scripts/start.mjs --web",
11
11
  "typecheck": "tsc --noEmit && backtick-tsc -p server --noEmit",
12
+ "test": "node --import ./server/test/setup.mjs --import @backtickjs/node-plugin --test \"server/**/*.test.tsx\"",
12
13
  "reset-project": "node scripts/reset-project.mjs",
13
14
  "format": "prettier --write --ignore-unknown .",
14
15
  "format:check": "prettier --check --ignore-unknown ."
15
16
  },
16
17
  "dependencies": {
17
- "@backtickjs/bundler": "^0.1.3",
18
- "@backtickjs/core": "^0.1.3",
19
- "@backtickjs/node-plugin": "^0.1.3",
18
+ "@backtickjs/bundler": "^0.1.5",
19
+ "@backtickjs/core": "^0.1.5",
20
+ "@backtickjs/node-plugin": "^0.1.5",
20
21
  "@backtickjs/react": "^19.2.3",
21
22
  "@backtickjs/react-native": "^0.86.3",
22
- "@backtickjs/react-native-client": "^0.1.3",
23
+ "@backtickjs/react-native-client": "^0.1.5",
23
24
  "expo": "~57.0.26",
24
25
  "expo-constants": "~57.0.20",
25
26
  "expo-status-bar": "~57.0.1",
@@ -30,10 +31,13 @@
30
31
  "typescript": "^6.0.3"
31
32
  },
32
33
  "devDependencies": {
33
- "@backtickjs/prettier-plugin": "^0.1.3",
34
- "@backtickjs/tsc": "^0.1.3",
34
+ "@backtickjs/prettier-plugin": "^0.1.5",
35
+ "@backtickjs/tsc": "^0.1.5",
36
+ "@testing-library/react": "^16.3.3",
35
37
  "@types/node": "^25.9.1",
36
38
  "@types/react": "19.2.18",
39
+ "global-jsdom": "^30.0.0",
40
+ "jsdom": "^30.0.1",
37
41
  "prettier": "^3.6.2"
38
42
  },
39
43
  "backtick": {
@@ -15,6 +15,20 @@ export async function Home() {
15
15
  }
16
16
  `,
17
17
  );
18
+ writeFileSync(
19
+ "server/Home.test.tsx",
20
+ `import assert from "node:assert/strict";
21
+ import { it } from "node:test";
22
+ import { render, screen } from "@testing-library/react";
23
+ import { Home } from "./Home.js";
24
+ import { drawScreen } from "./test/drawScreen.js";
25
+
26
+ it("asks to be edited", async () => {
27
+ render(await drawScreen(<Home />));
28
+ assert.ok(screen.getByText("Edit server/Home.tsx to edit this screen."));
29
+ });
30
+ `,
31
+ );
18
32
  rmSync("server/HelloWave.tsx", { force: true });
19
33
 
20
34
  console.log(
@@ -4,6 +4,8 @@
4
4
  // `--ios`.
5
5
  import { spawn } from "node:child_process";
6
6
  import { readFileSync } from "node:fs";
7
+ import { networkInterfaces } from "node:os";
8
+ import { setTimeout as sleep } from "node:timers/promises";
7
9
 
8
10
  // The runtime is the one package.json has a Backtick loader for: Bun's, or
9
11
  // else Node's.
@@ -29,3 +31,27 @@ expo.on("exit", (code) => {
29
31
  server.kill();
30
32
  process.exit(code ?? 0);
31
33
  });
34
+
35
+ // Expo shows how to open the app, its QR code, only in an interactive
36
+ // terminal. Run elsewhere, as an agent runs it, this prints the address once
37
+ // Metro is up.
38
+ if (!process.stdout.isTTY) {
39
+ const args = process.argv.slice(2);
40
+ const flag = args.findIndex((arg) => arg === "--port" || arg === "-p");
41
+ const port = flag === -1 ? 8081 : Number(args[flag + 1]);
42
+ const address = Object.values(networkInterfaces())
43
+ .flat()
44
+ .find(
45
+ (network) => network?.family === "IPv4" && !network.internal,
46
+ )?.address;
47
+ while (address !== undefined && expo.exitCode === null) {
48
+ const status = await fetch(`http://localhost:${port}/status`)
49
+ .then((response) => response.text())
50
+ .catch(() => "");
51
+ if (status === "packager-status:running") {
52
+ console.log(`\nOpen exp://${address}:${port} in Expo Go.\n`);
53
+ break;
54
+ }
55
+ await sleep(500);
56
+ }
57
+ }
@@ -1,11 +1,13 @@
1
1
  import { cs } from "@backtickjs/core";
2
2
  import { useEffect, useRef } from "@backtickjs/react";
3
- import { Animated, Pressable } from "@backtickjs/react-native";
3
+ import { Animated, Platform, Pressable } from "@backtickjs/react-native";
4
4
 
5
5
  // A client component: it runs on the phone, so its animation does too. It
6
6
  // waves when it appears, and again when you tap it.
7
7
  export const HelloWave = cs`() => {
8
8
  const rotation = $useRef(new $Animated.Value(0)).current;
9
+ // The web has no native animation driver.
10
+ const nativeDriver = $Platform.OS !== "web";
9
11
  const wave = () =>
10
12
  $Animated
11
13
  .loop(
@@ -13,12 +15,12 @@ export const HelloWave = cs`() => {
13
15
  $Animated.timing(rotation, {
14
16
  toValue: 1,
15
17
  duration: 150,
16
- useNativeDriver: true,
18
+ useNativeDriver: nativeDriver,
17
19
  }),
18
20
  $Animated.timing(rotation, {
19
21
  toValue: 0,
20
22
  duration: 150,
21
- useNativeDriver: true,
23
+ useNativeDriver: nativeDriver,
22
24
  }),
23
25
  ]),
24
26
  { iterations: 4 },
@@ -0,0 +1,11 @@
1
+ import assert from "node:assert/strict";
2
+ import { it } from "node:test";
3
+ import { render, screen } from "@testing-library/react";
4
+ import { Home } from "./Home.js";
5
+ import { drawScreen } from "./test/drawScreen.js";
6
+
7
+ it("shows the Node version the server ran", async () => {
8
+ render(await drawScreen(<Home />));
9
+ assert.ok(screen.getByText("Welcome!"));
10
+ assert.ok(screen.getByText(new RegExp(`with Node ${process.version}`)));
11
+ });
@@ -0,0 +1,29 @@
1
+ import { createRequire } from "node:module";
2
+ import { bundler } from "@backtickjs/bundler";
3
+ import type { Spliceable } from "@backtickjs/core";
4
+ import { evaluate } from "@backtickjs/react-native-client";
5
+ import * as React from "react";
6
+ import * as JSXRuntime from "react/jsx-runtime";
7
+ import * as ReactNativeWeb from "react-native-web";
8
+
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,
21
+ };
22
+
23
+ // A screen, bundled as your server bundles it and run as your app runs it,
24
+ // for Testing Library to draw.
25
+ export async function drawScreen(input: Spliceable): Promise<React.ReactNode> {
26
+ const bundle = await bundler.build({ input, packageVersions });
27
+ const { code } = bundle.generate({ format: "cjs" });
28
+ return evaluate(code, modules) as React.ReactNode;
29
+ }
@@ -0,0 +1,2 @@
1
+ // React Native's web build, which ships no types of its own.
2
+ declare module "react-native-web";
@@ -0,0 +1,21 @@
1
+ // What `npm test` runs in: jsdom as the document Testing Library draws into,
2
+ // React told it's under test, and each test's drawing cleared after it.
3
+ import "global-jsdom/register";
4
+ import { cleanup } from "@testing-library/react";
5
+ import { afterEach } from "node:test";
6
+
7
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
8
+
9
+ afterEach(cleanup);
10
+
11
+ // jsdom has no ResizeObserver, which React Native's web build looks for as it
12
+ // loads, to report layout.
13
+ window.ResizeObserver ??= class {
14
+ observe() {}
15
+ unobserve() {}
16
+ disconnect() {}
17
+ };
18
+
19
+ // Loaded before `@backtickjs/node-plugin`, so Node loads it as plain
20
+ // CommonJS.
21
+ await import("react-native-web");
@@ -11,15 +11,15 @@
11
11
  "format:check": "prettier --check --ignore-unknown ."
12
12
  },
13
13
  "dependencies": {
14
- "@backtickjs/bundler": "^0.1.3",
15
- "@backtickjs/core": "^0.1.3",
16
- "@backtickjs/node-plugin": "^0.1.3",
14
+ "@backtickjs/bundler": "^0.1.5",
15
+ "@backtickjs/core": "^0.1.5",
16
+ "@backtickjs/node-plugin": "^0.1.5",
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.3",
22
- "@backtickjs/tsc": "^0.1.3",
21
+ "@backtickjs/prettier-plugin": "^0.1.5",
22
+ "@backtickjs/tsc": "^0.1.5",
23
23
  "@types/node": "^25.9.1",
24
24
  "prettier": "^3.6.2"
25
25
  },