create-backtick-app 0.0.0-stage → 0.1.1
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/LICENSE +21 -0
- package/README.md +84 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +150 -0
- package/package.json +56 -3
- package/templates/react/.prettierignore +8 -0
- package/templates/react/.prettierrc +3 -0
- package/templates/react/.vscode/extensions.json +6 -0
- package/templates/react/gitignore +16 -0
- package/templates/react/package.json +31 -0
- package/templates/react/scripts/reset-project.mjs +21 -0
- package/templates/react/scripts/start.mjs +20 -0
- package/templates/react/server/HelloWave.tsx +30 -0
- package/templates/react/server/Home.tsx +65 -0
- package/templates/react/server/html.ts +62 -0
- package/templates/react/server/index.tsx +33 -0
- package/templates/react/tsconfig.json +18 -0
- package/templates/react-native/.prettierignore +16 -0
- package/templates/react-native/.prettierrc +3 -0
- package/templates/react-native/.vscode/extensions.json +6 -0
- package/templates/react-native/App.tsx +117 -0
- package/templates/react-native/app.json +9 -0
- package/templates/react-native/gitignore +41 -0
- package/templates/react-native/index.ts +8 -0
- package/templates/react-native/package.json +44 -0
- package/templates/react-native/scripts/reset-project.mjs +22 -0
- package/templates/react-native/scripts/start.mjs +31 -0
- package/templates/react-native/server/HelloWave.tsx +51 -0
- package/templates/react-native/server/Home.tsx +84 -0
- package/templates/react-native/server/index.tsx +50 -0
- package/templates/react-native/server/tsconfig.json +17 -0
- package/templates/react-native/tsconfig.json +7 -0
- package/templates/solid-js/.prettierignore +8 -0
- package/templates/solid-js/.prettierrc +3 -0
- package/templates/solid-js/.vscode/extensions.json +6 -0
- package/templates/solid-js/gitignore +16 -0
- package/templates/solid-js/package.json +31 -0
- package/templates/solid-js/scripts/reset-project.mjs +21 -0
- package/templates/solid-js/scripts/start.mjs +20 -0
- package/templates/solid-js/server/HelloWave.tsx +34 -0
- package/templates/solid-js/server/Home.tsx +65 -0
- package/templates/solid-js/server/html.ts +62 -0
- package/templates/solid-js/server/index.tsx +33 -0
- package/templates/solid-js/tsconfig.json +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jean-François Bisson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,85 @@
|
|
|
1
|
-
#
|
|
1
|
+
# create-backtick-app
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx create-backtick-app@latest
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Or with your package manager's `create`:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
npm create backtick-app@latest
|
|
15
|
+
pnpm create backtick-app
|
|
16
|
+
yarn create backtick-app
|
|
17
|
+
bun create backtick-app
|
|
18
|
+
```
|
|
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.
|
|
27
|
+
|
|
28
|
+
## Options
|
|
29
|
+
|
|
30
|
+
```sh
|
|
31
|
+
npx create-backtick-app@latest [name] [options]
|
|
32
|
+
```
|
|
33
|
+
|
|
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. |
|
|
41
|
+
|
|
42
|
+
With `npm create`, pass options after `--`: `npm create backtick-app@latest my-app -- -t react`.
|
|
43
|
+
|
|
44
|
+
## Templates
|
|
45
|
+
|
|
46
|
+
Every template has:
|
|
47
|
+
|
|
48
|
+
- `server/Home.tsx`: the welcome screen, a server component. It reads values on the server and splices them into a client script.
|
|
49
|
+
- `server/HelloWave.tsx`: a client component, a waving hand animated where the screen runs. Tap or click it.
|
|
50
|
+
- `server/index.tsx`: the Backtick server on http://localhost:3000.
|
|
51
|
+
- `scripts/start.mjs`: starts the server in development, restarting it whenever you save. The client polls `/live`, which exists only in development, and redraws when the server restarts.
|
|
52
|
+
- `npm run reset-project`: replaces `server/Home.tsx` with a blank screen and removes `HelloWave.tsx`.
|
|
53
|
+
- `npm run typecheck`: runs `backtick-tsc` over the server, scripts and splices included. In `react-native`, it also runs `tsc` over the Expo app.
|
|
54
|
+
- Prettier with `@backtickjs/prettier-plugin`, which formats client scripts too, and VS Code recommendations for the Backtick and Prettier extensions.
|
|
55
|
+
|
|
56
|
+
### `react-native`
|
|
57
|
+
|
|
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
|
+
|
|
60
|
+
```sh
|
|
61
|
+
npm run ios # or android, or web
|
|
62
|
+
```
|
|
63
|
+
|
|
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
|
+
|
|
66
|
+
### `react` and `solid-js`
|
|
67
|
+
|
|
68
|
+
A web page: the server bundles `server/Home.tsx` into an HTML page at `/`, with the framework loaded from a CDN by an import map.
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
npm run start
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Then open http://localhost:3000. Edit `server/Home.tsx` and save to see the page change.
|
|
75
|
+
|
|
76
|
+
## Related
|
|
77
|
+
|
|
78
|
+
- [`@backtickjs/core`](https://github.com/backtickjs/backtick/tree/main/packages/core): the `cs` tag
|
|
79
|
+
- [`@backtickjs/bundler`](https://github.com/backtickjs/backtick/tree/main/packages/bundler): runs server components and writes the bundle
|
|
80
|
+
- [`@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)
|
|
82
|
+
|
|
83
|
+
## License
|
|
84
|
+
|
|
85
|
+
MIT
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import { cpSync, existsSync, readdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { parseArgs } from "node:util";
|
|
7
|
+
import prompts from "prompts";
|
|
8
|
+
const TEMPLATES = {
|
|
9
|
+
"react-native": {
|
|
10
|
+
title: "React Native",
|
|
11
|
+
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.",
|
|
14
|
+
},
|
|
15
|
+
react: {
|
|
16
|
+
title: "React",
|
|
17
|
+
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.",
|
|
20
|
+
},
|
|
21
|
+
"solid-js": {
|
|
22
|
+
title: "solid-js",
|
|
23
|
+
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.",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
const { values, positionals } = parseArgs({
|
|
29
|
+
allowPositionals: true,
|
|
30
|
+
options: {
|
|
31
|
+
template: { type: "string", short: "t" },
|
|
32
|
+
runtime: { type: "string", short: "r" },
|
|
33
|
+
yes: { type: "boolean", short: "y" },
|
|
34
|
+
"no-install": { type: "boolean" },
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
if (values.template !== undefined && !(values.template in TEMPLATES)) {
|
|
38
|
+
console.error(`There's no template named ${values.template}. Pick one of: ${Object.keys(TEMPLATES).join(", ")}.`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
// What runs your Backtick server, and the loader that compiles it for that
|
|
42
|
+
// runtime.
|
|
43
|
+
const RUNTIMES = {
|
|
44
|
+
node: { title: "Node", loader: "@backtickjs/node-plugin" },
|
|
45
|
+
bun: { title: "Bun", loader: "@backtickjs/bun-plugin" },
|
|
46
|
+
};
|
|
47
|
+
if (values.runtime !== undefined && !(values.runtime in RUNTIMES)) {
|
|
48
|
+
console.error(`There's no runtime named ${values.runtime}. Pick one of: ${Object.keys(RUNTIMES).join(", ")}.`);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
// The package manager this was run with, as `npm create` and its peers say,
|
|
52
|
+
// which is also the runtime to suggest: `bun create` suggests Bun.
|
|
53
|
+
const manager = (process.env.npm_config_user_agent ?? "npm").split("/")[0];
|
|
54
|
+
const run = ["npm", "pnpm", "yarn", "bun"].includes(manager) ? manager : "npm";
|
|
55
|
+
const suggestedRuntime = run === "bun" ? "bun" : "node";
|
|
56
|
+
// Asked only what the command line didn't say, as `create-expo-app` asks.
|
|
57
|
+
const answers = await prompts([
|
|
58
|
+
{
|
|
59
|
+
type: positionals[0] !== undefined || values.yes ? null : "text",
|
|
60
|
+
name: "name",
|
|
61
|
+
message: "What is your app named?",
|
|
62
|
+
initial: "my-app",
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
type: values.template !== undefined || values.yes ? null : "select",
|
|
66
|
+
name: "template",
|
|
67
|
+
message: "Which framework?",
|
|
68
|
+
choices: Object.entries(TEMPLATES).map(([value, template]) => ({
|
|
69
|
+
title: template.title,
|
|
70
|
+
description: template.description,
|
|
71
|
+
value,
|
|
72
|
+
})),
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
type: values.runtime !== undefined || values.yes ? null : "select",
|
|
76
|
+
name: "runtime",
|
|
77
|
+
message: "Which runtime?",
|
|
78
|
+
choices: Object.entries(RUNTIMES).map(([value, runtime]) => ({
|
|
79
|
+
title: runtime.title,
|
|
80
|
+
value,
|
|
81
|
+
})),
|
|
82
|
+
initial: Object.keys(RUNTIMES).indexOf(suggestedRuntime),
|
|
83
|
+
},
|
|
84
|
+
], { onCancel: () => process.exit(1) });
|
|
85
|
+
const name = positionals[0] ?? answers.name ?? "my-app";
|
|
86
|
+
const templateName = values.template ?? answers.template ?? "react-native";
|
|
87
|
+
const template = TEMPLATES[templateName];
|
|
88
|
+
const runtimeName = values.runtime ?? answers.runtime ?? suggestedRuntime;
|
|
89
|
+
const target = path.resolve(name);
|
|
90
|
+
const slug = path.basename(target);
|
|
91
|
+
if (existsSync(target) && readdirSync(target).length > 0) {
|
|
92
|
+
console.error(`The directory ${slug} has files that might be overwritten. Try using a new directory name.`);
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
// The templates beside this file once built: `dist/../templates`.
|
|
96
|
+
const templateFolder = fileURLToPath(new URL(`../templates/${templateName}/`, import.meta.url));
|
|
97
|
+
cpSync(templateFolder, target, {
|
|
98
|
+
recursive: true,
|
|
99
|
+
// What running a template inside the Backtick repository leaves behind,
|
|
100
|
+
// matched within the template: the CLI itself runs from a `node_modules`
|
|
101
|
+
// when installed, and a path from the root would match every file.
|
|
102
|
+
filter: (source) => !/(^|[\\/])(node_modules|\.expo|\.turbo)([\\/]|$)/.test(path.relative(templateFolder, source)),
|
|
103
|
+
});
|
|
104
|
+
// npm drops a `.gitignore` from a published package, so it ships unnamed.
|
|
105
|
+
renameSync(path.join(target, "gitignore"), path.join(target, ".gitignore"));
|
|
106
|
+
rewriteJson(path.join(target, "package.json"), (json) => {
|
|
107
|
+
json.name = slug;
|
|
108
|
+
// The templates ship Node's loader; another runtime gets its own, which is
|
|
109
|
+
// how `scripts/start.mjs` knows what to run.
|
|
110
|
+
const loader = RUNTIMES[runtimeName].loader;
|
|
111
|
+
if (!(loader in json.dependencies)) {
|
|
112
|
+
const version = json.dependencies["@backtickjs/node-plugin"];
|
|
113
|
+
delete json.dependencies["@backtickjs/node-plugin"];
|
|
114
|
+
json.dependencies = Object.fromEntries(Object.entries({ ...json.dependencies, [loader]: version }).sort());
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
if (existsSync(path.join(target, "app.json"))) {
|
|
118
|
+
rewriteJson(path.join(target, "app.json"), (json) => {
|
|
119
|
+
json.expo.name = slug;
|
|
120
|
+
json.expo.slug = slug;
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
console.log("✔ Created project files.");
|
|
124
|
+
if (!values["no-install"]) {
|
|
125
|
+
console.log(`> ${run} install`);
|
|
126
|
+
const { status } = spawnSync(run, ["install"], {
|
|
127
|
+
cwd: target,
|
|
128
|
+
stdio: "inherit",
|
|
129
|
+
shell: process.platform === "win32",
|
|
130
|
+
});
|
|
131
|
+
if (status !== 0) {
|
|
132
|
+
console.error(`${run} install failed. Run it again in ${slug}.`);
|
|
133
|
+
process.exit(status ?? 1);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
console.log(`
|
|
137
|
+
✅ Your project is ready!
|
|
138
|
+
|
|
139
|
+
To run your project, navigate to the directory and run one of the following ${run} commands.
|
|
140
|
+
|
|
141
|
+
- cd ${slug}
|
|
142
|
+
${template.scripts.map((script) => `- ${run} run ${script}`).join("\n")}
|
|
143
|
+
|
|
144
|
+
${template.then}
|
|
145
|
+
`);
|
|
146
|
+
function rewriteJson(file, change) {
|
|
147
|
+
const json = JSON.parse(readFileSync(file, "utf8"));
|
|
148
|
+
change(json);
|
|
149
|
+
writeFileSync(file, `${JSON.stringify(json, null, 2)}\n`);
|
|
150
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,59 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-backtick-app",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Creates a Backtick app: React Native, React or Solid, with screens from your server on Node or Bun",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"backtick",
|
|
7
|
+
"react-native",
|
|
8
|
+
"server-driven-ui",
|
|
9
|
+
"server-components",
|
|
10
|
+
"create",
|
|
11
|
+
"starter",
|
|
12
|
+
"template",
|
|
13
|
+
"expo",
|
|
14
|
+
"react",
|
|
15
|
+
"solid"
|
|
16
|
+
],
|
|
17
|
+
"homepage": "https://backtickjs.com",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/backtickjs/backtick/issues"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/backtickjs/backtick.git",
|
|
24
|
+
"directory": "packages/create-backtick-app"
|
|
25
|
+
},
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": "Jean-François Bisson <jf@backtickjs.com>",
|
|
28
|
+
"type": "module",
|
|
29
|
+
"bin": {
|
|
30
|
+
"create-backtick-app": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"templates",
|
|
35
|
+
"!templates/*/node_modules",
|
|
36
|
+
"!templates/*/.expo",
|
|
37
|
+
"!templates/*/.turbo"
|
|
38
|
+
],
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=22.15"
|
|
41
|
+
},
|
|
42
|
+
"devDependencies": {
|
|
43
|
+
"@types/node": "^25.9.1",
|
|
44
|
+
"@types/prompts": "^2.4.9",
|
|
45
|
+
"prettier": "^3.6.2",
|
|
46
|
+
"typescript": "^6.0.3"
|
|
47
|
+
},
|
|
48
|
+
"dependencies": {
|
|
49
|
+
"prompts": "^2.4.2"
|
|
50
|
+
},
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc -b",
|
|
53
|
+
"dev": "tsc -b --watch",
|
|
54
|
+
"format": "prettier --write --ignore-unknown .",
|
|
55
|
+
"format:check": "prettier --check --ignore-unknown .",
|
|
56
|
+
"test": "node --test \"test/**/*.test.ts\"",
|
|
57
|
+
"clean": "rm -rf node_modules dist .turbo"
|
|
58
|
+
}
|
|
6
59
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "backtick-app-react",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node scripts/start.mjs",
|
|
8
|
+
"reset-project": "node scripts/reset-project.mjs",
|
|
9
|
+
"typecheck": "backtick-tsc --noEmit",
|
|
10
|
+
"format": "prettier --write --ignore-unknown .",
|
|
11
|
+
"format:check": "prettier --check --ignore-unknown ."
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@backtickjs/bundler": "^0.1.1",
|
|
15
|
+
"@backtickjs/core": "^0.1.1",
|
|
16
|
+
"@backtickjs/node-plugin": "^0.1.1",
|
|
17
|
+
"@backtickjs/react": "^19.2.3",
|
|
18
|
+
"typescript": "^6.0.3"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@backtickjs/prettier-plugin": "^0.1.1",
|
|
22
|
+
"@backtickjs/tsc": "^0.1.1",
|
|
23
|
+
"@types/node": "^25.9.1",
|
|
24
|
+
"prettier": "^3.6.2"
|
|
25
|
+
},
|
|
26
|
+
"backtick": {
|
|
27
|
+
"plugins": [
|
|
28
|
+
"@backtickjs/react/plugin"
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Replaces the welcome page with a blank one, so you start fresh.
|
|
2
|
+
import { rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
writeFileSync(
|
|
5
|
+
"server/Home.tsx",
|
|
6
|
+
`import { cs } from "@backtickjs/core";
|
|
7
|
+
|
|
8
|
+
export async function Home() {
|
|
9
|
+
return cs\`(
|
|
10
|
+
<main style={{ padding: 24, fontFamily: "system-ui, sans-serif" }}>
|
|
11
|
+
<p>Edit server/Home.tsx to edit this page.</p>
|
|
12
|
+
</main>
|
|
13
|
+
)\`;
|
|
14
|
+
}
|
|
15
|
+
`,
|
|
16
|
+
);
|
|
17
|
+
rmSync("server/HelloWave.tsx", { force: true });
|
|
18
|
+
|
|
19
|
+
console.log(
|
|
20
|
+
"✅ Project reset. server/Home.tsx is now a blank page, and the welcome example is gone.",
|
|
21
|
+
);
|
|
@@ -0,0 +1,20 @@
|
|
|
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));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { cs } from "@backtickjs/core";
|
|
2
|
+
import { useEffect, useRef } from "@backtickjs/react";
|
|
3
|
+
|
|
4
|
+
// A client component: it runs in the browser, so its animation does too. It
|
|
5
|
+
// waves when it appears, and again when you click it.
|
|
6
|
+
export const HelloWave = cs`() => {
|
|
7
|
+
const hand = $useRef<HTMLSpanElement>(null);
|
|
8
|
+
const wave = () => {
|
|
9
|
+
hand.current?.animate(
|
|
10
|
+
[
|
|
11
|
+
{ transform: "rotate(0deg)" },
|
|
12
|
+
{ transform: "rotate(25deg)" },
|
|
13
|
+
{ transform: "rotate(0deg)" },
|
|
14
|
+
],
|
|
15
|
+
{ duration: 300, iterations: 4 },
|
|
16
|
+
);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
$useEffect(wave, []);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<span
|
|
23
|
+
ref={hand}
|
|
24
|
+
onClick={wave}
|
|
25
|
+
style={{ display: "inline-block", cursor: "pointer", fontSize: 28 }}
|
|
26
|
+
>
|
|
27
|
+
👋
|
|
28
|
+
</span>
|
|
29
|
+
);
|
|
30
|
+
}`;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { cs } from "@backtickjs/core";
|
|
2
|
+
import { HelloWave } from "./HelloWave.js";
|
|
3
|
+
|
|
4
|
+
// A server component: it runs on your server for every request, and hands the
|
|
5
|
+
// browser its data and its client components.
|
|
6
|
+
export async function Home() {
|
|
7
|
+
// Read here, on your server, and sent to the browser as plain values.
|
|
8
|
+
const node = process.version;
|
|
9
|
+
const assembledAt = new Date().toLocaleTimeString();
|
|
10
|
+
|
|
11
|
+
return cs`{
|
|
12
|
+
const styles = {
|
|
13
|
+
page: {
|
|
14
|
+
maxWidth: 640,
|
|
15
|
+
padding: 24,
|
|
16
|
+
fontFamily: "system-ui, sans-serif",
|
|
17
|
+
display: "grid",
|
|
18
|
+
gap: 24,
|
|
19
|
+
},
|
|
20
|
+
titleRow: { display: "flex", alignItems: "center", gap: 8 },
|
|
21
|
+
title: { margin: 0, fontSize: 32, lineHeight: 1 },
|
|
22
|
+
step: { display: "grid", gap: 8 },
|
|
23
|
+
subtitle: { margin: 0, fontSize: 20 },
|
|
24
|
+
body: { margin: 0, fontSize: 16, lineHeight: 1.5 },
|
|
25
|
+
link: { fontSize: 16, color: "#0a7ea4" },
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<main style={styles.page}>
|
|
30
|
+
<div style={styles.titleRow}>
|
|
31
|
+
<h1 style={styles.title}>Welcome!</h1>
|
|
32
|
+
<$HelloWave />
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<section style={styles.step}>
|
|
36
|
+
<h2 style={styles.subtitle}>Step 1: Try it</h2>
|
|
37
|
+
<p style={styles.body}>
|
|
38
|
+
Edit <b>server/Home.tsx</b> and save. This page redraws on its own.
|
|
39
|
+
</p>
|
|
40
|
+
</section>
|
|
41
|
+
|
|
42
|
+
<section style={styles.step}>
|
|
43
|
+
<h2 style={styles.subtitle}>Step 2: Server and client</h2>
|
|
44
|
+
<p style={styles.body}>
|
|
45
|
+
Your server assembled this page with Node {$node} at {$assembledAt}.
|
|
46
|
+
The waving hand is a client component: its animation runs in your
|
|
47
|
+
browser. Click it.
|
|
48
|
+
</p>
|
|
49
|
+
</section>
|
|
50
|
+
|
|
51
|
+
<section style={styles.step}>
|
|
52
|
+
<h2 style={styles.subtitle}>Step 3: Get a fresh start</h2>
|
|
53
|
+
<p style={styles.body}>
|
|
54
|
+
When you're ready, run <b>npm run reset-project</b> for a blank{" "}
|
|
55
|
+
<b>server/Home.tsx</b>.
|
|
56
|
+
</p>
|
|
57
|
+
</section>
|
|
58
|
+
|
|
59
|
+
<a style={styles.link} href="https://backtickjs.com/docs">
|
|
60
|
+
Read the docs →
|
|
61
|
+
</a>
|
|
62
|
+
</main>
|
|
63
|
+
);
|
|
64
|
+
}`;
|
|
65
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { bundler } from "@backtickjs/bundler";
|
|
2
|
+
import { cs } from "@backtickjs/core";
|
|
3
|
+
import { createRoot } from "@backtickjs/react/client";
|
|
4
|
+
import type { JSX } from "@backtickjs/react/jsx-runtime";
|
|
5
|
+
|
|
6
|
+
// The versions of React the page loads, below: what each page is bundled for.
|
|
7
|
+
const packageVersions = { react: "19.2.3", "react-dom": "19.2.3" };
|
|
8
|
+
|
|
9
|
+
// Development is only what `npm start` runs, which sets NODE_ENV: any other
|
|
10
|
+
// run, a deploy included, is production.
|
|
11
|
+
export const development = process.env.NODE_ENV === "development";
|
|
12
|
+
|
|
13
|
+
// In development, what reloads the page whenever the server restarts, as it
|
|
14
|
+
// does when you save.
|
|
15
|
+
const reload = `
|
|
16
|
+
<script type="module">
|
|
17
|
+
let run;
|
|
18
|
+
setInterval(async () => {
|
|
19
|
+
try {
|
|
20
|
+
const next = await (await fetch("/live")).text();
|
|
21
|
+
if (run !== undefined && next !== run) location.reload();
|
|
22
|
+
run = next;
|
|
23
|
+
} catch {}
|
|
24
|
+
}, 1000);
|
|
25
|
+
</script>`;
|
|
26
|
+
|
|
27
|
+
// A page: the element, bundled for the browser, and React from a CDN.
|
|
28
|
+
export async function toHtml(element: JSX.Element): Promise<string> {
|
|
29
|
+
const bundle = await bundler.build({
|
|
30
|
+
input: cs`$createRoot(document.getElementById("app")!).render($element)`,
|
|
31
|
+
packageVersions,
|
|
32
|
+
});
|
|
33
|
+
// In development, a map back to the files you wrote, for the browser's
|
|
34
|
+
// devtools.
|
|
35
|
+
const { code } = bundle.generate({
|
|
36
|
+
format: "es",
|
|
37
|
+
sourcemap: development ? "inline" : false,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return `<!doctype html>
|
|
41
|
+
<html lang="en">
|
|
42
|
+
<head>
|
|
43
|
+
<meta charset="utf-8">
|
|
44
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
45
|
+
<title>Backtick</title>
|
|
46
|
+
<script type="importmap">
|
|
47
|
+
{
|
|
48
|
+
"imports": {
|
|
49
|
+
"react": "https://esm.sh/react@19.2.3",
|
|
50
|
+
"react/jsx-runtime": "https://esm.sh/react@19.2.3/jsx-runtime",
|
|
51
|
+
"react-dom/client": "https://esm.sh/react-dom@19.2.3/client?external=react"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
</script>
|
|
55
|
+
</head>
|
|
56
|
+
<body style="margin: 0">
|
|
57
|
+
<div id="app"></div>
|
|
58
|
+
<script type="module" src="data:text/javascript,${encodeURIComponent(code)}"></script>
|
|
59
|
+
${development ? reload : ""}
|
|
60
|
+
</body>
|
|
61
|
+
</html>`;
|
|
62
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createServer } from "node:http";
|
|
2
|
+
import { Home } from "./Home.js";
|
|
3
|
+
import { development, toHtml } from "./html.js";
|
|
4
|
+
|
|
5
|
+
// In development, this run of the server: `--watch` starts a new one whenever
|
|
6
|
+
// you save, and the page, seeing a new one at `/live`, reloads.
|
|
7
|
+
const run = crypto.randomUUID();
|
|
8
|
+
|
|
9
|
+
const server = createServer(async (request, response) => {
|
|
10
|
+
if (development && request.url === "/live") {
|
|
11
|
+
response.end(run);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
if (request.url === "/") {
|
|
15
|
+
try {
|
|
16
|
+
const html = await toHtml(<Home />);
|
|
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();
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
server.listen(3000, () => {
|
|
32
|
+
console.log("Backtick server on http://localhost:3000");
|
|
33
|
+
});
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Imports kept as written: a name a page only splices, as `$useRef`, looks
|
|
4
|
+
// unused to TypeScript, which would otherwise drop its import.
|
|
5
|
+
"verbatimModuleSyntax": true,
|
|
6
|
+
"module": "esnext",
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"target": "esnext",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"lib": ["esnext", "dom"],
|
|
13
|
+
"types": ["node"],
|
|
14
|
+
"jsx": "react-jsx",
|
|
15
|
+
"jsxImportSource": "@backtickjs/react"
|
|
16
|
+
},
|
|
17
|
+
"include": ["server"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Written by the package manager.
|
|
2
|
+
pnpm-lock.yaml
|
|
3
|
+
package-lock.json
|
|
4
|
+
yarn.lock
|
|
5
|
+
bun.lock
|
|
6
|
+
|
|
7
|
+
# Generated by Expo and its native builds.
|
|
8
|
+
.expo/
|
|
9
|
+
dist/
|
|
10
|
+
web-build/
|
|
11
|
+
expo-env.d.ts
|
|
12
|
+
/ios
|
|
13
|
+
/android
|
|
14
|
+
|
|
15
|
+
# Written the way VS Code writes them, one entry per line.
|
|
16
|
+
.vscode/extensions.json
|