create-backtick-app 0.1.4 → 0.1.6

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
  # create-backtick-app
2
2
 
3
- Creates an app whose screens come from your server, with [Backtick](https://backtickjs.com). Pick React Native and you get an Expo app with a Backtick server beside it; pick React or Solid and you get a web page served by one. Each starts with a welcome screen written as a server component, a small client component, and a development setup that redraws the screen when you save.
3
+ Creates an app whose screens come from your server, with [Backtick](https://backtickjs.com): an Expo app with a Backtick server beside it, or with `--template react` or `--template solid-js`, a web page served by one. Each starts with a welcome screen written as a server component, a small client component, and a development setup that redraws the screen when you save.
4
4
 
5
5
  ## Usage
6
6
 
@@ -17,13 +17,7 @@ yarn create backtick-app
17
17
  bun create backtick-app
18
18
  ```
19
19
 
20
- It asks only what the command line didn't say:
21
-
22
- 1. What is your app named? (default `my-app`)
23
- 2. Which framework? `react-native` (an Expo app), `react` or `solid-js` (a web page)
24
- 3. Which runtime? `node` or `bun`: Bun is the default when you ran it with Bun (`bun create`), Node otherwise
25
-
26
- It then copies the template into a new directory, installs with the package manager you ran it with (npm, pnpm, Yarn or Bun), and prints the commands to start. Requires Node 22.15 or later.
20
+ It asks for a name, and nothing else: it copies the React Native template into that directory, installs with the package manager you ran it with (npm, pnpm, Yarn or Bun), and prints the commands to start. The server runs on Node, or on Bun when you ran it with Bun (`bun create`). Given the name on the command line, it asks nothing. Requires Node 22.15 or later.
27
21
 
28
22
  ## Options
29
23
 
@@ -31,13 +25,13 @@ It then copies the template into a new directory, installs with the package mana
31
25
  npx create-backtick-app@latest [name] [options]
32
26
  ```
33
27
 
34
- | Option | Description |
35
- | ----------------------- | ------------------------------------------------------------------------------- |
36
- | `name` | The directory to create, and the app's name. It must be new or empty. |
37
- | `-t, --template <name>` | `react-native`, `react` or `solid-js`. |
38
- | `-r, --runtime <name>` | What runs your Backtick server: `node` or `bun`. |
39
- | `-y, --yes` | Ask nothing: `my-app`, `react-native`, and Bun if run with Bun, Node otherwise. |
40
- | `--no-install` | Copy the files without installing dependencies. |
28
+ | Option | Description |
29
+ | ----------------------- | --------------------------------------------------------------------- |
30
+ | `name` | The directory to create, and the app's name. It must be new or empty. |
31
+ | `-t, --template <name>` | `react-native`, the default; or `react` or `solid-js`, a web page. |
32
+ | `-r, --runtime <name>` | What runs your Backtick server: `node`, the default, or `bun`. |
33
+ | `-y, --yes` | Take `my-app` as the name rather than asking. |
34
+ | `--no-install` | Copy the files without installing dependencies. |
41
35
 
42
36
  With `npm create`, pass options after `--`: `npm create backtick-app@latest my-app -- -t react`.
43
37
 
@@ -58,10 +52,13 @@ Every template has:
58
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.
59
53
 
60
54
  ```sh
61
- npm run ios # or android, or web
55
+ npm start # then scan the QR code with your phone, in Expo Go
56
+ npm run ios # or android, or web: a simulator, an emulator or a browser
62
57
  ```
63
58
 
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.
59
+ 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.
60
+
61
+ `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
62
 
66
63
  ### `react` and `solid-js`
67
64
 
@@ -78,7 +75,8 @@ Then open http://localhost:3000. Edit `server/Home.tsx` and save to see the page
78
75
  - [`@backtickjs/core`](https://github.com/backtickjs/backtick/tree/main/packages/core): the `cs` tag
79
76
  - [`@backtickjs/bundler`](https://github.com/backtickjs/backtick/tree/main/packages/bundler): runs server components and writes the bundle
80
77
  - [`@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)
78
+ - [Quick start](https://backtickjs.com/docs)
79
+ - [Tutorial](https://backtickjs.com/docs/tutorial)
82
80
 
83
81
  ## License
84
82
 
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
@@ -33,15 +49,15 @@ const RUNTIMES = {
33
49
  };
34
50
  const USAGE = `Usage: create-backtick-app [name] [options]
35
51
 
52
+ Creates a React Native app with a Backtick server beside it, on Node, or on
53
+ Bun when run with Bun.
54
+
36
55
  Options:
37
56
  -t, --template <name> The framework: ${Object.keys(TEMPLATES).join(", ")}
38
57
  -r, --runtime <name> The server's runtime: ${Object.keys(RUNTIMES).join(", ")}
39
- -y, --yes Use the defaults for anything not given
58
+ -y, --yes Take my-app as the name rather than asking
40
59
  --no-install Skip installing packages
41
- -h, --help Show this help
42
-
43
- Asks for what isn't given, in a terminal; elsewhere, uses the defaults:
44
- my-app, react-native, and node (bun when run with bun).`;
60
+ -h, --help Show this help`;
45
61
  let parsed;
46
62
  try {
47
63
  parsed = parseArgs({
@@ -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);
@@ -80,41 +103,24 @@ if (values.runtime !== undefined && !(values.runtime in RUNTIMES)) {
80
103
  const manager = (process.env.npm_config_user_agent ?? "npm").split("/")[0];
81
104
  const run = ["npm", "pnpm", "yarn", "bun"].includes(manager) ? manager : "npm";
82
105
  const suggestedRuntime = run === "bun" ? "bun" : "node";
83
- // Asked only what the command line didn't say, and only in a terminal: an
84
- // agent or a CI script can't answer, so it gets the defaults, as `--yes` does.
85
- const ask = process.stdin.isTTY === true && !values.yes;
106
+ // The name is the one question, asked when none was given, and only in a
107
+ // terminal: an agent or a CI script can't answer, so it gets `my-app`, as
108
+ // `--yes` does. The framework and the runtime have defaults, and flags for
109
+ // the rest, as create-expo-app does: a name on the command line is a project
110
+ // with no questions.
111
+ const ask = positionals[0] === undefined && process.stdin.isTTY === true && !values.yes;
86
112
  const answers = await prompts([
87
113
  {
88
- type: positionals[0] !== undefined || !ask ? null : "text",
114
+ type: ask ? "text" : null,
89
115
  name: "name",
90
116
  message: "What is your app named?",
91
117
  initial: "my-app",
92
118
  },
93
- {
94
- type: values.template !== undefined || !ask ? null : "select",
95
- name: "template",
96
- message: "Which framework?",
97
- choices: Object.entries(TEMPLATES).map(([value, template]) => ({
98
- title: template.title,
99
- description: template.description,
100
- value,
101
- })),
102
- },
103
- {
104
- type: values.runtime !== undefined || !ask ? null : "select",
105
- name: "runtime",
106
- message: "Which runtime?",
107
- choices: Object.entries(RUNTIMES).map(([value, runtime]) => ({
108
- title: runtime.title,
109
- value,
110
- })),
111
- initial: Object.keys(RUNTIMES).indexOf(suggestedRuntime),
112
- },
113
119
  ], { onCancel: () => process.exit(1) });
114
120
  const name = positionals[0] ?? answers.name ?? "my-app";
115
- const templateName = values.template ?? answers.template ?? "react-native";
121
+ const templateName = values.template ?? "react-native";
116
122
  const template = TEMPLATES[templateName];
117
- const runtimeName = values.runtime ?? answers.runtime ?? suggestedRuntime;
123
+ const runtimeName = values.runtime ?? suggestedRuntime;
118
124
  const target = path.resolve(name);
119
125
  const slug = path.basename(target);
120
126
  if (existsSync(target) && readdirSync(target).length > 0) {
@@ -162,15 +168,16 @@ if (!values["no-install"]) {
162
168
  process.exit(status ?? 1);
163
169
  }
164
170
  }
171
+ // Created where it was run, there's nowhere to go first.
172
+ const here = target === process.cwd();
173
+ const script = (name) => `${run} run ${name}`;
174
+ const start = [...(here ? [] : [`cd ${slug}`]), script("start")]
175
+ .map((command) => ` ${command}`)
176
+ .join("\n");
165
177
  console.log(`
166
178
  ✅ Your project is ready!
167
179
 
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}
180
+ ${template.next(start, script)}
174
181
  `);
175
182
  function rewriteJson(file, change) {
176
183
  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.4",
3
+ "version": "0.1.6",
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.4",
15
- "@backtickjs/core": "^0.1.4",
16
- "@backtickjs/node-plugin": "^0.1.4",
14
+ "@backtickjs/bundler": "^0.1.6",
15
+ "@backtickjs/core": "^0.1.6",
16
+ "@backtickjs/node-plugin": "^0.1.6",
17
17
  "@backtickjs/react": "^19.2.3",
18
18
  "typescript": "^6.0.3"
19
19
  },
20
20
  "devDependencies": {
21
- "@backtickjs/prettier-plugin": "^0.1.4",
22
- "@backtickjs/tsc": "^0.1.4",
21
+ "@backtickjs/prettier-plugin": "^0.1.6",
22
+ "@backtickjs/tsc": "^0.1.6",
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.4",
18
- "@backtickjs/core": "^0.1.4",
19
- "@backtickjs/node-plugin": "^0.1.4",
18
+ "@backtickjs/bundler": "^0.1.6",
19
+ "@backtickjs/core": "^0.1.6",
20
+ "@backtickjs/node-plugin": "^0.1.6",
20
21
  "@backtickjs/react": "^19.2.3",
21
22
  "@backtickjs/react-native": "^0.86.3",
22
- "@backtickjs/react-native-client": "^0.1.4",
23
+ "@backtickjs/react-native-client": "^0.1.6",
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.4",
34
- "@backtickjs/tsc": "^0.1.4",
34
+ "@backtickjs/prettier-plugin": "^0.1.6",
35
+ "@backtickjs/tsc": "^0.1.6",
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.4",
15
- "@backtickjs/core": "^0.1.4",
16
- "@backtickjs/node-plugin": "^0.1.4",
14
+ "@backtickjs/bundler": "^0.1.6",
15
+ "@backtickjs/core": "^0.1.6",
16
+ "@backtickjs/node-plugin": "^0.1.6",
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.4",
22
- "@backtickjs/tsc": "^0.1.4",
21
+ "@backtickjs/prettier-plugin": "^0.1.6",
22
+ "@backtickjs/tsc": "^0.1.6",
23
23
  "@types/node": "^25.9.1",
24
24
  "prettier": "^3.6.2"
25
25
  },