create-backtick-app 0.2.0 → 0.2.2
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 +3 -6
- package/dist/index.js +7 -4
- package/package.json +1 -1
- package/templates/react/package.json +6 -6
- package/templates/react-native/AGENTS.md +1 -1
- package/templates/react-native/app.json +1 -1
- package/templates/react-native/client/App.tsx +7 -7
- package/templates/react-native/client/moduleMap.ts +3 -3
- package/templates/react-native/package.json +8 -14
- package/templates/react-native/server/index.tsx +4 -1
- package/templates/react-native/server/screen.tsx +3 -11
- package/templates/react-native/server/test/drawScreen.ts +7 -5
- package/templates/solid-js/package.json +7 -7
- package/templates/solid-js/server/html.ts +6 -6
- package/templates/react-native/scripts/reload-expo.mjs +0 -37
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ yarn create backtick-app
|
|
|
17
17
|
bun create backtick-app
|
|
18
18
|
```
|
|
19
19
|
|
|
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.
|
|
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`) or passed `--runtime bun`; tests run on Node either way. Given the name on the command line, it asks nothing. Requires Node 22.15 or later.
|
|
21
21
|
|
|
22
22
|
## Options
|
|
23
23
|
|
|
@@ -52,17 +52,14 @@ Every template has:
|
|
|
52
52
|
|
|
53
53
|
An Expo app (SDK 57) with the Backtick server beside it in `server/`. `client/App.tsx` fetches the screen at `/` and runs it with `execRoot` from [`@backtickjs/core/cjs`](https://github.com/backtickjs/backtick/tree/main/packages/core), offering screens what `client/moduleMap.ts` lists.
|
|
54
54
|
|
|
55
|
-
Each build of the app sends
|
|
56
|
-
|
|
57
|
-
- The server draws only registered builds, one file each in `builds/`. `npm start` registers this code's builds as Expo starts; `npm run register-build` does it without Expo, as before a build for the stores. Commit what they write.
|
|
58
|
-
- On EAS, `eas-build-post-install` fails a preview or production build that isn't registered.
|
|
55
|
+
Each build of the app sends the modules it carries: those `client/moduleMap.ts` offers, at the versions the `@backtickjs/expo` plugin in `app.json` records. The server bundles each screen for those modules ([more](https://github.com/backtickjs/backtick/tree/main/packages/core#every-build-in-users-hands)), so a build in users' hands keeps working as the app moves on, with nothing to record before it ships.
|
|
59
56
|
|
|
60
57
|
```sh
|
|
61
58
|
npm start # then scan the QR code with your phone, in Expo Go
|
|
62
59
|
npm run ios # or android, or web: a simulator, an emulator or a browser
|
|
63
60
|
```
|
|
64
61
|
|
|
65
|
-
Each starts your Backtick server alongside Expo, in one terminal. Edit `server/Home.tsx` and save: the server restarts, and
|
|
62
|
+
Each starts your Backtick server alongside Expo, in one terminal. Edit `server/Home.tsx` and save: the server restarts, and once it listens, `reloadExpo()` in `server/index.tsx` reloads the app. The app fetches the new screen without rebuilding.
|
|
66
63
|
|
|
67
64
|
`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.
|
|
68
65
|
|
package/dist/index.js
CHANGED
|
@@ -142,8 +142,8 @@ rewriteJson(path.join(target, "package.json"), (json) => {
|
|
|
142
142
|
json.name = slug;
|
|
143
143
|
// The templates ship Node's loader and scripts; another runtime gets its own.
|
|
144
144
|
const loader = RUNTIMES[runtimeName].loader;
|
|
145
|
+
const version = json.dependencies["@backtickjs/node-plugin"];
|
|
145
146
|
if (!(loader in json.dependencies)) {
|
|
146
|
-
const version = json.dependencies["@backtickjs/node-plugin"];
|
|
147
147
|
delete json.dependencies["@backtickjs/node-plugin"];
|
|
148
148
|
json.dependencies = Object.fromEntries(Object.entries({ ...json.dependencies, [loader]: version }).sort());
|
|
149
149
|
}
|
|
@@ -155,9 +155,12 @@ rewriteJson(path.join(target, "package.json"), (json) => {
|
|
|
155
155
|
for (const [name, command] of Object.entries(json.scripts)) {
|
|
156
156
|
json.scripts[name] = bun(command);
|
|
157
157
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
158
|
+
// What still runs on Node, as the tests do, keeps Node's loader.
|
|
159
|
+
if (Object.values(json.scripts).some((command) => command.includes("@backtickjs/node-plugin"))) {
|
|
160
|
+
json.devDependencies = Object.fromEntries(Object.entries({
|
|
161
|
+
...json.devDependencies,
|
|
162
|
+
"@backtickjs/node-plugin": version,
|
|
163
|
+
}).sort());
|
|
161
164
|
}
|
|
162
165
|
}
|
|
163
166
|
});
|
package/package.json
CHANGED
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
"format:check": "prettier --check --ignore-unknown ."
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@backtickjs/bundler": "^0.2.
|
|
15
|
-
"@backtickjs/core": "^0.2.
|
|
16
|
-
"@backtickjs/node-plugin": "^0.2.
|
|
17
|
-
"@backtickjs/jsx-react": "^0.2.
|
|
14
|
+
"@backtickjs/bundler": "^0.2.2",
|
|
15
|
+
"@backtickjs/core": "^0.2.2",
|
|
16
|
+
"@backtickjs/node-plugin": "^0.2.2",
|
|
17
|
+
"@backtickjs/jsx-react": "^0.2.2",
|
|
18
18
|
"@hono/node-server": "^2.1.4",
|
|
19
19
|
"hono": "^4.13.13",
|
|
20
20
|
"react": "19.2.3",
|
|
@@ -22,8 +22,8 @@
|
|
|
22
22
|
"typescript": "^6.0.3"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
|
-
"@backtickjs/prettier-plugin": "^0.2.
|
|
26
|
-
"@backtickjs/tsc": "^0.2.
|
|
25
|
+
"@backtickjs/prettier-plugin": "^0.2.2",
|
|
26
|
+
"@backtickjs/tsc": "^0.2.2",
|
|
27
27
|
"@types/node": "^25.9.1",
|
|
28
28
|
"@types/react": "19.2.18",
|
|
29
29
|
"@types/react-dom": "19.2.7",
|
|
@@ -6,4 +6,4 @@ The server's output is in the terminal `npm start` runs in.
|
|
|
6
6
|
|
|
7
7
|
Saving a file under `server/` restarts the server, and Expo reloads the app once it's back: what the server keeps in memory starts over.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
An upgraded package reaches the server once the app is closed and opened again: a reload keeps the versions it was opened with.
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type { Bundle } from "@backtickjs/core";
|
|
1
|
+
import { type Bundle, modulesOf } from "@backtickjs/core";
|
|
3
2
|
import { execRoot } from "@backtickjs/core/cjs";
|
|
3
|
+
import { getBacktickExtra } from "@backtickjs/expo";
|
|
4
4
|
import Constants from "expo-constants";
|
|
5
5
|
import { StatusBar } from "expo-status-bar";
|
|
6
6
|
import { type ReactNode, Suspense, use, useState } from "react";
|
|
7
|
-
import { ActivityIndicator,
|
|
7
|
+
import { ActivityIndicator, Text } from "react-native";
|
|
8
8
|
import {
|
|
9
9
|
ErrorBoundary,
|
|
10
10
|
type FallbackProps,
|
|
@@ -16,15 +16,15 @@ import { moduleMap } from "./moduleMap";
|
|
|
16
16
|
const host = Constants.expoConfig?.hostUri?.split(":")[0] ?? "localhost";
|
|
17
17
|
const server = `http://${host}:3000`;
|
|
18
18
|
|
|
19
|
-
//
|
|
20
|
-
|
|
21
|
-
const
|
|
19
|
+
// The modules this build carries, at the versions app.json's plugin records.
|
|
20
|
+
const { installedPackages } = getBacktickExtra(Constants.expoConfig);
|
|
21
|
+
const modules = JSON.stringify(modulesOf(moduleMap, installedPackages));
|
|
22
22
|
|
|
23
23
|
// The screen at `path` on your server: its bundle, fetched and run with the
|
|
24
24
|
// app's modules. Your fetch to shape: headers, auth, caching.
|
|
25
25
|
async function fetchScreen(path: string): Promise<ReactNode> {
|
|
26
26
|
const response = await fetch(`${server}${path}`, {
|
|
27
|
-
headers: { "x-
|
|
27
|
+
headers: { "x-backtick-modules": modules },
|
|
28
28
|
});
|
|
29
29
|
if (!response.ok) {
|
|
30
30
|
throw new Error(`${path} answered ${response.status}.`);
|
|
@@ -3,9 +3,9 @@ import * as JSXRuntime from "react/jsx-runtime";
|
|
|
3
3
|
import * as ReactNative from "react-native";
|
|
4
4
|
|
|
5
5
|
// What a screen may require, as your app imports it: the packages it's built
|
|
6
|
-
// with. Add one when a screen uses another package
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// with. Add one when a screen uses another package, from package.json's
|
|
7
|
+
// dependencies: the app sends your server what it offers, at the versions
|
|
8
|
+
// it's built with. A platform that differs gets its own file, as
|
|
9
9
|
// Metro picks files: moduleMap.web.ts, moduleMap.ios.ts.
|
|
10
10
|
export const moduleMap = {
|
|
11
11
|
react: React,
|
|
@@ -12,17 +12,15 @@
|
|
|
12
12
|
"typecheck": "tsc --noEmit && backtick-tsc -p server --noEmit",
|
|
13
13
|
"test": "node --import ./server/test/setup.mjs --import @backtickjs/node-plugin --test \"server/**/*.test.tsx\"",
|
|
14
14
|
"reset-project": "node scripts/reset-project.mjs",
|
|
15
|
-
"register-build": "backtick-builds register",
|
|
16
|
-
"eas-build-post-install": "backtick-builds check",
|
|
17
15
|
"format": "prettier --write --ignore-unknown .",
|
|
18
16
|
"format:check": "prettier --check --ignore-unknown ."
|
|
19
17
|
},
|
|
20
18
|
"dependencies": {
|
|
21
|
-
"@backtickjs/
|
|
22
|
-
"@backtickjs/
|
|
23
|
-
"@backtickjs/
|
|
24
|
-
"@backtickjs/node-plugin": "^0.2.
|
|
25
|
-
"@backtickjs/jsx-react": "^0.2.
|
|
19
|
+
"@backtickjs/bundler": "^0.2.2",
|
|
20
|
+
"@backtickjs/core": "^0.2.2",
|
|
21
|
+
"@backtickjs/expo": "^0.2.2",
|
|
22
|
+
"@backtickjs/node-plugin": "^0.2.2",
|
|
23
|
+
"@backtickjs/jsx-react": "^0.2.2",
|
|
26
24
|
"@hono/node-server": "^2.1.4",
|
|
27
25
|
"expo": "~57.0.26",
|
|
28
26
|
"expo-constants": "~57.0.20",
|
|
@@ -36,8 +34,8 @@
|
|
|
36
34
|
"typescript": "^6.0.3"
|
|
37
35
|
},
|
|
38
36
|
"devDependencies": {
|
|
39
|
-
"@backtickjs/prettier-plugin": "^0.2.
|
|
40
|
-
"@backtickjs/tsc": "^0.2.
|
|
37
|
+
"@backtickjs/prettier-plugin": "^0.2.2",
|
|
38
|
+
"@backtickjs/tsc": "^0.2.2",
|
|
41
39
|
"@testing-library/react": "^16.3.3",
|
|
42
40
|
"@testing-library/user-event": "^14.6.7",
|
|
43
41
|
"@types/node": "^25.9.1",
|
|
@@ -53,13 +51,9 @@
|
|
|
53
51
|
"watch": [
|
|
54
52
|
"server"
|
|
55
53
|
],
|
|
56
|
-
"ext": "ts,tsx"
|
|
57
|
-
"events": {
|
|
58
|
-
"start": "node scripts/reload-expo.mjs"
|
|
59
|
-
}
|
|
54
|
+
"ext": "ts,tsx"
|
|
60
55
|
},
|
|
61
56
|
"backtick": {
|
|
62
|
-
"clientRoot": "client",
|
|
63
57
|
"plugins": [
|
|
64
58
|
"@backtickjs/jsx-react/plugin"
|
|
65
59
|
]
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { reloadExpo } from "@backtickjs/expo/server";
|
|
1
2
|
import { serve } from "@hono/node-server";
|
|
2
3
|
import { Hono } from "hono";
|
|
3
4
|
import { cors } from "hono/cors";
|
|
@@ -7,7 +8,7 @@ import { screen } from "./screen.js";
|
|
|
7
8
|
const app = new Hono();
|
|
8
9
|
|
|
9
10
|
// `npm run web` serves the app from another origin.
|
|
10
|
-
app.use(cors({ origin: "*", allowHeaders: ["x-
|
|
11
|
+
app.use(cors({ origin: "*", allowHeaders: ["x-backtick-modules"] }));
|
|
11
12
|
|
|
12
13
|
// Your app's screens: add a route, and the app can fetch it. A path can
|
|
13
14
|
// take parameters, `/stories/:id`, read with `context.req.param("id")`.
|
|
@@ -19,6 +20,8 @@ app.onError((error, context) => {
|
|
|
19
20
|
return context.text(String(error), 500);
|
|
20
21
|
});
|
|
21
22
|
|
|
23
|
+
// Once it listens, the app Expo serves reloads to show this server's screens.
|
|
22
24
|
serve({ fetch: app.fetch, port: 3000 }, () => {
|
|
23
25
|
console.log("Backtick server on http://localhost:3000");
|
|
26
|
+
reloadExpo();
|
|
24
27
|
});
|
|
@@ -1,22 +1,14 @@
|
|
|
1
|
-
import { fileURLToPath } from "node:url";
|
|
2
|
-
import { createBuilds } from "@backtickjs/builds";
|
|
3
1
|
import { bundler } from "@backtickjs/bundler";
|
|
4
2
|
import type { JSX } from "@backtickjs/jsx-react/jsx-runtime";
|
|
5
3
|
import type { Context } from "hono";
|
|
6
4
|
|
|
7
|
-
// Your app's builds: each sends its id, and a screen is bundled for the
|
|
8
|
-
// modules that build carries, from client/moduleMap.ts. A build in users' hands
|
|
9
|
-
// keeps working as your app moves on, once registered: see builds/.
|
|
10
|
-
const builds = createBuilds({
|
|
11
|
-
projectRoot: fileURLToPath(new URL("..", import.meta.url)),
|
|
12
|
-
});
|
|
13
|
-
|
|
14
5
|
/**
|
|
15
6
|
* `element` as the response to a request from your app, bundled for the
|
|
16
|
-
* modules the requesting build carries.
|
|
7
|
+
* modules the requesting build carries, as client/moduleMap.ts offers them
|
|
8
|
+
* at its packages' versions.
|
|
17
9
|
*/
|
|
18
10
|
export async function screen(context: Context, element: JSX.Element) {
|
|
19
|
-
const modules =
|
|
11
|
+
const modules = JSON.parse(context.req.header("x-backtick-modules") ?? "{}");
|
|
20
12
|
const bundle = await bundler.build({ input: element, modules });
|
|
21
13
|
const { code } = bundle.generate({ format: "cjs" });
|
|
22
14
|
return context.body(code, 200, { "content-type": "text/javascript" });
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { fileURLToPath } from "node:url";
|
|
2
|
-
import { createBuilds } from "@backtickjs/builds";
|
|
3
2
|
import { bundler } from "@backtickjs/bundler";
|
|
4
|
-
import type
|
|
3
|
+
import { modulesOf, type Spliceable } from "@backtickjs/core";
|
|
4
|
+
import { installedPackages } from "@backtickjs/core/node";
|
|
5
5
|
import { execRoot } from "@backtickjs/core/cjs";
|
|
6
6
|
import * as React from "react";
|
|
7
7
|
import * as JSXRuntime from "react/jsx-runtime";
|
|
@@ -10,14 +10,16 @@ import * as ReactNativeWeb from "react-native-web";
|
|
|
10
10
|
// What the app's web build provides, as client/moduleMap.ts says, with React
|
|
11
11
|
// Native's web build standing in for React Native. Add a stand-in here when
|
|
12
12
|
// your module map offers another package.
|
|
13
|
-
const modules = createBuilds({
|
|
14
|
-
projectRoot: fileURLToPath(new URL("../..", import.meta.url)),
|
|
15
|
-
}).installed("web");
|
|
16
13
|
const moduleMap = {
|
|
17
14
|
react: React,
|
|
18
15
|
"react/jsx-runtime": JSXRuntime,
|
|
19
16
|
"react-native": ReactNativeWeb,
|
|
20
17
|
};
|
|
18
|
+
// at the versions the app installs, as its build sends them
|
|
19
|
+
const modules = modulesOf(
|
|
20
|
+
moduleMap,
|
|
21
|
+
installedPackages(fileURLToPath(new URL("../..", import.meta.url))),
|
|
22
|
+
);
|
|
21
23
|
|
|
22
24
|
// A screen, bundled as your server bundles it and run as your app runs it,
|
|
23
25
|
// for Testing Library to draw.
|
|
@@ -11,18 +11,18 @@
|
|
|
11
11
|
"format:check": "prettier --check --ignore-unknown ."
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@backtickjs/bundler": "^0.2.
|
|
15
|
-
"@backtickjs/core": "^0.2.
|
|
16
|
-
"@backtickjs/node-plugin": "^0.2.
|
|
17
|
-
"@backtickjs/jsx-solid": "^0.2.
|
|
14
|
+
"@backtickjs/bundler": "^0.2.2",
|
|
15
|
+
"@backtickjs/core": "^0.2.2",
|
|
16
|
+
"@backtickjs/node-plugin": "^0.2.2",
|
|
17
|
+
"@backtickjs/jsx-solid": "^0.2.2",
|
|
18
18
|
"@hono/node-server": "^2.1.4",
|
|
19
19
|
"hono": "^4.13.13",
|
|
20
|
-
"solid-js": "1.9.
|
|
20
|
+
"solid-js": "1.9.17",
|
|
21
21
|
"typescript": "^6.0.3"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@backtickjs/prettier-plugin": "^0.2.
|
|
25
|
-
"@backtickjs/tsc": "^0.2.
|
|
24
|
+
"@backtickjs/prettier-plugin": "^0.2.2",
|
|
25
|
+
"@backtickjs/tsc": "^0.2.2",
|
|
26
26
|
"@types/node": "^25.9.1",
|
|
27
27
|
"agent-device": "^0.21.27",
|
|
28
28
|
"nodemon": "^3.1.14",
|
|
@@ -5,9 +5,9 @@ import type { JSX } from "@backtickjs/jsx-solid/jsx-runtime";
|
|
|
5
5
|
|
|
6
6
|
// The version of Solid the page loads, below: what each page is bundled for.
|
|
7
7
|
const modules = {
|
|
8
|
-
"solid-js": "1.9.
|
|
9
|
-
"solid-js/store": "1.9.
|
|
10
|
-
"solid-js/web": "1.9.
|
|
8
|
+
"solid-js": "1.9.17",
|
|
9
|
+
"solid-js/store": "1.9.17",
|
|
10
|
+
"solid-js/web": "1.9.17",
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
// Development unless NODE_ENV says production, as hosts set it for a deploy.
|
|
@@ -35,9 +35,9 @@ export async function toHtml(element: JSX.Element): Promise<string> {
|
|
|
35
35
|
<script type="importmap">
|
|
36
36
|
{
|
|
37
37
|
"imports": {
|
|
38
|
-
"solid-js": "https://cdn.jsdelivr.net/npm/solid-js@1.9.
|
|
39
|
-
"solid-js/web": "https://cdn.jsdelivr.net/npm/solid-js@1.9.
|
|
40
|
-
"solid-js/store": "https://cdn.jsdelivr.net/npm/solid-js@1.9.
|
|
38
|
+
"solid-js": "https://cdn.jsdelivr.net/npm/solid-js@1.9.17/dist/solid.js",
|
|
39
|
+
"solid-js/web": "https://cdn.jsdelivr.net/npm/solid-js@1.9.17/web/dist/web.js",
|
|
40
|
+
"solid-js/store": "https://cdn.jsdelivr.net/npm/solid-js@1.9.17/store/dist/store.js"
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
</script>
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
// Run by nodemon each time it starts your server: once the server listens,
|
|
2
|
-
// the apps Expo serves reload, as pressing `r` in Expo's terminal does: a
|
|
3
|
-
// `reload` on its message socket, which it accepts from this computer.
|
|
4
|
-
// Nothing happens without Expo running.
|
|
5
|
-
import { connect } from "node:net";
|
|
6
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
7
|
-
|
|
8
|
-
// Your server's port, and Expo's: React Native's variable for it.
|
|
9
|
-
const port = 3000;
|
|
10
|
-
const metro = `ws://127.0.0.1:${process.env.RCT_METRO_PORT ?? 8081}/message`;
|
|
11
|
-
|
|
12
|
-
// A connection, not a request, so the server has nothing to answer or log.
|
|
13
|
-
const listening = () =>
|
|
14
|
-
new Promise((resolve) => {
|
|
15
|
-
const socket = connect(port, "localhost");
|
|
16
|
-
socket.on("connect", () => {
|
|
17
|
-
socket.end();
|
|
18
|
-
resolve(true);
|
|
19
|
-
});
|
|
20
|
-
socket.on("error", () => resolve(false));
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// Waited for, so the app doesn't fetch from a server still starting; given up
|
|
24
|
-
// on when it crashed instead.
|
|
25
|
-
for (let tries = 0; tries < 120; tries++) {
|
|
26
|
-
if (await listening()) {
|
|
27
|
-
const socket = new WebSocket(metro);
|
|
28
|
-
socket.onopen = () => {
|
|
29
|
-
socket.send(JSON.stringify({ version: 2, method: "reload" }));
|
|
30
|
-
socket.close();
|
|
31
|
-
};
|
|
32
|
-
// Expo isn't running: there's no app to reload.
|
|
33
|
-
socket.onerror = () => {};
|
|
34
|
-
break;
|
|
35
|
-
}
|
|
36
|
-
await sleep(250);
|
|
37
|
-
}
|