create-backtick-app 0.1.9 → 0.1.11
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 +12 -12
- package/dist/index.js +14 -2
- package/package.json +1 -1
- package/templates/react/.mcp.json +8 -0
- package/templates/react/AGENTS.md +7 -0
- package/templates/react/CLAUDE.md +1 -0
- package/templates/react/package.json +16 -6
- package/templates/react/server/Home.tsx +2 -2
- package/templates/react/server/html.ts +2 -18
- package/templates/react/server/index.tsx +13 -26
- package/templates/react-native/.mcp.json +8 -0
- package/templates/react-native/AGENTS.md +9 -0
- package/templates/react-native/CLAUDE.md +1 -0
- package/templates/react-native/app.json +2 -1
- package/templates/react-native/client/App.tsx +69 -0
- package/templates/react-native/client/moduleMap.ts +14 -0
- package/templates/react-native/package.json +28 -12
- package/templates/react-native/scripts/reload-expo.mjs +37 -0
- package/templates/react-native/server/Home.tsx +3 -3
- package/templates/react-native/server/index.tsx +16 -53
- package/templates/react-native/server/screen.tsx +23 -0
- package/templates/react-native/server/test/drawScreen.ts +7 -7
- package/templates/react-native/server/tsconfig.json +1 -2
- package/templates/react-native/tsconfig.json +2 -1
- package/templates/solid-js/.mcp.json +8 -0
- package/templates/solid-js/AGENTS.md +7 -0
- package/templates/solid-js/CLAUDE.md +1 -0
- package/templates/solid-js/package.json +16 -6
- package/templates/solid-js/server/Home.tsx +2 -2
- package/templates/solid-js/server/html.ts +2 -18
- package/templates/solid-js/server/index.tsx +13 -26
- package/templates/react/scripts/start.mjs +0 -20
- package/templates/react-native/App.tsx +0 -119
- package/templates/react-native/app.config.ts +0 -10
- package/templates/react-native/moduleMap.ts +0 -12
- package/templates/react-native/scripts/start.mjs +0 -57
- package/templates/solid-js/scripts/start.mjs +0 -20
- /package/templates/react-native/{index.ts → client/index.ts} +0 -0
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): 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
|
|
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 restarts the server when you save.
|
|
4
4
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
@@ -41,19 +41,20 @@ 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.
|
|
45
|
-
- `
|
|
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
|
+
- `npm start`: starts the server in development with [nodemon](https://nodemon.io), restarting it whenever you save a file under `server/`. In `react-native`, Expo starts beside it.
|
|
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.
|
|
48
|
+
- `AGENTS.md`, which `CLAUDE.md` points to: how a coding agent sees the app while it works, with [agent-device](https://github.com/callstackincubator/agent-device)'s MCP tools from `.mcp.json`. In `react-native` they use the app on a phone `adb devices` lists; in `react` and `solid-js`, the page in a browser.
|
|
48
49
|
- Prettier with `@backtickjs/prettier-plugin`, which formats client scripts too, and VS Code recommendations for the Backtick and Prettier extensions.
|
|
49
50
|
|
|
50
51
|
### `react-native`
|
|
51
52
|
|
|
52
|
-
An Expo app (SDK 57) with the Backtick server beside it in `server/`. `App.tsx` fetches the screen at
|
|
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.
|
|
53
54
|
|
|
54
|
-
Each build of the app sends an id that names the modules it carries, which `app.
|
|
55
|
+
Each build of the app sends an id that names the modules it carries, which the `@backtickjs/builds/expo` plugin in `app.json` works out from `client/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
|
|
|
56
|
-
- `npm
|
|
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.
|
|
57
58
|
- On EAS, `eas-build-post-install` fails a preview or production build that isn't registered.
|
|
58
59
|
|
|
59
60
|
```sh
|
|
@@ -61,9 +62,9 @@ npm start # then scan the QR code with your phone, in Expo Go
|
|
|
61
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
|
|
65
|
+
Each starts your Backtick server alongside Expo, in one terminal. Edit `server/Home.tsx` and save: the server restarts, and `scripts/reload-expo.mjs`, which nodemon runs each time it starts the server, reloads the app once the server listens. The app fetches the new screen without rebuilding.
|
|
65
66
|
|
|
66
|
-
`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.
|
|
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.
|
|
67
68
|
|
|
68
69
|
### `react` and `solid-js`
|
|
69
70
|
|
|
@@ -73,15 +74,14 @@ A web page: the server bundles `server/Home.tsx` into an HTML page at `/`, with
|
|
|
73
74
|
npm start
|
|
74
75
|
```
|
|
75
76
|
|
|
76
|
-
Then open http://localhost:3000. Edit `server/Home.tsx
|
|
77
|
+
Then open http://localhost:3000. Edit `server/Home.tsx`, save, and refresh the page to see it change.
|
|
77
78
|
|
|
78
79
|
## Related
|
|
79
80
|
|
|
80
81
|
- [`@backtickjs/core`](https://github.com/backtickjs/backtick/tree/main/packages/core): the `cs` tag
|
|
81
82
|
- [`@backtickjs/bundler`](https://github.com/backtickjs/backtick/tree/main/packages/bundler): runs server components and writes the bundle
|
|
82
|
-
- [
|
|
83
|
-
- [
|
|
84
|
-
- [Tutorial](https://backtickjs.com/docs/tutorial)
|
|
83
|
+
- [Quick start](https://backtickjs.com/docs/)
|
|
84
|
+
- [Tutorial](https://backtickjs.com/docs/tutorial/)
|
|
85
85
|
|
|
86
86
|
## License
|
|
87
87
|
|
package/dist/index.js
CHANGED
|
@@ -140,14 +140,26 @@ cpSync(templateFolder, target, {
|
|
|
140
140
|
renameSync(path.join(target, "gitignore"), path.join(target, ".gitignore"));
|
|
141
141
|
rewriteJson(path.join(target, "package.json"), (json) => {
|
|
142
142
|
json.name = slug;
|
|
143
|
-
// The templates ship Node's loader; another runtime gets its own
|
|
144
|
-
// how `scripts/start.mjs` knows what to run.
|
|
143
|
+
// The templates ship Node's loader and scripts; another runtime gets its own.
|
|
145
144
|
const loader = RUNTIMES[runtimeName].loader;
|
|
146
145
|
if (!(loader in json.dependencies)) {
|
|
147
146
|
const version = json.dependencies["@backtickjs/node-plugin"];
|
|
148
147
|
delete json.dependencies["@backtickjs/node-plugin"];
|
|
149
148
|
json.dependencies = Object.fromEntries(Object.entries({ ...json.dependencies, [loader]: version }).sort());
|
|
150
149
|
}
|
|
150
|
+
if (runtimeName === "bun") {
|
|
151
|
+
const bun = (command) => command
|
|
152
|
+
.replace("node --enable-source-maps --import @backtickjs/node-plugin", `bun --preload ${loader}`)
|
|
153
|
+
.replace("node scripts/", "bun scripts/")
|
|
154
|
+
.replace("npm:server", "bun:server");
|
|
155
|
+
for (const [name, command] of Object.entries(json.scripts)) {
|
|
156
|
+
json.scripts[name] = bun(command);
|
|
157
|
+
}
|
|
158
|
+
const events = json.nodemonConfig.events ?? {};
|
|
159
|
+
for (const [name, command] of Object.entries(events)) {
|
|
160
|
+
events[name] = bun(command);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
151
163
|
});
|
|
152
164
|
if (existsSync(path.join(target, "app.json"))) {
|
|
153
165
|
rewriteJson(path.join(target, "app.json"), (json) => {
|
package/package.json
CHANGED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Seeing the page while you work
|
|
2
|
+
|
|
3
|
+
`npm start` runs the server on http://localhost:3000, which serves the page. The `agent-device` MCP tools, from `.mcp.json`, see and use it in a browser, once `npx agent-device web setup` has installed one; they need Node 24 or later.
|
|
4
|
+
|
|
5
|
+
The server's output is in the terminal `npm start` runs in.
|
|
6
|
+
|
|
7
|
+
Saving a file under `server/` restarts the server: open the page again to see the change. What the server keeps in memory starts over.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -4,25 +4,35 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"start": "node
|
|
7
|
+
"start": "nodemon --exec \"node --enable-source-maps --import @backtickjs/node-plugin server/index.tsx\"",
|
|
8
8
|
"reset-project": "node scripts/reset-project.mjs",
|
|
9
9
|
"typecheck": "backtick-tsc --noEmit",
|
|
10
10
|
"format": "prettier --write --ignore-unknown .",
|
|
11
11
|
"format:check": "prettier --check --ignore-unknown ."
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@backtickjs/bundler": "^0.1.
|
|
15
|
-
"@backtickjs/core": "^0.1.
|
|
16
|
-
"@backtickjs/node-plugin": "^0.1.
|
|
14
|
+
"@backtickjs/bundler": "^0.1.11",
|
|
15
|
+
"@backtickjs/core": "^0.1.11",
|
|
16
|
+
"@backtickjs/node-plugin": "^0.1.11",
|
|
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.
|
|
22
|
-
"@backtickjs/tsc": "^0.1.
|
|
23
|
+
"@backtickjs/prettier-plugin": "^0.1.11",
|
|
24
|
+
"@backtickjs/tsc": "^0.1.11",
|
|
23
25
|
"@types/node": "^25.9.1",
|
|
26
|
+
"agent-device": "^0.21.27",
|
|
27
|
+
"nodemon": "^3.1.14",
|
|
24
28
|
"prettier": "^3.6.2"
|
|
25
29
|
},
|
|
30
|
+
"nodemonConfig": {
|
|
31
|
+
"watch": [
|
|
32
|
+
"server"
|
|
33
|
+
],
|
|
34
|
+
"ext": "ts,tsx"
|
|
35
|
+
},
|
|
26
36
|
"backtick": {
|
|
27
37
|
"plugins": [
|
|
28
38
|
"@backtickjs/react/plugin"
|
|
@@ -35,7 +35,7 @@ export async function Home() {
|
|
|
35
35
|
<section style={styles.step}>
|
|
36
36
|
<h2 style={styles.subtitle}>Step 1: Try it</h2>
|
|
37
37
|
<p style={styles.body}>
|
|
38
|
-
Edit <b>server/Home.tsx</b
|
|
38
|
+
Edit <b>server/Home.tsx</b>, save, and refresh this page.
|
|
39
39
|
</p>
|
|
40
40
|
</section>
|
|
41
41
|
|
|
@@ -56,7 +56,7 @@ export async function Home() {
|
|
|
56
56
|
</p>
|
|
57
57
|
</section>
|
|
58
58
|
|
|
59
|
-
<a style={styles.link} href="https://backtickjs.com/docs">
|
|
59
|
+
<a style={styles.link} href="https://backtickjs.com/docs/">
|
|
60
60
|
Read the docs →
|
|
61
61
|
</a>
|
|
62
62
|
</main>
|
|
@@ -10,23 +10,8 @@ const modules = {
|
|
|
10
10
|
"react-dom/client": "19.2.3",
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
// Development
|
|
14
|
-
|
|
15
|
-
export const development = process.env.NODE_ENV === "development";
|
|
16
|
-
|
|
17
|
-
// In development, what reloads the page whenever the server restarts, as it
|
|
18
|
-
// does when you save.
|
|
19
|
-
const reload = `
|
|
20
|
-
<script type="module">
|
|
21
|
-
let run;
|
|
22
|
-
setInterval(async () => {
|
|
23
|
-
try {
|
|
24
|
-
const next = await (await fetch("/live")).text();
|
|
25
|
-
if (run !== undefined && next !== run) location.reload();
|
|
26
|
-
run = next;
|
|
27
|
-
} catch {}
|
|
28
|
-
}, 1000);
|
|
29
|
-
</script>`;
|
|
13
|
+
// Development unless NODE_ENV says production, as hosts set it for a deploy.
|
|
14
|
+
const development = process.env.NODE_ENV !== "production";
|
|
30
15
|
|
|
31
16
|
// A page: the element, bundled for the browser, and React from a CDN.
|
|
32
17
|
export async function toHtml(element: JSX.Element): Promise<string> {
|
|
@@ -60,7 +45,6 @@ export async function toHtml(element: JSX.Element): Promise<string> {
|
|
|
60
45
|
<body style="margin: 0">
|
|
61
46
|
<div id="app"></div>
|
|
62
47
|
<script type="module" src="data:text/javascript,${encodeURIComponent(code)}"></script>
|
|
63
|
-
${development ? reload : ""}
|
|
64
48
|
</body>
|
|
65
49
|
</html>`;
|
|
66
50
|
}
|
|
@@ -1,33 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { serve } from "@hono/node-server";
|
|
2
|
+
import { Hono } from "hono";
|
|
2
3
|
import { Home } from "./Home.js";
|
|
3
|
-
import {
|
|
4
|
+
import { toHtml } from "./html.js";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
// you save, and the page, seeing a new one at `/live`, reloads.
|
|
7
|
-
const run = crypto.randomUUID();
|
|
6
|
+
const app = new Hono();
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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();
|
|
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
|
+
|
|
12
|
+
// Answered rather than crashed on, so the browser can show why.
|
|
13
|
+
app.onError((error, context) => {
|
|
14
|
+
console.error(error);
|
|
15
|
+
return context.text(String(error), 500);
|
|
29
16
|
});
|
|
30
17
|
|
|
31
|
-
|
|
18
|
+
serve({ fetch: app.fetch, port: 3000 }, () => {
|
|
32
19
|
console.log("Backtick server on http://localhost:3000");
|
|
33
20
|
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Seeing the app while you work
|
|
2
|
+
|
|
3
|
+
`npm start` runs the server on http://localhost:3000, and Expo beside it; the app runs in Expo Go. The `agent-device` MCP tools, from `.mcp.json`, see and use it, once `adb devices` lists the phone: by USB, or Wi-Fi with Wireless debugging paired.
|
|
4
|
+
|
|
5
|
+
The server's output is in the terminal `npm start` runs in.
|
|
6
|
+
|
|
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
|
+
|
|
9
|
+
A change to `client/moduleMap.ts` or the packages makes a new build: the app has to be closed and opened again. Until then, the server answers the open app with `UnknownBuild`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { buildIdFrom } from "@backtickjs/builds/client";
|
|
2
|
+
import type { Bundle } from "@backtickjs/core";
|
|
3
|
+
import { execRoot } from "@backtickjs/core/cjs";
|
|
4
|
+
import Constants from "expo-constants";
|
|
5
|
+
import { StatusBar } from "expo-status-bar";
|
|
6
|
+
import { type ReactNode, Suspense, use, useState } from "react";
|
|
7
|
+
import { ActivityIndicator, Platform, Text } from "react-native";
|
|
8
|
+
import {
|
|
9
|
+
ErrorBoundary,
|
|
10
|
+
type FallbackProps,
|
|
11
|
+
getErrorMessage,
|
|
12
|
+
} from "react-error-boundary";
|
|
13
|
+
import { moduleMap } from "./moduleMap";
|
|
14
|
+
|
|
15
|
+
// Your Backtick server runs on the computer Expo was started from.
|
|
16
|
+
const host = Constants.expoConfig?.hostUri?.split(":")[0] ?? "localhost";
|
|
17
|
+
const server = `http://${host}:3000`;
|
|
18
|
+
|
|
19
|
+
// This build's id, which names the modules it carries, for the server to
|
|
20
|
+
// bundle screens it can run: `@backtickjs/builds/expo`, in app.json, adds it.
|
|
21
|
+
const buildId = buildIdFrom(Constants.expoConfig?.extra, Platform.OS);
|
|
22
|
+
|
|
23
|
+
// The screen at `path` on your server: its bundle, fetched and run with the
|
|
24
|
+
// app's modules. Your fetch to shape: headers, auth, caching.
|
|
25
|
+
async function fetchScreen(path: string): Promise<ReactNode> {
|
|
26
|
+
const response = await fetch(`${server}${path}`, {
|
|
27
|
+
headers: { "x-build-id": buildId },
|
|
28
|
+
});
|
|
29
|
+
if (!response.ok) {
|
|
30
|
+
throw new Error(`${path} answered ${response.status}.`);
|
|
31
|
+
}
|
|
32
|
+
const bundle = (await response.text()) as Bundle<ReactNode>;
|
|
33
|
+
return execRoot(bundle, moduleMap);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export default function App() {
|
|
37
|
+
// Held here, which doesn't suspend, so the request outlives the render
|
|
38
|
+
// that waits for it.
|
|
39
|
+
const [screen] = useState(() => fetchScreen("/"));
|
|
40
|
+
// The whole screen, its layout included, comes from the server: see
|
|
41
|
+
// server/Home.tsx.
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
<ErrorBoundary FallbackComponent={Failed}>
|
|
45
|
+
<Suspense fallback={<ActivityIndicator style={{ flex: 1 }} />}>
|
|
46
|
+
<Screen screen={screen} />
|
|
47
|
+
</Suspense>
|
|
48
|
+
</ErrorBoundary>
|
|
49
|
+
<StatusBar style="auto" />
|
|
50
|
+
</>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// What the server sent, once it has: suspending until then, and throwing,
|
|
55
|
+
// for the boundary, a screen that couldn't be fetched or run.
|
|
56
|
+
function Screen({ screen }: { screen: Promise<ReactNode> }) {
|
|
57
|
+
return use(screen);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Why the screen couldn't be drawn: the server unreachable, or a screen the
|
|
61
|
+
// app can't run.
|
|
62
|
+
function Failed({ error }: FallbackProps) {
|
|
63
|
+
return (
|
|
64
|
+
<Text style={{ margin: 24, marginTop: 96, color: "#b91c1c" }}>
|
|
65
|
+
{getErrorMessage(error) ?? String(error)}
|
|
66
|
+
{"\n\n"}Is your Backtick server running? npm start runs it alongside Expo.
|
|
67
|
+
</Text>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as JSXRuntime from "react/jsx-runtime";
|
|
3
|
+
import * as ReactNative from "react-native";
|
|
4
|
+
|
|
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. Imports and a table,
|
|
7
|
+
// nothing else: the server reads it, without running it, to know what each
|
|
8
|
+
// build of your app carries. A platform that differs gets its own file, as
|
|
9
|
+
// Metro picks files: moduleMap.web.ts, moduleMap.ios.ts.
|
|
10
|
+
export const moduleMap = {
|
|
11
|
+
react: React,
|
|
12
|
+
"react/jsx-runtime": JSXRuntime,
|
|
13
|
+
"react-native": ReactNative,
|
|
14
|
+
};
|
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
"name": "backtick-app-react-native",
|
|
3
3
|
"version": "1.0.0",
|
|
4
4
|
"private": true,
|
|
5
|
-
"main": "index.ts",
|
|
5
|
+
"main": "client/index.ts",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
7
|
+
"server": "nodemon --exec \"node --enable-source-maps --import @backtickjs/node-plugin server/index.tsx\"",
|
|
8
|
+
"start": "concurrently --raw --kill-others npm:server \"expo start\"",
|
|
9
|
+
"android": "concurrently --raw --kill-others npm:server \"expo start --android\"",
|
|
10
|
+
"ios": "concurrently --raw --kill-others npm:server \"expo start --ios\"",
|
|
11
|
+
"web": "concurrently --raw --kill-others npm:server \"expo start --web\"",
|
|
11
12
|
"typecheck": "tsc --noEmit && backtick-tsc -p server --noEmit",
|
|
12
13
|
"test": "node --import ./server/test/setup.mjs --import @backtickjs/node-plugin --test \"server/**/*.test.tsx\"",
|
|
13
14
|
"reset-project": "node scripts/reset-project.mjs",
|
|
@@ -17,34 +18,49 @@
|
|
|
17
18
|
"format:check": "prettier --check --ignore-unknown ."
|
|
18
19
|
},
|
|
19
20
|
"dependencies": {
|
|
20
|
-
"@backtickjs/builds": "^0.1.
|
|
21
|
-
"@backtickjs/bundler": "^0.1.
|
|
22
|
-
"@backtickjs/core": "^0.1.
|
|
23
|
-
"@backtickjs/node-plugin": "^0.1.
|
|
21
|
+
"@backtickjs/builds": "^0.1.11",
|
|
22
|
+
"@backtickjs/bundler": "^0.1.11",
|
|
23
|
+
"@backtickjs/core": "^0.1.11",
|
|
24
|
+
"@backtickjs/node-plugin": "^0.1.11",
|
|
24
25
|
"@backtickjs/react": "^19.2.3",
|
|
25
26
|
"@backtickjs/react-native": "^0.86.3",
|
|
26
|
-
"@
|
|
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",
|
|
34
|
+
"react-error-boundary": "6.1.6",
|
|
32
35
|
"react-native": "0.86.3",
|
|
33
36
|
"react-native-web": "^0.21.0",
|
|
34
37
|
"typescript": "^6.0.3"
|
|
35
38
|
},
|
|
36
39
|
"devDependencies": {
|
|
37
|
-
"@backtickjs/prettier-plugin": "^0.1.
|
|
38
|
-
"@backtickjs/tsc": "^0.1.
|
|
40
|
+
"@backtickjs/prettier-plugin": "^0.1.11",
|
|
41
|
+
"@backtickjs/tsc": "^0.1.11",
|
|
39
42
|
"@testing-library/react": "^16.3.3",
|
|
40
43
|
"@testing-library/user-event": "^14.6.7",
|
|
41
44
|
"@types/node": "^25.9.1",
|
|
42
45
|
"@types/react": "19.2.18",
|
|
46
|
+
"agent-device": "^0.21.27",
|
|
47
|
+
"concurrently": "^10.0.6",
|
|
43
48
|
"global-jsdom": "^30.0.0",
|
|
44
49
|
"jsdom": "^30.0.1",
|
|
50
|
+
"nodemon": "^3.1.14",
|
|
45
51
|
"prettier": "^3.6.2"
|
|
46
52
|
},
|
|
53
|
+
"nodemonConfig": {
|
|
54
|
+
"watch": [
|
|
55
|
+
"server"
|
|
56
|
+
],
|
|
57
|
+
"ext": "ts,tsx",
|
|
58
|
+
"events": {
|
|
59
|
+
"start": "node scripts/reload-expo.mjs"
|
|
60
|
+
}
|
|
61
|
+
},
|
|
47
62
|
"backtick": {
|
|
63
|
+
"clientRoot": "client",
|
|
48
64
|
"plugins": [
|
|
49
65
|
"@backtickjs/react/plugin"
|
|
50
66
|
]
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
}
|
|
@@ -49,8 +49,8 @@ export async function Home() {
|
|
|
49
49
|
<$View style={styles.step}>
|
|
50
50
|
<$Text style={styles.subtitle}>Step 1: Try it</$Text>
|
|
51
51
|
<$Text style={styles.body}>
|
|
52
|
-
Edit <$Text style={styles.code}>server/Home.tsx</$Text
|
|
53
|
-
This screen
|
|
52
|
+
Edit <$Text style={styles.code}>server/Home.tsx</$Text>, save, and
|
|
53
|
+
reload the app. This screen changes without rebuilding it.
|
|
54
54
|
</$Text>
|
|
55
55
|
</$View>
|
|
56
56
|
|
|
@@ -74,7 +74,7 @@ export async function Home() {
|
|
|
74
74
|
|
|
75
75
|
<$Text
|
|
76
76
|
style={styles.link}
|
|
77
|
-
onPress={() => $Linking.openURL("https://backtickjs.com/docs")}
|
|
77
|
+
onPress={() => $Linking.openURL("https://backtickjs.com/docs/")}
|
|
78
78
|
>
|
|
79
79
|
Read the docs →
|
|
80
80
|
</$Text>
|
|
@@ -1,61 +1,24 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
6
|
|
|
7
|
-
|
|
8
|
-
// run, a deploy included, is production. In development, this run of the
|
|
9
|
-
// server: `--watch` starts a new one whenever you save, and the app, seeing a
|
|
10
|
-
// new one at `/live`, draws its screen again.
|
|
11
|
-
const development = process.env.NODE_ENV === "development";
|
|
12
|
-
const run = crypto.randomUUID();
|
|
7
|
+
const app = new Hono();
|
|
13
8
|
|
|
14
|
-
//
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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 />));
|
|
20
15
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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();
|
|
16
|
+
// Answered rather than crashed on, so the app can show why.
|
|
17
|
+
app.onError((error, context) => {
|
|
18
|
+
console.error(error);
|
|
19
|
+
return context.text(String(error), 500);
|
|
57
20
|
});
|
|
58
21
|
|
|
59
|
-
|
|
22
|
+
serve({ fetch: app.fetch, port: 3000 }, () => {
|
|
60
23
|
console.log("Backtick server on http://localhost:3000");
|
|
61
24
|
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import { createBuilds } 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
|
+
|
|
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
|
+
/**
|
|
15
|
+
* `element` as the response to a request from your app, bundled for the
|
|
16
|
+
* modules the requesting build carries.
|
|
17
|
+
*/
|
|
18
|
+
export async function screen(context: Context, element: JSX.Element) {
|
|
19
|
+
const modules = builds.modulesOf(context.req.header("x-build-id") ?? null);
|
|
20
|
+
const bundle = await bundler.build({ input: element, modules });
|
|
21
|
+
const { code } = bundle.generate({ format: "cjs" });
|
|
22
|
+
return context.body(code, 200, { "content-type": "text/javascript" });
|
|
23
|
+
}
|
|
@@ -2,27 +2,27 @@ import { fileURLToPath } from "node:url";
|
|
|
2
2
|
import { createBuilds } from "@backtickjs/builds";
|
|
3
3
|
import { bundler } from "@backtickjs/bundler";
|
|
4
4
|
import type { Spliceable } from "@backtickjs/core";
|
|
5
|
-
import {
|
|
5
|
+
import { execRoot } from "@backtickjs/core/cjs";
|
|
6
6
|
import * as React from "react";
|
|
7
7
|
import * as JSXRuntime from "react/jsx-runtime";
|
|
8
8
|
import * as ReactNativeWeb from "react-native-web";
|
|
9
9
|
|
|
10
|
-
// What the app's web build provides, as moduleMap.ts says, with React
|
|
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
13
|
const modules = createBuilds({
|
|
14
14
|
projectRoot: fileURLToPath(new URL("../..", import.meta.url)),
|
|
15
15
|
}).installed("web");
|
|
16
16
|
const moduleMap = {
|
|
17
|
-
react:
|
|
18
|
-
"react/jsx-runtime":
|
|
19
|
-
"react-native":
|
|
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,
|
|
23
23
|
// for Testing Library to draw.
|
|
24
|
-
export async function drawScreen(input:
|
|
24
|
+
export async function drawScreen<T extends Spliceable>(input: T) {
|
|
25
25
|
const bundle = await bundler.build({ input, modules });
|
|
26
26
|
const { code } = bundle.generate({ format: "cjs" });
|
|
27
|
-
return
|
|
27
|
+
return execRoot(code, moduleMap);
|
|
28
28
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Seeing the page while you work
|
|
2
|
+
|
|
3
|
+
`npm start` runs the server on http://localhost:3000, which serves the page. The `agent-device` MCP tools, from `.mcp.json`, see and use it in a browser, once `npx agent-device web setup` has installed one; they need Node 24 or later.
|
|
4
|
+
|
|
5
|
+
The server's output is in the terminal `npm start` runs in.
|
|
6
|
+
|
|
7
|
+
Saving a file under `server/` restarts the server: open the page again to see the change. What the server keeps in memory starts over.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
|
@@ -4,25 +4,35 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"start": "node
|
|
7
|
+
"start": "nodemon --exec \"node --enable-source-maps --import @backtickjs/node-plugin server/index.tsx\"",
|
|
8
8
|
"reset-project": "node scripts/reset-project.mjs",
|
|
9
9
|
"typecheck": "backtick-tsc --noEmit",
|
|
10
10
|
"format": "prettier --write --ignore-unknown .",
|
|
11
11
|
"format:check": "prettier --check --ignore-unknown ."
|
|
12
12
|
},
|
|
13
13
|
"dependencies": {
|
|
14
|
-
"@backtickjs/bundler": "^0.1.
|
|
15
|
-
"@backtickjs/core": "^0.1.
|
|
16
|
-
"@backtickjs/node-plugin": "^0.1.
|
|
14
|
+
"@backtickjs/bundler": "^0.1.11",
|
|
15
|
+
"@backtickjs/core": "^0.1.11",
|
|
16
|
+
"@backtickjs/node-plugin": "^0.1.11",
|
|
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.
|
|
22
|
-
"@backtickjs/tsc": "^0.1.
|
|
23
|
+
"@backtickjs/prettier-plugin": "^0.1.11",
|
|
24
|
+
"@backtickjs/tsc": "^0.1.11",
|
|
23
25
|
"@types/node": "^25.9.1",
|
|
26
|
+
"agent-device": "^0.21.27",
|
|
27
|
+
"nodemon": "^3.1.14",
|
|
24
28
|
"prettier": "^3.6.2"
|
|
25
29
|
},
|
|
30
|
+
"nodemonConfig": {
|
|
31
|
+
"watch": [
|
|
32
|
+
"server"
|
|
33
|
+
],
|
|
34
|
+
"ext": "ts,tsx"
|
|
35
|
+
},
|
|
26
36
|
"backtick": {
|
|
27
37
|
"plugins": [
|
|
28
38
|
"@backtickjs/solid-js/plugin"
|
|
@@ -35,7 +35,7 @@ export async function Home() {
|
|
|
35
35
|
<section style={styles.step}>
|
|
36
36
|
<h2 style={styles.subtitle}>Step 1: Try it</h2>
|
|
37
37
|
<p style={styles.body}>
|
|
38
|
-
Edit <b>server/Home.tsx</b
|
|
38
|
+
Edit <b>server/Home.tsx</b>, save, and refresh this page.
|
|
39
39
|
</p>
|
|
40
40
|
</section>
|
|
41
41
|
|
|
@@ -56,7 +56,7 @@ export async function Home() {
|
|
|
56
56
|
</p>
|
|
57
57
|
</section>
|
|
58
58
|
|
|
59
|
-
<a style={styles.link} href="https://backtickjs.com/docs">
|
|
59
|
+
<a style={styles.link} href="https://backtickjs.com/docs/">
|
|
60
60
|
Read the docs →
|
|
61
61
|
</a>
|
|
62
62
|
</main>
|
|
@@ -10,23 +10,8 @@ const modules = {
|
|
|
10
10
|
"solid-js/web": "1.9.14",
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
// Development
|
|
14
|
-
|
|
15
|
-
export const development = process.env.NODE_ENV === "development";
|
|
16
|
-
|
|
17
|
-
// In development, what reloads the page whenever the server restarts, as it
|
|
18
|
-
// does when you save.
|
|
19
|
-
const reload = `
|
|
20
|
-
<script type="module">
|
|
21
|
-
let run;
|
|
22
|
-
setInterval(async () => {
|
|
23
|
-
try {
|
|
24
|
-
const next = await (await fetch("/live")).text();
|
|
25
|
-
if (run !== undefined && next !== run) location.reload();
|
|
26
|
-
run = next;
|
|
27
|
-
} catch {}
|
|
28
|
-
}, 1000);
|
|
29
|
-
</script>`;
|
|
13
|
+
// Development unless NODE_ENV says production, as hosts set it for a deploy.
|
|
14
|
+
const development = process.env.NODE_ENV !== "production";
|
|
30
15
|
|
|
31
16
|
// A page: the element, bundled for the browser, and Solid from a CDN.
|
|
32
17
|
export async function toHtml(element: JSX.Element): Promise<string> {
|
|
@@ -60,7 +45,6 @@ export async function toHtml(element: JSX.Element): Promise<string> {
|
|
|
60
45
|
<body style="margin: 0">
|
|
61
46
|
<div id="app"></div>
|
|
62
47
|
<script type="module" src="data:text/javascript,${encodeURIComponent(code)}"></script>
|
|
63
|
-
${development ? reload : ""}
|
|
64
48
|
</body>
|
|
65
49
|
</html>`;
|
|
66
50
|
}
|
|
@@ -1,33 +1,20 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { serve } from "@hono/node-server";
|
|
2
|
+
import { Hono } from "hono";
|
|
2
3
|
import { Home } from "./Home.js";
|
|
3
|
-
import {
|
|
4
|
+
import { toHtml } from "./html.js";
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
// you save, and the page, seeing a new one at `/live`, reloads.
|
|
7
|
-
const run = crypto.randomUUID();
|
|
6
|
+
const app = new Hono();
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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();
|
|
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
|
+
|
|
12
|
+
// Answered rather than crashed on, so the browser can show why.
|
|
13
|
+
app.onError((error, context) => {
|
|
14
|
+
console.error(error);
|
|
15
|
+
return context.text(String(error), 500);
|
|
29
16
|
});
|
|
30
17
|
|
|
31
|
-
|
|
18
|
+
serve({ fetch: app.fetch, port: 3000 }, () => {
|
|
32
19
|
console.log("Backtick server on http://localhost:3000");
|
|
33
20
|
});
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
// Starts your Backtick server in development: it restarts whenever a file
|
|
2
|
-
// under `server/` changes, and the page reloads with it. Any other way of
|
|
3
|
-
// running the server, a deploy included, is production.
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
|
-
import { readFileSync } from "node:fs";
|
|
6
|
-
|
|
7
|
-
// The runtime is the one package.json has a Backtick loader for: Bun's, or
|
|
8
|
-
// else Node's.
|
|
9
|
-
const { dependencies } = JSON.parse(readFileSync("package.json", "utf8"));
|
|
10
|
-
const [runtime, ...loader] =
|
|
11
|
-
"@backtickjs/bun-plugin" in dependencies
|
|
12
|
-
? ["bun", "--preload", "@backtickjs/bun-plugin"]
|
|
13
|
-
: ["node", "--enable-source-maps", "--import", "@backtickjs/node-plugin"];
|
|
14
|
-
|
|
15
|
-
const server = spawn(runtime, ["--watch", ...loader, "server/index.tsx"], {
|
|
16
|
-
stdio: "inherit",
|
|
17
|
-
env: { ...process.env, NODE_ENV: process.env.NODE_ENV ?? "development" },
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
server.on("exit", (code) => process.exit(code ?? 0));
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { buildIdFrom, evaluate } from "@backtickjs/react-native-client";
|
|
2
|
-
import Constants from "expo-constants";
|
|
3
|
-
import { StatusBar } from "expo-status-bar";
|
|
4
|
-
import {
|
|
5
|
-
Component,
|
|
6
|
-
type ReactNode,
|
|
7
|
-
Suspense,
|
|
8
|
-
use,
|
|
9
|
-
useEffect,
|
|
10
|
-
useState,
|
|
11
|
-
} from "react";
|
|
12
|
-
import { ActivityIndicator, Platform, Text } from "react-native";
|
|
13
|
-
import { moduleMap } from "./moduleMap";
|
|
14
|
-
|
|
15
|
-
// Your Backtick server runs on the computer Expo was started from.
|
|
16
|
-
const host = Constants.expoConfig?.hostUri?.split(":")[0] ?? "localhost";
|
|
17
|
-
const server = `http://${host}:3000`;
|
|
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
|
-
|
|
23
|
-
// The screen at `path` on your server: its bundle, fetched and run with the
|
|
24
|
-
// app's modules. Your fetch to shape: headers, auth, caching.
|
|
25
|
-
async function fetchScreen(path: string): Promise<ReactNode> {
|
|
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
|
-
}
|
|
34
|
-
if (!response.ok) {
|
|
35
|
-
throw new Error(`${path} answered ${response.status}.`);
|
|
36
|
-
}
|
|
37
|
-
return evaluate(await response.text(), moduleMap) as ReactNode;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export default function App() {
|
|
41
|
-
// Held here, which doesn't suspend, so the request outlives the render
|
|
42
|
-
// that waits for it. Each request is numbered, so a new one also clears an
|
|
43
|
-
// error the last one showed.
|
|
44
|
-
const [request, setRequest] = useState(() => ({
|
|
45
|
-
number: 0,
|
|
46
|
-
screen: fetchScreen("/home"),
|
|
47
|
-
}));
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
// In development, the screen is fetched again each time the server
|
|
50
|
-
// restarts, so a change to server/ shows as soon as you save.
|
|
51
|
-
if (__DEV__) {
|
|
52
|
-
return onServerRestart(() =>
|
|
53
|
-
setRequest(({ number }) => ({
|
|
54
|
-
number: number + 1,
|
|
55
|
-
screen: fetchScreen("/home"),
|
|
56
|
-
})),
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
}, []);
|
|
60
|
-
// The whole screen, its layout included, comes from the server: see
|
|
61
|
-
// server/Home.tsx.
|
|
62
|
-
return (
|
|
63
|
-
<>
|
|
64
|
-
<Boundary key={request.number}>
|
|
65
|
-
<Suspense fallback={<ActivityIndicator style={{ flex: 1 }} />}>
|
|
66
|
-
<Screen screen={request.screen} />
|
|
67
|
-
</Suspense>
|
|
68
|
-
</Boundary>
|
|
69
|
-
<StatusBar style="auto" />
|
|
70
|
-
</>
|
|
71
|
-
);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// What the server sent, once it has: suspending until then, and throwing,
|
|
75
|
-
// for the boundary, a screen that couldn't be fetched or run.
|
|
76
|
-
function Screen({ screen }: { screen: Promise<ReactNode> }) {
|
|
77
|
-
return use(screen);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Calls `restarted` when the server starts a new run: it names each run at
|
|
81
|
-
// `/live`, and a new name is a new run.
|
|
82
|
-
function onServerRestart(restarted: () => void): () => void {
|
|
83
|
-
let run: string | undefined;
|
|
84
|
-
const timer = setInterval(async () => {
|
|
85
|
-
try {
|
|
86
|
-
const next = await (await fetch(`${server}/live`)).text();
|
|
87
|
-
if (run !== undefined && next !== run) {
|
|
88
|
-
restarted();
|
|
89
|
-
}
|
|
90
|
-
run = next;
|
|
91
|
-
} catch {
|
|
92
|
-
// Restarting: the next tick finds the new run.
|
|
93
|
-
}
|
|
94
|
-
}, 1000);
|
|
95
|
-
return () => clearInterval(timer);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Why the screen couldn't be drawn: the server unreachable, or a screen the
|
|
99
|
-
// app can't run.
|
|
100
|
-
class Boundary extends Component<{ children: ReactNode }, { error: unknown }> {
|
|
101
|
-
state = { error: null as unknown };
|
|
102
|
-
|
|
103
|
-
static getDerivedStateFromError(error: unknown) {
|
|
104
|
-
return { error };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
render() {
|
|
108
|
-
if (this.state.error === null) {
|
|
109
|
-
return this.props.children;
|
|
110
|
-
}
|
|
111
|
-
return (
|
|
112
|
-
<Text style={{ margin: 24, marginTop: 96, color: "#b91c1c" }}>
|
|
113
|
-
{String((this.state.error as Error).message ?? this.state.error)}
|
|
114
|
-
{"\n\n"}Is your Backtick server running? npm start runs it alongside
|
|
115
|
-
Expo.
|
|
116
|
-
</Text>
|
|
117
|
-
);
|
|
118
|
-
}
|
|
119
|
-
}
|
|
@@ -1,10 +0,0 @@
|
|
|
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;
|
|
@@ -1,12 +0,0 @@
|
|
|
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;
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
// Starts your Backtick server in development, then Expo, both in this terminal:
|
|
2
|
-
// Expo keeps its keys and QR code, and the server restarts whenever a file
|
|
3
|
-
// under `server/` changes. Arguments are Expo's, as `npm run ios` passes
|
|
4
|
-
// `--ios`.
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
6
|
-
import { readFileSync } from "node:fs";
|
|
7
|
-
import { networkInterfaces } from "node:os";
|
|
8
|
-
import { setTimeout as sleep } from "node:timers/promises";
|
|
9
|
-
|
|
10
|
-
// The runtime is the one package.json has a Backtick loader for: Bun's, or
|
|
11
|
-
// else Node's.
|
|
12
|
-
const { dependencies } = JSON.parse(readFileSync("package.json", "utf8"));
|
|
13
|
-
const [runtime, ...loader] =
|
|
14
|
-
"@backtickjs/bun-plugin" in dependencies
|
|
15
|
-
? ["bun", "--preload", "@backtickjs/bun-plugin"]
|
|
16
|
-
: ["node", "--enable-source-maps", "--import", "@backtickjs/node-plugin"];
|
|
17
|
-
|
|
18
|
-
const server = spawn(runtime, ["--watch", ...loader, "server/index.tsx"], {
|
|
19
|
-
stdio: "inherit",
|
|
20
|
-
env: { ...process.env, NODE_ENV: process.env.NODE_ENV ?? "development" },
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
// `bunx` where Bun runs this script, as it does where Node isn't installed.
|
|
24
|
-
const npx = process.versions.bun ? "bunx" : "npx";
|
|
25
|
-
const expo = spawn(npx, ["expo", "start", ...process.argv.slice(2)], {
|
|
26
|
-
stdio: "inherit",
|
|
27
|
-
shell: process.platform === "win32",
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
expo.on("exit", (code) => {
|
|
31
|
-
server.kill();
|
|
32
|
-
process.exit(code ?? 0);
|
|
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,20 +0,0 @@
|
|
|
1
|
-
// Starts your Backtick server in development: it restarts whenever a file
|
|
2
|
-
// under `server/` changes, and the page reloads with it. Any other way of
|
|
3
|
-
// running the server, a deploy included, is production.
|
|
4
|
-
import { spawn } from "node:child_process";
|
|
5
|
-
import { readFileSync } from "node:fs";
|
|
6
|
-
|
|
7
|
-
// The runtime is the one package.json has a Backtick loader for: Bun's, or
|
|
8
|
-
// else Node's.
|
|
9
|
-
const { dependencies } = JSON.parse(readFileSync("package.json", "utf8"));
|
|
10
|
-
const [runtime, ...loader] =
|
|
11
|
-
"@backtickjs/bun-plugin" in dependencies
|
|
12
|
-
? ["bun", "--preload", "@backtickjs/bun-plugin"]
|
|
13
|
-
: ["node", "--enable-source-maps", "--import", "@backtickjs/node-plugin"];
|
|
14
|
-
|
|
15
|
-
const server = spawn(runtime, ["--watch", ...loader, "server/index.tsx"], {
|
|
16
|
-
stdio: "inherit",
|
|
17
|
-
env: { ...process.env, NODE_ENV: process.env.NODE_ENV ?? "development" },
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
server.on("exit", (code) => process.exit(code ?? 0));
|
|
File without changes
|