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.
Files changed (32) hide show
  1. package/index.mjs +144 -0
  2. package/package.json +29 -0
  3. package/templates/starter/README.md +48 -0
  4. package/templates/starter/_gitignore +9 -0
  5. package/templates/starter/index.html +12 -0
  6. package/templates/starter/package.json +28 -0
  7. package/templates/starter/src/main.tsx +16 -0
  8. package/templates/starter/src/pages/_error.tsx +19 -0
  9. package/templates/starter/src/pages/_layout.tsx +11 -0
  10. package/templates/starter/src/pages/_loading.tsx +7 -0
  11. package/templates/starter/src/pages/index.module.css +44 -0
  12. package/templates/starter/src/pages/index.tsx +20 -0
  13. package/templates/starter/src/routeManifest.generated.ts +6 -0
  14. package/templates/starter/src/vite-env.d.ts +7 -0
  15. package/templates/starter/tsconfig.json +18 -0
  16. package/templates/starter/vite.config.ts +9 -0
  17. package/templates/todo/README.md +48 -0
  18. package/templates/todo/_gitignore +9 -0
  19. package/templates/todo/index.html +12 -0
  20. package/templates/todo/package.json +28 -0
  21. package/templates/todo/src/main.tsx +20 -0
  22. package/templates/todo/src/pages/_error.tsx +19 -0
  23. package/templates/todo/src/pages/_layout.tsx +11 -0
  24. package/templates/todo/src/pages/_loading.tsx +7 -0
  25. package/templates/todo/src/pages/index.module.css +93 -0
  26. package/templates/todo/src/pages/index.tsx +50 -0
  27. package/templates/todo/src/routeManifest.generated.ts +8 -0
  28. package/templates/todo/src/stores/todoStore.tsx +19 -0
  29. package/templates/todo/src/vite-env.d.ts +7 -0
  30. package/templates/todo/tsconfig.json +18 -0
  31. package/templates/todo/vite.config.ts +9 -0
  32. 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,7 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ // Ambient type for CSS modules so `import styles from "./x.module.css"` type-checks.
4
+ declare module "*.module.css" {
5
+ const classes: Readonly<Record<string, string>>
6
+ export default classes
7
+ }
@@ -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,7 @@
1
+ /// <reference types="vite/client" />
2
+
3
+ // Ambient type for CSS modules so `import styles from "./x.module.css"` type-checks.
4
+ declare module "*.module.css" {
5
+ const classes: Readonly<Record<string, string>>
6
+ export default classes
7
+ }
@@ -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,3 @@
1
+ {
2
+ "reactra": "0.1.0-alpha.0"
3
+ }