create-reactra 0.1.0-alpha.0
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/index.mjs +144 -0
- package/package.json +29 -0
- package/templates/starter/README.md +48 -0
- package/templates/starter/_gitignore +9 -0
- package/templates/starter/index.html +12 -0
- package/templates/starter/package.json +28 -0
- package/templates/starter/src/main.tsx +16 -0
- package/templates/starter/src/pages/_error.tsx +19 -0
- package/templates/starter/src/pages/_layout.tsx +11 -0
- package/templates/starter/src/pages/_loading.tsx +7 -0
- package/templates/starter/src/pages/index.module.css +44 -0
- package/templates/starter/src/pages/index.tsx +20 -0
- package/templates/starter/src/routeManifest.generated.ts +6 -0
- package/templates/starter/src/vite-env.d.ts +7 -0
- package/templates/starter/tsconfig.json +18 -0
- package/templates/starter/vite.config.ts +9 -0
- package/templates/todo/README.md +48 -0
- package/templates/todo/_gitignore +9 -0
- package/templates/todo/index.html +12 -0
- package/templates/todo/package.json +28 -0
- package/templates/todo/src/main.tsx +20 -0
- package/templates/todo/src/pages/_error.tsx +19 -0
- package/templates/todo/src/pages/_layout.tsx +11 -0
- package/templates/todo/src/pages/_loading.tsx +7 -0
- package/templates/todo/src/pages/index.module.css +93 -0
- package/templates/todo/src/pages/index.tsx +50 -0
- package/templates/todo/src/routeManifest.generated.ts +8 -0
- package/templates/todo/src/stores/todoStore.tsx +19 -0
- package/templates/todo/src/vite-env.d.ts +7 -0
- package/templates/todo/tsconfig.json +18 -0
- package/templates/todo/vite.config.ts +9 -0
- package/templates/versions.json +3 -0
package/index.mjs
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// create-reactra — scaffolds a new Reactra app from npm-published @reactra/* packages.
|
|
3
|
+
// Dependency-free (Node built-ins only) so `npm create reactra@latest` needs nothing
|
|
4
|
+
// but Node. Per plan/alpha-publish-and-docs-plan.md §10.
|
|
5
|
+
//
|
|
6
|
+
// npm create reactra@latest my-app
|
|
7
|
+
// npm create reactra@latest my-app -- --template starter --yes
|
|
8
|
+
|
|
9
|
+
import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, writeFileSync } from "node:fs"
|
|
10
|
+
import { join, dirname, resolve } from "node:path"
|
|
11
|
+
import { fileURLToPath } from "node:url"
|
|
12
|
+
import { createInterface } from "node:readline/promises"
|
|
13
|
+
import { stdin, stdout, argv, env, exit } from "node:process"
|
|
14
|
+
import { execSync } from "node:child_process"
|
|
15
|
+
|
|
16
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
17
|
+
const TEMPLATES = join(here, "templates")
|
|
18
|
+
const VERSIONS = JSON.parse(readFileSync(join(TEMPLATES, "versions.json"), "utf8"))
|
|
19
|
+
|
|
20
|
+
/** Parse argv: positional name + --flags. */
|
|
21
|
+
const parseArgs = (args) => {
|
|
22
|
+
const out = { name: undefined, template: undefined, yes: false, git: true, force: false, install: false }
|
|
23
|
+
for (let i = 0; i < args.length; i++) {
|
|
24
|
+
const a = args[i]
|
|
25
|
+
if (a === "--yes" || a === "-y") out.yes = true
|
|
26
|
+
else if (a === "--no-git") out.git = false
|
|
27
|
+
else if (a === "--force" || a === "-f") out.force = true
|
|
28
|
+
else if (a === "--install") out.install = true
|
|
29
|
+
else if (a === "--template" || a === "-t") out.template = args[++i]
|
|
30
|
+
else if (a.startsWith("--template=")) out.template = a.slice("--template=".length)
|
|
31
|
+
else if (!a.startsWith("-") && !out.name) out.name = a
|
|
32
|
+
}
|
|
33
|
+
return out
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const TEMPLATE_CHOICES = ["starter", "todo"]
|
|
37
|
+
const isValidName = (n) => /^[a-z0-9][a-z0-9._-]*$/i.test(n)
|
|
38
|
+
|
|
39
|
+
/** Detect the invoking package manager from npm_config_user_agent. */
|
|
40
|
+
const detectPM = () => {
|
|
41
|
+
const ua = env.npm_config_user_agent ?? ""
|
|
42
|
+
if (ua.startsWith("pnpm")) return "pnpm"
|
|
43
|
+
if (ua.startsWith("yarn")) return "yarn"
|
|
44
|
+
if (ua.startsWith("bun")) return "bun"
|
|
45
|
+
return "npm"
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Recursively copy a template dir, substituting tokens in text files. */
|
|
49
|
+
const copyTemplate = (src, dest, tokens) => {
|
|
50
|
+
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
51
|
+
const from = join(src, entry.name)
|
|
52
|
+
// _gitignore → .gitignore (npm strips real dotfiles from published tarballs).
|
|
53
|
+
const name = entry.name === "_gitignore" ? ".gitignore" : entry.name
|
|
54
|
+
const to = join(dest, name)
|
|
55
|
+
if (entry.isDirectory()) {
|
|
56
|
+
mkdirSync(to, { recursive: true })
|
|
57
|
+
copyTemplate(from, to, tokens)
|
|
58
|
+
} else {
|
|
59
|
+
let content = readFileSync(from, "utf8")
|
|
60
|
+
for (const [k, v] of Object.entries(tokens)) content = content.replaceAll(k, v)
|
|
61
|
+
writeFileSync(to, content)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const main = async () => {
|
|
67
|
+
const opts = parseArgs(argv.slice(2))
|
|
68
|
+
const interactive = stdin.isTTY && !opts.yes
|
|
69
|
+
|
|
70
|
+
let name = opts.name
|
|
71
|
+
let template = opts.template
|
|
72
|
+
|
|
73
|
+
if (interactive) {
|
|
74
|
+
const rl = createInterface({ input: stdin, output: stdout })
|
|
75
|
+
if (!name) {
|
|
76
|
+
const ans = (await rl.question("Project name: (my-reactra-app) ")).trim()
|
|
77
|
+
name = ans || "my-reactra-app"
|
|
78
|
+
}
|
|
79
|
+
if (!template) {
|
|
80
|
+
const ans = (await rl.question("Template — starter / todo: (starter) ")).trim()
|
|
81
|
+
template = ans || "starter"
|
|
82
|
+
}
|
|
83
|
+
rl.close()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
name = name || "my-reactra-app"
|
|
87
|
+
template = template || "starter"
|
|
88
|
+
|
|
89
|
+
if (!isValidName(name)) {
|
|
90
|
+
console.error(`✗ "${name}" is not a valid project name (use letters, digits, - . _).`)
|
|
91
|
+
exit(1)
|
|
92
|
+
}
|
|
93
|
+
if (!TEMPLATE_CHOICES.includes(template)) {
|
|
94
|
+
console.error(`✗ Unknown template "${template}". Choose one of: ${TEMPLATE_CHOICES.join(", ")}.`)
|
|
95
|
+
exit(1)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const templateDir = join(TEMPLATES, template)
|
|
99
|
+
if (!existsSync(templateDir)) {
|
|
100
|
+
console.error(`✗ Template "${template}" is not available in this release.`)
|
|
101
|
+
exit(1)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const targetDir = resolve(process.cwd(), name)
|
|
105
|
+
if (existsSync(targetDir) && readdirSync(targetDir).length > 0 && !opts.force) {
|
|
106
|
+
console.error(`✗ Directory "${name}" already exists and is not empty. Use --force to overwrite.`)
|
|
107
|
+
exit(1)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
mkdirSync(targetDir, { recursive: true })
|
|
111
|
+
copyTemplate(templateDir, targetDir, {
|
|
112
|
+
__PROJECT_NAME__: name,
|
|
113
|
+
__REACTRA_VERSION__: VERSIONS.reactra,
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
if (opts.git) {
|
|
117
|
+
try {
|
|
118
|
+
execSync("git init -q", { cwd: targetDir, stdio: "ignore" })
|
|
119
|
+
} catch {
|
|
120
|
+
// git not available — not fatal.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const pm = detectPM()
|
|
125
|
+
const installCmd = pm === "yarn" ? "yarn" : `${pm} install`
|
|
126
|
+
const devCmd = pm === "npm" ? "npm run dev" : `${pm} dev`
|
|
127
|
+
|
|
128
|
+
if (opts.install) {
|
|
129
|
+
console.log(`\nInstalling dependencies with ${pm}…`)
|
|
130
|
+
execSync(installCmd, { cwd: targetDir, stdio: "inherit" })
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
console.log(`\n✓ Created ${name} (${template} template, @reactra ${VERSIONS.reactra})\n`)
|
|
134
|
+
console.log("Next steps:")
|
|
135
|
+
console.log(` cd ${name}`)
|
|
136
|
+
if (!opts.install) console.log(` ${installCmd}`)
|
|
137
|
+
console.log(` ${devCmd}\n`)
|
|
138
|
+
console.log("Docs: https://reactra.dev\n")
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
main().catch((e) => {
|
|
142
|
+
console.error(e instanceof Error ? e.message : String(e))
|
|
143
|
+
exit(1)
|
|
144
|
+
})
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-reactra",
|
|
3
|
+
"version": "0.1.0-alpha.0",
|
|
4
|
+
"description": "Scaffold a new Reactra app — a compiler-first, React-19-compatible framework.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-reactra": "index.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"index.mjs",
|
|
11
|
+
"templates"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=22.18"
|
|
15
|
+
},
|
|
16
|
+
"keywords": ["reactra", "react", "scaffold", "create", "starter", "template"],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public",
|
|
19
|
+
"tag": "alpha",
|
|
20
|
+
"provenance": false
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "git+https://github.com/akhilshastri/reactra.git",
|
|
25
|
+
"directory": "packages/create-reactra"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://reactra-docs.vercel.app",
|
|
28
|
+
"license": "MIT"
|
|
29
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# __PROJECT_NAME__
|
|
2
|
+
|
|
3
|
+
A [Reactra](https://reactra.dev) app — a compiler-first, React-19-compatible framework.
|
|
4
|
+
|
|
5
|
+
## Run
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Open the printed URL (default `http://localhost:5173`). Edit `src/pages/index.tsx`
|
|
13
|
+
and save — the page hot-reloads.
|
|
14
|
+
|
|
15
|
+
## Project layout
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
src/
|
|
19
|
+
├── main.tsx # boots the router, mounts <RouteRenderer>
|
|
20
|
+
├── routeManifest.generated.ts # generated — do not edit
|
|
21
|
+
└── pages/
|
|
22
|
+
├── index.tsx # → / (a Reactra DSL component)
|
|
23
|
+
├── _layout.tsx # wraps every page
|
|
24
|
+
├── _loading.tsx # shown while a route loads
|
|
25
|
+
└── _error.tsx # route error boundary
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Add a page
|
|
29
|
+
|
|
30
|
+
Drop a file under `src/pages` — its path is its route:
|
|
31
|
+
|
|
32
|
+
- `src/pages/about.tsx` → `/about`
|
|
33
|
+
- `src/pages/users/[id].tsx` → `/users/:id` (with a typed `param id`)
|
|
34
|
+
|
|
35
|
+
`@reactra/vite-plugin` regenerates the route manifest automatically.
|
|
36
|
+
|
|
37
|
+
## Learn more
|
|
38
|
+
|
|
39
|
+
- [Documentation](https://reactra.dev)
|
|
40
|
+
- [Your first page](https://reactra.dev/getting-started/first-page/)
|
|
41
|
+
- [The DSL guide](https://reactra.dev/guide/components-and-view/)
|
|
42
|
+
|
|
43
|
+
## Build
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm run build # production build → dist/
|
|
47
|
+
npm run preview # serve the built output
|
|
48
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
node_modules
|
|
2
|
+
dist
|
|
3
|
+
*.log
|
|
4
|
+
|
|
5
|
+
# Regenerated by @reactra/vite-plugin on every src/pages change. A placeholder is
|
|
6
|
+
# committed so a fresh clone type-checks before `vite dev` has run once.
|
|
7
|
+
src/routeManifest.generated.ts
|
|
8
|
+
src/router-middleware.generated.ts
|
|
9
|
+
.reactra
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>__PROJECT_NAME__</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@reactra/router": "__REACTRA_VERSION__",
|
|
13
|
+
"@reactra/store": "__REACTRA_VERSION__",
|
|
14
|
+
"@reactra/service": "__REACTRA_VERSION__",
|
|
15
|
+
"@reactra/resource": "__REACTRA_VERSION__",
|
|
16
|
+
"@reactra/behaviours": "__REACTRA_VERSION__",
|
|
17
|
+
"react": "^19.2.0",
|
|
18
|
+
"react-dom": "^19.2.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@reactra/vite-plugin": "__REACTRA_VERSION__",
|
|
22
|
+
"@types/react": "^19.2.0",
|
|
23
|
+
"@types/react-dom": "^19.2.0",
|
|
24
|
+
"@vitejs/plugin-react": "^5.0.0",
|
|
25
|
+
"typescript": "^5.7.0",
|
|
26
|
+
"vite": "^7.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Browser entry point — boots the router and mounts the app.
|
|
2
|
+
import { StrictMode } from "react"
|
|
3
|
+
import { createRoot } from "react-dom/client"
|
|
4
|
+
import { configureRouter, RouteRenderer } from "@reactra/router"
|
|
5
|
+
import { ROUTES } from "./routeManifest.generated"
|
|
6
|
+
|
|
7
|
+
configureRouter({ mode: "history", routes: ROUTES })
|
|
8
|
+
|
|
9
|
+
const rootEl = document.getElementById("root")
|
|
10
|
+
if (!rootEl) throw new Error("#root element missing from index.html")
|
|
11
|
+
|
|
12
|
+
createRoot(rootEl).render(
|
|
13
|
+
<StrictMode>
|
|
14
|
+
<RouteRenderer />
|
|
15
|
+
</StrictMode>,
|
|
16
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Error boundary — shown when a route throws while rendering. Receives the thrown
|
|
2
|
+
// `error` plus a `reset` callback that clears it so the children re-render.
|
|
3
|
+
interface RouteErrorProps {
|
|
4
|
+
error: unknown
|
|
5
|
+
reset: () => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const RouteError = ({ error, reset }: RouteErrorProps): import("react").JSX.Element => {
|
|
9
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
10
|
+
return (
|
|
11
|
+
<div style={{ padding: "1rem", color: "#b3261e" }}>
|
|
12
|
+
<h2>Something went wrong</h2>
|
|
13
|
+
<p>{message}</p>
|
|
14
|
+
<button type="button" onClick={reset}>Try again</button>
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default RouteError
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Root layout — the chrome that wraps every route. Persists across navigation,
|
|
2
|
+
// so anything here (a header, nav) stays mounted while pages swap. Plain React.
|
|
3
|
+
interface RootLayoutProps {
|
|
4
|
+
children?: import("react").ReactNode
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const RootLayout = ({ children }: RootLayoutProps): import("react").JSX.Element => (
|
|
8
|
+
<div>{children}</div>
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
export default RootLayout
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Loading fallback — shown while a route's code-split chunk is loading (the
|
|
2
|
+
// router wires this up as the Suspense fallback for routes at this level).
|
|
3
|
+
const RouteLoading = (): import("react").JSX.Element => (
|
|
4
|
+
<div style={{ padding: "1rem", color: "#6b7d73", fontStyle: "italic" }}>loading…</div>
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
export default RouteLoading
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
.app {
|
|
2
|
+
max-width: 40rem;
|
|
3
|
+
margin: 4rem auto;
|
|
4
|
+
padding: 0 1.5rem;
|
|
5
|
+
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
6
|
+
color: #1a2b22;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.app h1 {
|
|
10
|
+
font-size: 2rem;
|
|
11
|
+
margin: 0 0 0.5rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.muted {
|
|
15
|
+
color: #4a6256;
|
|
16
|
+
font-size: 1.1rem;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.button {
|
|
20
|
+
margin-top: 0.5rem;
|
|
21
|
+
padding: 0.5rem 1.1rem;
|
|
22
|
+
font-size: 1rem;
|
|
23
|
+
color: #fff;
|
|
24
|
+
background: #137a41;
|
|
25
|
+
border: none;
|
|
26
|
+
border-radius: 0.5rem;
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.button:hover {
|
|
31
|
+
background: #0f6536;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.hint {
|
|
35
|
+
margin-top: 2rem;
|
|
36
|
+
color: #6b7d73;
|
|
37
|
+
font-size: 0.9rem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.hint code {
|
|
41
|
+
background: #e7f0ea;
|
|
42
|
+
padding: 0.1rem 0.35rem;
|
|
43
|
+
border-radius: 0.3rem;
|
|
44
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import styles from "./index.module.css"
|
|
2
|
+
|
|
3
|
+
export component HomePage {
|
|
4
|
+
meta { title: "__PROJECT_NAME__" }
|
|
5
|
+
|
|
6
|
+
state count: number = 0
|
|
7
|
+
derived label = count === 1 ? "1 click" : `${count} clicks`
|
|
8
|
+
action inc() { count = count + 1 }
|
|
9
|
+
|
|
10
|
+
view {
|
|
11
|
+
<main className={styles.app}>
|
|
12
|
+
<h1>Welcome to Reactra</h1>
|
|
13
|
+
<p className={styles.muted}>{label}</p>
|
|
14
|
+
<button type="button" className={styles.button} onClick={inc}>click me</button>
|
|
15
|
+
<p className={styles.hint}>
|
|
16
|
+
Edit <code>src/pages/index.tsx</code> and save — the page hot-reloads.
|
|
17
|
+
</p>
|
|
18
|
+
</main>
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// routeManifest.generated.ts — DO NOT EDIT.
|
|
2
|
+
// Regenerated by @reactra/vite-plugin on every src/pages change. This committed
|
|
3
|
+
// placeholder lets a fresh clone type-check before `vite dev` has run once.
|
|
4
|
+
import type { RouteConfig } from "@reactra/router"
|
|
5
|
+
|
|
6
|
+
export const ROUTES: RouteConfig[] = []
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"allowImportingTsExtensions": true,
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"verbatimModuleSyntax": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*", "vite.config.ts"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Plugin order is load-bearing: @reactra/vite-plugin runs at enforce:"pre" so it
|
|
2
|
+
// transforms Reactra DSL → React-19 TSX before @vitejs/plugin-react transforms it.
|
|
3
|
+
import { defineConfig } from "vite"
|
|
4
|
+
import react from "@vitejs/plugin-react"
|
|
5
|
+
import reactra from "@reactra/vite-plugin"
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
plugins: [reactra(), react()],
|
|
9
|
+
})
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# __PROJECT_NAME__
|
|
2
|
+
|
|
3
|
+
A [Reactra](https://reactra.dev) app — a compiler-first, React-19-compatible framework.
|
|
4
|
+
|
|
5
|
+
## Run
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install
|
|
9
|
+
npm run dev
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Open the printed URL (default `http://localhost:5173`). Edit `src/pages/index.tsx`
|
|
13
|
+
and save — the page hot-reloads.
|
|
14
|
+
|
|
15
|
+
## Project layout
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
src/
|
|
19
|
+
├── main.tsx # boots the router, mounts <RouteRenderer>
|
|
20
|
+
├── routeManifest.generated.ts # generated — do not edit
|
|
21
|
+
└── pages/
|
|
22
|
+
├── index.tsx # → / (a Reactra DSL component)
|
|
23
|
+
├── _layout.tsx # wraps every page
|
|
24
|
+
├── _loading.tsx # shown while a route loads
|
|
25
|
+
└── _error.tsx # route error boundary
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Add a page
|
|
29
|
+
|
|
30
|
+
Drop a file under `src/pages` — its path is its route:
|
|
31
|
+
|
|
32
|
+
- `src/pages/about.tsx` → `/about`
|
|
33
|
+
- `src/pages/users/[id].tsx` → `/users/:id` (with a typed `param id`)
|
|
34
|
+
|
|
35
|
+
`@reactra/vite-plugin` regenerates the route manifest automatically.
|
|
36
|
+
|
|
37
|
+
## Learn more
|
|
38
|
+
|
|
39
|
+
- [Documentation](https://reactra.dev)
|
|
40
|
+
- [Your first page](https://reactra.dev/getting-started/first-page/)
|
|
41
|
+
- [The DSL guide](https://reactra.dev/guide/components-and-view/)
|
|
42
|
+
|
|
43
|
+
## Build
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm run build # production build → dist/
|
|
47
|
+
npm run preview # serve the built output
|
|
48
|
+
```
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
node_modules
|
|
2
|
+
dist
|
|
3
|
+
*.log
|
|
4
|
+
|
|
5
|
+
# Regenerated by @reactra/vite-plugin on every src/pages change. A placeholder is
|
|
6
|
+
# committed so a fresh clone type-checks before `vite dev` has run once.
|
|
7
|
+
src/routeManifest.generated.ts
|
|
8
|
+
src/router-middleware.generated.ts
|
|
9
|
+
.reactra
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>__PROJECT_NAME__</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/src/main.tsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__PROJECT_NAME__",
|
|
3
|
+
"private": true,
|
|
4
|
+
"version": "0.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@reactra/router": "__REACTRA_VERSION__",
|
|
13
|
+
"@reactra/store": "__REACTRA_VERSION__",
|
|
14
|
+
"@reactra/service": "__REACTRA_VERSION__",
|
|
15
|
+
"@reactra/resource": "__REACTRA_VERSION__",
|
|
16
|
+
"@reactra/behaviours": "__REACTRA_VERSION__",
|
|
17
|
+
"react": "^19.2.0",
|
|
18
|
+
"react-dom": "^19.2.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@reactra/vite-plugin": "__REACTRA_VERSION__",
|
|
22
|
+
"@types/react": "^19.2.0",
|
|
23
|
+
"@types/react-dom": "^19.2.0",
|
|
24
|
+
"@vitejs/plugin-react": "^5.0.0",
|
|
25
|
+
"typescript": "^5.7.0",
|
|
26
|
+
"vite": "^7.0.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Browser entry point — boots the stores + router and mounts the app.
|
|
2
|
+
// Sequence matters: configureStores precedes configureRouter so every store
|
|
3
|
+
// factory is registered before the first route renders.
|
|
4
|
+
import { StrictMode } from "react"
|
|
5
|
+
import { createRoot } from "react-dom/client"
|
|
6
|
+
import { configureStores } from "@reactra/store"
|
|
7
|
+
import { configureRouter, RouteRenderer } from "@reactra/router"
|
|
8
|
+
import { ROUTES, STORES } from "./routeManifest.generated"
|
|
9
|
+
|
|
10
|
+
configureStores({ stores: STORES })
|
|
11
|
+
configureRouter({ mode: "history", routes: ROUTES })
|
|
12
|
+
|
|
13
|
+
const rootEl = document.getElementById("root")
|
|
14
|
+
if (!rootEl) throw new Error("#root element missing from index.html")
|
|
15
|
+
|
|
16
|
+
createRoot(rootEl).render(
|
|
17
|
+
<StrictMode>
|
|
18
|
+
<RouteRenderer />
|
|
19
|
+
</StrictMode>,
|
|
20
|
+
)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Error boundary — shown when a route throws while rendering. Receives the thrown
|
|
2
|
+
// `error` plus a `reset` callback that clears it so the children re-render.
|
|
3
|
+
interface RouteErrorProps {
|
|
4
|
+
error: unknown
|
|
5
|
+
reset: () => void
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const RouteError = ({ error, reset }: RouteErrorProps): import("react").JSX.Element => {
|
|
9
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
10
|
+
return (
|
|
11
|
+
<div style={{ padding: "1rem", color: "#b3261e" }}>
|
|
12
|
+
<h2>Something went wrong</h2>
|
|
13
|
+
<p>{message}</p>
|
|
14
|
+
<button type="button" onClick={reset}>Try again</button>
|
|
15
|
+
</div>
|
|
16
|
+
)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default RouteError
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Root layout — the chrome that wraps every route. Persists across navigation,
|
|
2
|
+
// so anything here (a header, nav) stays mounted while pages swap. Plain React.
|
|
3
|
+
interface RootLayoutProps {
|
|
4
|
+
children?: import("react").ReactNode
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const RootLayout = ({ children }: RootLayoutProps): import("react").JSX.Element => (
|
|
8
|
+
<div>{children}</div>
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
export default RootLayout
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// Loading fallback — shown while a route's code-split chunk is loading (the
|
|
2
|
+
// router wires this up as the Suspense fallback for routes at this level).
|
|
3
|
+
const RouteLoading = (): import("react").JSX.Element => (
|
|
4
|
+
<div style={{ padding: "1rem", color: "#6b7d73", fontStyle: "italic" }}>loading…</div>
|
|
5
|
+
)
|
|
6
|
+
|
|
7
|
+
export default RouteLoading
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
.app {
|
|
2
|
+
max-width: 40rem;
|
|
3
|
+
margin: 4rem auto;
|
|
4
|
+
padding: 0 1.5rem;
|
|
5
|
+
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
6
|
+
color: #1a2b22;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.app h1 {
|
|
10
|
+
font-size: 2rem;
|
|
11
|
+
margin: 0 0 0.5rem;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.muted {
|
|
15
|
+
color: #4a6256;
|
|
16
|
+
font-size: 1.1rem;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.button {
|
|
20
|
+
margin-top: 0.5rem;
|
|
21
|
+
padding: 0.5rem 1.1rem;
|
|
22
|
+
font-size: 1rem;
|
|
23
|
+
color: #fff;
|
|
24
|
+
background: #137a41;
|
|
25
|
+
border: none;
|
|
26
|
+
border-radius: 0.5rem;
|
|
27
|
+
cursor: pointer;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.button:hover {
|
|
31
|
+
background: #0f6536;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.hint {
|
|
35
|
+
margin-top: 2rem;
|
|
36
|
+
color: #6b7d73;
|
|
37
|
+
font-size: 0.9rem;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.hint code {
|
|
41
|
+
background: #e7f0ea;
|
|
42
|
+
padding: 0.1rem 0.35rem;
|
|
43
|
+
border-radius: 0.3rem;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.row {
|
|
47
|
+
display: flex;
|
|
48
|
+
gap: 0.5rem;
|
|
49
|
+
margin: 1rem 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.input {
|
|
53
|
+
flex: 1;
|
|
54
|
+
padding: 0.5rem 0.75rem;
|
|
55
|
+
font-size: 1rem;
|
|
56
|
+
border: 1px solid #c3d3c9;
|
|
57
|
+
border-radius: 0.5rem;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.list {
|
|
61
|
+
list-style: none;
|
|
62
|
+
padding: 0;
|
|
63
|
+
margin: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.item {
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: space-between;
|
|
70
|
+
padding: 0.5rem 0;
|
|
71
|
+
border-bottom: 1px solid #eef3f0;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.item label {
|
|
75
|
+
display: flex;
|
|
76
|
+
align-items: center;
|
|
77
|
+
gap: 0.5rem;
|
|
78
|
+
cursor: pointer;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
.done {
|
|
82
|
+
color: #9aa8a0;
|
|
83
|
+
text-decoration: line-through;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.remove {
|
|
87
|
+
background: none;
|
|
88
|
+
border: none;
|
|
89
|
+
color: #b3261e;
|
|
90
|
+
font-size: 1.2rem;
|
|
91
|
+
cursor: pointer;
|
|
92
|
+
line-height: 1;
|
|
93
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import styles from "./index.module.css"
|
|
2
|
+
// The store name is bound by an `import type` — the compiler resolves
|
|
3
|
+
// `inject store todoStore` to this declaration.
|
|
4
|
+
import type { todoStore } from "../stores/todoStore"
|
|
5
|
+
|
|
6
|
+
export component HomePage {
|
|
7
|
+
meta { title: "__PROJECT_NAME__" }
|
|
8
|
+
|
|
9
|
+
// Bare `inject store` subscribes this component to the store (per-field).
|
|
10
|
+
inject store todoStore
|
|
11
|
+
|
|
12
|
+
state draft: string = ""
|
|
13
|
+
|
|
14
|
+
action add() {
|
|
15
|
+
const text = draft.trim()
|
|
16
|
+
if (text) {
|
|
17
|
+
todoStore.add(text)
|
|
18
|
+
draft = ""
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
view {
|
|
23
|
+
<main className={styles.app}>
|
|
24
|
+
<h1>Todos</h1>
|
|
25
|
+
<p className={styles.muted}>{todoStore.remaining} remaining</p>
|
|
26
|
+
|
|
27
|
+
<form className={styles.row} onSubmit={e => { e.preventDefault(); add() }}>
|
|
28
|
+
<input
|
|
29
|
+
className={styles.input}
|
|
30
|
+
value={draft}
|
|
31
|
+
placeholder="What needs doing?"
|
|
32
|
+
onInput={e => { draft = (e.target as HTMLInputElement).value }}
|
|
33
|
+
/>
|
|
34
|
+
<button type="submit" className={styles.button}>add</button>
|
|
35
|
+
</form>
|
|
36
|
+
|
|
37
|
+
<ul className={styles.list}>
|
|
38
|
+
{todoStore.items.map(t => (
|
|
39
|
+
<li key={t.id} className={styles.item}>
|
|
40
|
+
<label className={t.done ? styles.done : ""}>
|
|
41
|
+
<input type="checkbox" checked={t.done} onChange={() => todoStore.toggle(t.id)} />
|
|
42
|
+
{t.text}
|
|
43
|
+
</label>
|
|
44
|
+
<button type="button" className={styles.remove} onClick={() => todoStore.remove(t.id)}>×</button>
|
|
45
|
+
</li>
|
|
46
|
+
))}
|
|
47
|
+
</ul>
|
|
48
|
+
</main>
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// routeManifest.generated.ts — DO NOT EDIT.
|
|
2
|
+
// Regenerated by @reactra/vite-plugin on every src/pages or store change. This
|
|
3
|
+
// committed placeholder lets a fresh clone type-check before `vite dev` has run.
|
|
4
|
+
import type { RouteConfig } from "@reactra/router"
|
|
5
|
+
import type { StoreBinding } from "@reactra/store"
|
|
6
|
+
|
|
7
|
+
export const ROUTES: RouteConfig[] = []
|
|
8
|
+
export const STORES: StoreBinding[] = []
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// A session store — shared state that outlives a single component and persists
|
|
2
|
+
// across route navigation (for the lifetime of the browser session).
|
|
3
|
+
export session store todoStore {
|
|
4
|
+
state items: { id: number; text: string; done: boolean }[] = []
|
|
5
|
+
state nextId: number = 1
|
|
6
|
+
|
|
7
|
+
derived remaining = items.filter(t => !t.done).length
|
|
8
|
+
|
|
9
|
+
action add(text: string) {
|
|
10
|
+
items = [...items, { id: nextId, text, done: false }]
|
|
11
|
+
nextId = nextId + 1
|
|
12
|
+
}
|
|
13
|
+
action toggle(id: number) {
|
|
14
|
+
items = items.map(t => (t.id === id ? { ...t, done: !t.done } : t))
|
|
15
|
+
}
|
|
16
|
+
action remove(id: number) {
|
|
17
|
+
items = items.filter(t => t.id !== id)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"allowImportingTsExtensions": true,
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"noEmit": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"verbatimModuleSyntax": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*", "vite.config.ts"]
|
|
18
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Plugin order is load-bearing: @reactra/vite-plugin runs at enforce:"pre" so it
|
|
2
|
+
// transforms Reactra DSL → React-19 TSX before @vitejs/plugin-react transforms it.
|
|
3
|
+
import { defineConfig } from "vite"
|
|
4
|
+
import react from "@vitejs/plugin-react"
|
|
5
|
+
import reactra from "@reactra/vite-plugin"
|
|
6
|
+
|
|
7
|
+
export default defineConfig({
|
|
8
|
+
plugins: [reactra(), react()],
|
|
9
|
+
})
|