@pyreon/create-zero 0.1.1 → 0.3.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/README.md +24 -15
- package/bin/create-zero.js +0 -0
- package/lib/index.js +406 -16
- package/lib/index.js.map +1 -1
- package/package.json +4 -1
- package/templates/default/.mcp.json +8 -0
- package/templates/default/CLAUDE.md +58 -0
- package/templates/default/env.d.ts +10 -0
- package/templates/default/package.json +14 -12
- package/templates/default/src/entry-server.ts +8 -0
- package/templates/default/src/features/posts.ts +12 -0
- package/templates/default/src/routes/(admin)/dashboard.tsx +8 -18
- package/templates/default/src/routes/_layout.tsx +18 -2
- package/templates/default/src/routes/api/health.ts +7 -0
- package/templates/default/src/routes/api/posts.ts +40 -0
- package/templates/default/src/routes/counter.tsx +3 -3
- package/templates/default/src/routes/posts/index.tsx +7 -0
- package/templates/default/src/routes/posts/new.tsx +57 -0
- package/templates/default/src/stores/app.ts +7 -0
package/README.md
CHANGED
|
@@ -1,31 +1,40 @@
|
|
|
1
|
-
# create-zero
|
|
1
|
+
# @pyreon/create-zero
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Interactive scaffolding tool for [Pyreon Zero](https://github.com/pyreon/zero) projects.
|
|
4
4
|
|
|
5
5
|
## Usage
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
bun create zero my-app
|
|
8
|
+
bun create @pyreon/zero my-app
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Or
|
|
11
|
+
Or via the CLI:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
|
|
14
|
+
zero create my-app
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## Interactive Setup
|
|
18
18
|
|
|
19
|
-
The
|
|
19
|
+
The CLI prompts you to configure:
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
21
|
+
1. **Rendering mode** — SSR Streaming, SSR String, SSG, or SPA
|
|
22
|
+
2. **Features** — pick from store, query, forms, feature CRUD, i18n, tables, virtual lists, CSS-in-JS, UI elements, animations, hooks
|
|
23
|
+
3. **AI toolchain** — MCP server config, CLAUDE.md, doctor scripts
|
|
24
|
+
|
|
25
|
+
## What Gets Generated
|
|
26
|
+
|
|
27
|
+
Based on your selections:
|
|
28
|
+
|
|
29
|
+
- `package.json` — only the dependencies you chose
|
|
30
|
+
- `vite.config.ts` — configured for your rendering mode
|
|
31
|
+
- `src/entry-server.ts` — matching SSR/stream config with CORS + rate limiting
|
|
32
|
+
- `src/routes/` — example pages, API routes, protected dashboard
|
|
33
|
+
- `src/features/` — feature example with Zod schema (if selected)
|
|
34
|
+
- `src/stores/` — store example (if selected)
|
|
35
|
+
- `.mcp.json` — AI IDE integration (if AI toolchain selected)
|
|
36
|
+
- `CLAUDE.md` — project rules for AI agents (if AI toolchain selected)
|
|
37
|
+
- `env.d.ts` — virtual module type declarations
|
|
29
38
|
|
|
30
39
|
## License
|
|
31
40
|
|
package/bin/create-zero.js
CHANGED
|
File without changes
|
package/lib/index.js
CHANGED
|
@@ -1,26 +1,416 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
2
|
import { cp, readFile, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, join, resolve } from "node:path";
|
|
4
|
+
import * as p from "@clack/prompts";
|
|
4
5
|
|
|
5
6
|
//#region src/index.ts
|
|
7
|
+
const FEATURES = {
|
|
8
|
+
store: {
|
|
9
|
+
label: "State Management (@pyreon/store)",
|
|
10
|
+
deps: ["@pyreon/store"]
|
|
11
|
+
},
|
|
12
|
+
query: {
|
|
13
|
+
label: "Data Fetching (@pyreon/query)",
|
|
14
|
+
deps: ["@pyreon/query", "@tanstack/query-core"]
|
|
15
|
+
},
|
|
16
|
+
forms: {
|
|
17
|
+
label: "Forms + Validation (@pyreon/form, @pyreon/validation)",
|
|
18
|
+
deps: [
|
|
19
|
+
"@pyreon/form",
|
|
20
|
+
"@pyreon/validation",
|
|
21
|
+
"zod"
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
feature: {
|
|
25
|
+
label: "Feature CRUD (@pyreon/feature) — includes store, query, forms",
|
|
26
|
+
deps: [
|
|
27
|
+
"@pyreon/feature",
|
|
28
|
+
"@pyreon/store",
|
|
29
|
+
"@pyreon/query",
|
|
30
|
+
"@pyreon/form",
|
|
31
|
+
"@pyreon/validation",
|
|
32
|
+
"@tanstack/query-core",
|
|
33
|
+
"zod"
|
|
34
|
+
]
|
|
35
|
+
},
|
|
36
|
+
i18n: {
|
|
37
|
+
label: "Internationalization (@pyreon/i18n)",
|
|
38
|
+
deps: ["@pyreon/i18n"]
|
|
39
|
+
},
|
|
40
|
+
table: {
|
|
41
|
+
label: "Tables (@pyreon/table)",
|
|
42
|
+
deps: ["@pyreon/table", "@tanstack/table-core"]
|
|
43
|
+
},
|
|
44
|
+
virtual: {
|
|
45
|
+
label: "Virtual Lists (@pyreon/virtual)",
|
|
46
|
+
deps: ["@pyreon/virtual", "@tanstack/virtual-core"]
|
|
47
|
+
},
|
|
48
|
+
styler: {
|
|
49
|
+
label: "CSS-in-JS (@pyreon/styler)",
|
|
50
|
+
deps: ["@pyreon/styler", "@pyreon/ui-core"]
|
|
51
|
+
},
|
|
52
|
+
elements: {
|
|
53
|
+
label: "UI Elements (@pyreon/elements, @pyreon/coolgrid)",
|
|
54
|
+
deps: [
|
|
55
|
+
"@pyreon/elements",
|
|
56
|
+
"@pyreon/coolgrid",
|
|
57
|
+
"@pyreon/unistyle",
|
|
58
|
+
"@pyreon/ui-core"
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
animations: {
|
|
62
|
+
label: "Animations (@pyreon/kinetic + 120 presets)",
|
|
63
|
+
deps: ["@pyreon/kinetic", "@pyreon/kinetic-presets"]
|
|
64
|
+
},
|
|
65
|
+
hooks: {
|
|
66
|
+
label: "Hooks (@pyreon/hooks — 25+ signal-based utilities)",
|
|
67
|
+
deps: ["@pyreon/hooks"]
|
|
68
|
+
},
|
|
69
|
+
charts: {
|
|
70
|
+
label: "Charts (@pyreon/charts — reactive ECharts)",
|
|
71
|
+
deps: ["@pyreon/charts"]
|
|
72
|
+
},
|
|
73
|
+
hotkeys: {
|
|
74
|
+
label: "Hotkeys (@pyreon/hotkeys — keyboard shortcuts)",
|
|
75
|
+
deps: ["@pyreon/hotkeys"]
|
|
76
|
+
},
|
|
77
|
+
storage: {
|
|
78
|
+
label: "Storage (@pyreon/storage — localStorage, cookies, IndexedDB)",
|
|
79
|
+
deps: ["@pyreon/storage"]
|
|
80
|
+
},
|
|
81
|
+
flow: {
|
|
82
|
+
label: "Flow Diagrams (@pyreon/flow — reactive node graphs)",
|
|
83
|
+
deps: ["@pyreon/flow"]
|
|
84
|
+
},
|
|
85
|
+
code: {
|
|
86
|
+
label: "Code Editor (@pyreon/code — CodeMirror 6)",
|
|
87
|
+
deps: ["@pyreon/code"]
|
|
88
|
+
}
|
|
89
|
+
};
|
|
6
90
|
const TEMPLATE_DIR = resolve(import.meta.dirname, "../templates/default");
|
|
7
91
|
async function main() {
|
|
8
|
-
const
|
|
9
|
-
if (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
.
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
92
|
+
const argName = process.argv.slice(2)[0];
|
|
93
|
+
if (argName === "--help" || argName === "-h") {
|
|
94
|
+
console.log("Usage: create-zero [project-name]");
|
|
95
|
+
process.exit(0);
|
|
96
|
+
}
|
|
97
|
+
p.intro("Create a new Pyreon Zero project");
|
|
98
|
+
const name = argName ?? await p.text({
|
|
99
|
+
message: "Project name",
|
|
100
|
+
placeholder: "my-zero-app",
|
|
101
|
+
validate: (v) => {
|
|
102
|
+
if (!v?.trim()) return "Project name is required";
|
|
103
|
+
if (existsSync(resolve(process.cwd(), v))) return `Directory "${v}" already exists`;
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
if (p.isCancel(name)) {
|
|
107
|
+
p.cancel("Cancelled.");
|
|
108
|
+
process.exit(0);
|
|
109
|
+
}
|
|
110
|
+
const targetDir = resolve(process.cwd(), name);
|
|
111
|
+
if (existsSync(targetDir)) {
|
|
112
|
+
p.cancel(`Directory "${name}" already exists.`);
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
const renderMode = await p.select({
|
|
116
|
+
message: "Rendering mode",
|
|
117
|
+
options: [
|
|
118
|
+
{
|
|
119
|
+
value: "ssr-stream",
|
|
120
|
+
label: "SSR Streaming",
|
|
121
|
+
hint: "recommended — progressive HTML with Suspense"
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
value: "ssr-string",
|
|
125
|
+
label: "SSR String",
|
|
126
|
+
hint: "buffered HTML, simpler but slower TTFB"
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
value: "ssg",
|
|
130
|
+
label: "Static (SSG)",
|
|
131
|
+
hint: "pre-rendered at build time"
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
value: "spa",
|
|
135
|
+
label: "SPA",
|
|
136
|
+
hint: "client-only, no server rendering"
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
});
|
|
140
|
+
if (p.isCancel(renderMode)) {
|
|
141
|
+
p.cancel("Cancelled.");
|
|
142
|
+
process.exit(0);
|
|
143
|
+
}
|
|
144
|
+
const features = await p.multiselect({
|
|
145
|
+
message: "Select features (space to toggle, enter to confirm)",
|
|
146
|
+
options: Object.entries(FEATURES).map(([key, { label }]) => ({
|
|
147
|
+
value: key,
|
|
148
|
+
label
|
|
149
|
+
})),
|
|
150
|
+
initialValues: [
|
|
151
|
+
"store",
|
|
152
|
+
"query",
|
|
153
|
+
"forms"
|
|
154
|
+
],
|
|
155
|
+
required: false
|
|
156
|
+
});
|
|
157
|
+
if (p.isCancel(features)) {
|
|
158
|
+
p.cancel("Cancelled.");
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}
|
|
161
|
+
const packageStrategy = await p.select({
|
|
162
|
+
message: "Package imports",
|
|
163
|
+
options: [{
|
|
164
|
+
value: "meta",
|
|
165
|
+
label: "@pyreon/meta (single barrel)",
|
|
166
|
+
hint: "one import for everything — simpler, tree-shaken at build"
|
|
167
|
+
}, {
|
|
168
|
+
value: "individual",
|
|
169
|
+
label: "Individual packages",
|
|
170
|
+
hint: "only install what you selected — smaller node_modules"
|
|
171
|
+
}]
|
|
172
|
+
});
|
|
173
|
+
if (p.isCancel(packageStrategy)) {
|
|
174
|
+
p.cancel("Cancelled.");
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
const aiToolchain = await p.confirm({
|
|
178
|
+
message: "Include AI toolchain? (MCP server, CLAUDE.md, doctor)",
|
|
179
|
+
initialValue: true
|
|
180
|
+
});
|
|
181
|
+
if (p.isCancel(aiToolchain)) {
|
|
182
|
+
p.cancel("Cancelled.");
|
|
183
|
+
process.exit(0);
|
|
184
|
+
}
|
|
185
|
+
const config = {
|
|
186
|
+
name,
|
|
187
|
+
targetDir,
|
|
188
|
+
renderMode,
|
|
189
|
+
features,
|
|
190
|
+
packageStrategy,
|
|
191
|
+
aiToolchain
|
|
192
|
+
};
|
|
193
|
+
const s = p.spinner();
|
|
194
|
+
s.start("Scaffolding project...");
|
|
195
|
+
await scaffold(config);
|
|
196
|
+
s.stop("Project created!");
|
|
197
|
+
p.note([
|
|
198
|
+
`cd ${config.name}`,
|
|
199
|
+
"bun install",
|
|
200
|
+
"bun run dev"
|
|
201
|
+
].join("\n"), "Next steps");
|
|
202
|
+
p.outro("Happy building!");
|
|
203
|
+
}
|
|
204
|
+
async function scaffold(config) {
|
|
205
|
+
await cp(TEMPLATE_DIR, config.targetDir, { recursive: true });
|
|
206
|
+
await writeFile(join(config.targetDir, "package.json"), generatePackageJson(config));
|
|
207
|
+
await writeFile(join(config.targetDir, "vite.config.ts"), generateViteConfig(config));
|
|
208
|
+
await writeFile(join(config.targetDir, "src/entry-server.ts"), generateEntryServer(config));
|
|
209
|
+
await writeFile(join(config.targetDir, "env.d.ts"), generateEnvDts(config));
|
|
210
|
+
await writeFile(join(config.targetDir, ".gitignore"), "node_modules\ndist\n.DS_Store\n*.local\n.pyreon\n");
|
|
211
|
+
if (config.aiToolchain) await writeFile(join(config.targetDir, ".mcp.json"), JSON.stringify({ mcpServers: { pyreon: {
|
|
212
|
+
command: "bunx",
|
|
213
|
+
args: ["@pyreon/mcp"]
|
|
214
|
+
} } }, null, 2));
|
|
215
|
+
else for (const f of [".mcp.json", "CLAUDE.md"]) {
|
|
216
|
+
const path = join(config.targetDir, f);
|
|
217
|
+
if (existsSync(path)) {
|
|
218
|
+
const { unlink } = await import("node:fs/promises");
|
|
219
|
+
await unlink(path);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (!config.features.includes("feature") && !config.features.includes("forms")) {
|
|
223
|
+
await removeIfExists(join(config.targetDir, "src/routes/posts/new.tsx"));
|
|
224
|
+
await removeIfExists(join(config.targetDir, "src/features"));
|
|
225
|
+
}
|
|
226
|
+
if (!config.features.includes("store")) await removeIfExists(join(config.targetDir, "src/stores"));
|
|
227
|
+
if (!config.features.includes("store")) {
|
|
228
|
+
const layoutPath = join(config.targetDir, "src/routes/_layout.tsx");
|
|
229
|
+
if (existsSync(layoutPath)) {
|
|
230
|
+
let layout = await readFile(layoutPath, "utf-8");
|
|
231
|
+
layout = layout.replace(/import .* from '\.\.\/stores\/app'\n/g, "").replace(/.*useAppStore.*\n/g, "").replace(/\s*<button[\s\S]*?sidebar-toggle[\s\S]*?<\/button>\n/g, "");
|
|
232
|
+
await writeFile(layoutPath, layout);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function pyreonVersion(pkg) {
|
|
237
|
+
if ([
|
|
238
|
+
"core",
|
|
239
|
+
"reactivity",
|
|
240
|
+
"runtime-dom",
|
|
241
|
+
"runtime-server",
|
|
242
|
+
"server",
|
|
243
|
+
"head",
|
|
244
|
+
"router",
|
|
245
|
+
"vite-plugin",
|
|
246
|
+
"compiler",
|
|
247
|
+
"cli",
|
|
248
|
+
"mcp"
|
|
249
|
+
].some((c) => pkg === `@pyreon/${c}`)) return "^0.7.0";
|
|
250
|
+
if (pkg === "@pyreon/zero" || pkg === "@pyreon/meta" || pkg === "@pyreon/zero-cli" || pkg === "@pyreon/create-zero") return "^0.2.0";
|
|
251
|
+
if ([
|
|
252
|
+
"store",
|
|
253
|
+
"form",
|
|
254
|
+
"validation",
|
|
255
|
+
"query",
|
|
256
|
+
"table",
|
|
257
|
+
"virtual",
|
|
258
|
+
"i18n",
|
|
259
|
+
"feature",
|
|
260
|
+
"machine",
|
|
261
|
+
"permissions",
|
|
262
|
+
"flow",
|
|
263
|
+
"code"
|
|
264
|
+
].some((f) => pkg === `@pyreon/${f}`)) return "^0.6.0";
|
|
265
|
+
return "^0.2.0";
|
|
266
|
+
}
|
|
267
|
+
function generatePackageJson(config) {
|
|
268
|
+
const deps = {
|
|
269
|
+
"@pyreon/core": pyreonVersion("@pyreon/core"),
|
|
270
|
+
"@pyreon/head": pyreonVersion("@pyreon/head"),
|
|
271
|
+
"@pyreon/reactivity": pyreonVersion("@pyreon/reactivity"),
|
|
272
|
+
"@pyreon/router": pyreonVersion("@pyreon/router"),
|
|
273
|
+
"@pyreon/runtime-dom": pyreonVersion("@pyreon/runtime-dom"),
|
|
274
|
+
"@pyreon/runtime-server": pyreonVersion("@pyreon/runtime-server"),
|
|
275
|
+
"@pyreon/server": pyreonVersion("@pyreon/server"),
|
|
276
|
+
"@pyreon/zero": pyreonVersion("@pyreon/zero")
|
|
277
|
+
};
|
|
278
|
+
if (config.packageStrategy === "meta") {
|
|
279
|
+
deps["@pyreon/meta"] = pyreonVersion("@pyreon/meta");
|
|
280
|
+
for (const key of config.features) {
|
|
281
|
+
const feature = FEATURES[key];
|
|
282
|
+
if (feature) {
|
|
283
|
+
for (const dep of feature.deps) if (!dep.startsWith("@pyreon/")) {
|
|
284
|
+
if (dep.startsWith("@tanstack/")) deps[dep] = dep.includes("query") ? "^5.90.0" : dep.includes("table") ? "^8.21.0" : "^3.13.0";
|
|
285
|
+
else if (dep === "zod") deps[dep] = "^4.0.0";
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
const allDeps = /* @__PURE__ */ new Set();
|
|
291
|
+
for (const key of config.features) {
|
|
292
|
+
const feature = FEATURES[key];
|
|
293
|
+
if (feature) for (const dep of feature.deps) allDeps.add(dep);
|
|
294
|
+
}
|
|
295
|
+
for (const dep of allDeps) if (dep.startsWith("@pyreon/")) deps[dep] = pyreonVersion(dep);
|
|
296
|
+
else if (dep.startsWith("@tanstack/")) deps[dep] = dep.includes("query") ? "^5.90.0" : dep.includes("table") ? "^8.21.0" : "^3.13.0";
|
|
297
|
+
else if (dep === "zod") deps[dep] = "^4.0.0";
|
|
298
|
+
}
|
|
299
|
+
const devDeps = {
|
|
300
|
+
"@pyreon/vite-plugin": pyreonVersion("@pyreon/vite-plugin"),
|
|
301
|
+
"@pyreon/zero-cli": pyreonVersion("@pyreon/zero-cli"),
|
|
302
|
+
"typescript": "^5.9.3",
|
|
303
|
+
"vite": "^7.0.0"
|
|
304
|
+
};
|
|
305
|
+
if (config.aiToolchain) devDeps["@pyreon/mcp"] = pyreonVersion("@pyreon/mcp");
|
|
306
|
+
const pkg = {
|
|
307
|
+
name: basename(config.name),
|
|
308
|
+
version: "0.0.1",
|
|
309
|
+
private: true,
|
|
310
|
+
type: "module",
|
|
311
|
+
scripts: {
|
|
312
|
+
dev: "zero dev",
|
|
313
|
+
build: "zero build",
|
|
314
|
+
preview: "zero preview",
|
|
315
|
+
doctor: "zero doctor",
|
|
316
|
+
"doctor:fix": "zero doctor --fix",
|
|
317
|
+
"doctor:ci": "zero doctor --ci"
|
|
318
|
+
},
|
|
319
|
+
dependencies: Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a.localeCompare(b))),
|
|
320
|
+
devDependencies: Object.fromEntries(Object.entries(devDeps).sort(([a], [b]) => a.localeCompare(b)))
|
|
321
|
+
};
|
|
322
|
+
return `${JSON.stringify(pkg, null, 2)}\n`;
|
|
323
|
+
}
|
|
324
|
+
function generateViteConfig(config) {
|
|
325
|
+
return `import pyreon from '@pyreon/vite-plugin'
|
|
326
|
+
import zero from '@pyreon/zero'
|
|
327
|
+
import { fontPlugin } from '@pyreon/zero/font'
|
|
328
|
+
import { seoPlugin } from '@pyreon/zero/seo'
|
|
329
|
+
|
|
330
|
+
export default {
|
|
331
|
+
plugins: [
|
|
332
|
+
pyreon(),
|
|
333
|
+
zero({ ${{
|
|
334
|
+
"ssr-stream": `mode: 'ssr', ssr: { mode: 'stream' }`,
|
|
335
|
+
"ssr-string": `mode: 'ssr'`,
|
|
336
|
+
ssg: `mode: 'ssg'`,
|
|
337
|
+
spa: `mode: 'spa'`
|
|
338
|
+
}[config.renderMode]} }),
|
|
339
|
+
|
|
340
|
+
// Google Fonts — self-hosted at build time, CDN in dev
|
|
341
|
+
fontPlugin({
|
|
342
|
+
google: ['Inter:wght@400;500;600;700;800', 'JetBrains Mono:wght@400'],
|
|
343
|
+
fallbacks: {
|
|
344
|
+
Inter: { fallback: 'Arial', sizeAdjust: 1.07, ascentOverride: 90 },
|
|
345
|
+
},
|
|
346
|
+
}),
|
|
347
|
+
|
|
348
|
+
// Generate sitemap.xml and robots.txt at build time
|
|
349
|
+
seoPlugin({
|
|
350
|
+
sitemap: { origin: 'https://example.com' },
|
|
351
|
+
robots: {
|
|
352
|
+
rules: [{ userAgent: '*', allow: ['/'] }],
|
|
353
|
+
sitemap: 'https://example.com/sitemap.xml',
|
|
354
|
+
},
|
|
355
|
+
}),
|
|
356
|
+
],
|
|
357
|
+
}
|
|
358
|
+
`;
|
|
359
|
+
}
|
|
360
|
+
function generateEntryServer(config) {
|
|
361
|
+
return `${[
|
|
362
|
+
`import { routes } from 'virtual:zero/routes'`,
|
|
363
|
+
`import { routeMiddleware } from 'virtual:zero/route-middleware'`,
|
|
364
|
+
`import { createServer } from '@pyreon/zero'`,
|
|
365
|
+
`import {\n cacheMiddleware,\n securityHeaders,\n varyEncoding,\n} from '@pyreon/zero/cache'`
|
|
366
|
+
].join("\n")}
|
|
367
|
+
|
|
368
|
+
export default createServer({
|
|
369
|
+
routes,
|
|
370
|
+
routeMiddleware,
|
|
371
|
+
config: {
|
|
372
|
+
ssr: { mode: '${{
|
|
373
|
+
"ssr-stream": `stream`,
|
|
374
|
+
"ssr-string": `string`,
|
|
375
|
+
ssg: `string`,
|
|
376
|
+
spa: `string`
|
|
377
|
+
}[config.renderMode]}' },
|
|
378
|
+
},
|
|
379
|
+
middleware: [
|
|
380
|
+
securityHeaders(),
|
|
381
|
+
cacheMiddleware({ staleWhileRevalidate: 120 }),
|
|
382
|
+
varyEncoding(),
|
|
383
|
+
],
|
|
384
|
+
})
|
|
385
|
+
`;
|
|
386
|
+
}
|
|
387
|
+
function generateEnvDts(config) {
|
|
388
|
+
let content = `/// <reference types="vite/client" />
|
|
389
|
+
|
|
390
|
+
declare module 'virtual:zero/routes' {
|
|
391
|
+
import type { RouteRecord } from '@pyreon/router'
|
|
392
|
+
export const routes: RouteRecord[]
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
declare module 'virtual:zero/route-middleware' {
|
|
396
|
+
import type { RouteMiddlewareEntry } from '@pyreon/zero'
|
|
397
|
+
export const routeMiddleware: RouteMiddlewareEntry[]
|
|
398
|
+
}
|
|
399
|
+
`;
|
|
400
|
+
if (config.features.includes("query")) content += `
|
|
401
|
+
declare module 'virtual:zero/actions' {
|
|
402
|
+
export {}
|
|
403
|
+
}
|
|
404
|
+
`;
|
|
405
|
+
return content;
|
|
406
|
+
}
|
|
407
|
+
async function removeIfExists(path) {
|
|
408
|
+
if (!existsSync(path)) return;
|
|
409
|
+
const { rm } = await import("node:fs/promises");
|
|
410
|
+
await rm(path, { recursive: true });
|
|
411
|
+
}
|
|
412
|
+
main().catch((err) => {
|
|
413
|
+
console.error(err);
|
|
24
414
|
process.exit(1);
|
|
25
415
|
});
|
|
26
416
|
|
package/lib/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { cp, readFile, writeFile } from 'node:fs/promises'\nimport { basename, join, resolve } from 'node:path'\n\n// ─── Config ──────────────────────────────────────────────────────────────────\n\nconst TEMPLATE_DIR = resolve(import.meta.dirname, '../templates/default')\n\n// ─── CLI ─────────────────────────────────────────────────────────────────────\n\nasync function main() {\n const args = process.argv.slice(2)\n const projectName = args[0]\n\n if (!projectName || projectName === '--help' || projectName === '-h') {\n process.exit(projectName ? 0 : 1)\n }\n\n const targetDir = resolve(process.cwd(), projectName)\n\n if (existsSync(targetDir)) {\n process.exit(1)\n }\n\n // Copy template\n await cp(TEMPLATE_DIR, targetDir, { recursive: true })\n\n // Update package.json with project name\n const pkgPath = join(targetDir, 'package.json')\n const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'))\n pkg.name = basename(projectName)\n await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\\n`)\n\n // Create .gitignore (can't be in template — npm ignores .gitignore in packages)\n await writeFile(\n join(targetDir, '.gitignore'),\n `node_modules\ndist\n.DS_Store\n*.local\n`,\n )\n}\n\nmain().catch((_err) => {\n process.exit(1)\n})\n"],"mappings":";;;;;AAMA,MAAM,eAAe,QAAQ,OAAO,KAAK,SAAS,uBAAuB;AAIzE,eAAe,OAAO;CAEpB,MAAM,cADO,QAAQ,KAAK,MAAM,EAAE,CACT;AAEzB,KAAI,CAAC,eAAe,gBAAgB,YAAY,gBAAgB,KAC9D,SAAQ,KAAK,cAAc,IAAI,EAAE;CAGnC,MAAM,YAAY,QAAQ,QAAQ,KAAK,EAAE,YAAY;AAErD,KAAI,WAAW,UAAU,CACvB,SAAQ,KAAK,EAAE;AAIjB,OAAM,GAAG,cAAc,WAAW,EAAE,WAAW,MAAM,CAAC;CAGtD,MAAM,UAAU,KAAK,WAAW,eAAe;CAC/C,MAAM,MAAM,KAAK,MAAM,MAAM,SAAS,SAAS,QAAQ,CAAC;AACxD,KAAI,OAAO,SAAS,YAAY;AAChC,OAAM,UAAU,SAAS,GAAG,KAAK,UAAU,KAAK,MAAM,EAAE,CAAC,IAAI;AAG7D,OAAM,UACJ,KAAK,WAAW,aAAa,EAC7B;;;;EAKD;;AAGH,MAAM,CAAC,OAAO,SAAS;AACrB,SAAQ,KAAK,EAAE;EACf"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../src/index.ts"],"sourcesContent":["import { existsSync } from 'node:fs'\nimport { cp, readFile, writeFile } from 'node:fs/promises'\nimport { basename, join, resolve } from 'node:path'\nimport * as p from '@clack/prompts'\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\ninterface ProjectConfig {\n name: string\n targetDir: string\n renderMode: 'ssr-stream' | 'ssr-string' | 'ssg' | 'spa'\n features: string[]\n packageStrategy: 'meta' | 'individual'\n aiToolchain: boolean\n}\n\n// ─── Feature definitions ────────────────────────────────────────────────────\n\nconst FEATURES = {\n store: {\n label: 'State Management (@pyreon/store)',\n deps: ['@pyreon/store'],\n },\n query: {\n label: 'Data Fetching (@pyreon/query)',\n deps: ['@pyreon/query', '@tanstack/query-core'],\n },\n forms: {\n label: 'Forms + Validation (@pyreon/form, @pyreon/validation)',\n deps: ['@pyreon/form', '@pyreon/validation', 'zod'],\n },\n feature: {\n label: 'Feature CRUD (@pyreon/feature) — includes store, query, forms',\n deps: [\n '@pyreon/feature',\n '@pyreon/store',\n '@pyreon/query',\n '@pyreon/form',\n '@pyreon/validation',\n '@tanstack/query-core',\n 'zod',\n ],\n },\n i18n: {\n label: 'Internationalization (@pyreon/i18n)',\n deps: ['@pyreon/i18n'],\n },\n table: {\n label: 'Tables (@pyreon/table)',\n deps: ['@pyreon/table', '@tanstack/table-core'],\n },\n virtual: {\n label: 'Virtual Lists (@pyreon/virtual)',\n deps: ['@pyreon/virtual', '@tanstack/virtual-core'],\n },\n styler: {\n label: 'CSS-in-JS (@pyreon/styler)',\n deps: ['@pyreon/styler', '@pyreon/ui-core'],\n },\n elements: {\n label: 'UI Elements (@pyreon/elements, @pyreon/coolgrid)',\n deps: ['@pyreon/elements', '@pyreon/coolgrid', '@pyreon/unistyle', '@pyreon/ui-core'],\n },\n animations: {\n label: 'Animations (@pyreon/kinetic + 120 presets)',\n deps: ['@pyreon/kinetic', '@pyreon/kinetic-presets'],\n },\n hooks: {\n label: 'Hooks (@pyreon/hooks — 25+ signal-based utilities)',\n deps: ['@pyreon/hooks'],\n },\n charts: {\n label: 'Charts (@pyreon/charts — reactive ECharts)',\n deps: ['@pyreon/charts'],\n },\n hotkeys: {\n label: 'Hotkeys (@pyreon/hotkeys — keyboard shortcuts)',\n deps: ['@pyreon/hotkeys'],\n },\n storage: {\n label: 'Storage (@pyreon/storage — localStorage, cookies, IndexedDB)',\n deps: ['@pyreon/storage'],\n },\n flow: {\n label: 'Flow Diagrams (@pyreon/flow — reactive node graphs)',\n deps: ['@pyreon/flow'],\n },\n code: {\n label: 'Code Editor (@pyreon/code — CodeMirror 6)',\n deps: ['@pyreon/code'],\n },\n} as const\n\ntype FeatureKey = keyof typeof FEATURES\n\n// ─── Template directory ─────────────────────────────────────────────────────\n\nconst TEMPLATE_DIR = resolve(import.meta.dirname, '../templates/default')\n\n// ─── Main ───────────────────────────────────────────────────────────────────\n\nasync function main() {\n const args = process.argv.slice(2)\n const argName = args[0]\n\n if (argName === '--help' || argName === '-h') {\n console.log('Usage: create-zero [project-name]')\n process.exit(0)\n }\n\n p.intro('Create a new Pyreon Zero project')\n\n // Project name\n const name = argName ?? (await p.text({\n message: 'Project name',\n placeholder: 'my-zero-app',\n validate: (v) => {\n if (!v?.trim()) return 'Project name is required'\n if (existsSync(resolve(process.cwd(), v))) return `Directory \"${v}\" already exists`\n },\n }))\n\n if (p.isCancel(name)) {\n p.cancel('Cancelled.')\n process.exit(0)\n }\n\n const targetDir = resolve(process.cwd(), name as string)\n if (existsSync(targetDir)) {\n p.cancel(`Directory \"${name}\" already exists.`)\n process.exit(1)\n }\n\n // Rendering mode\n const renderMode = await p.select({\n message: 'Rendering mode',\n options: [\n { value: 'ssr-stream', label: 'SSR Streaming', hint: 'recommended — progressive HTML with Suspense' },\n { value: 'ssr-string', label: 'SSR String', hint: 'buffered HTML, simpler but slower TTFB' },\n { value: 'ssg', label: 'Static (SSG)', hint: 'pre-rendered at build time' },\n { value: 'spa', label: 'SPA', hint: 'client-only, no server rendering' },\n ],\n })\n\n if (p.isCancel(renderMode)) {\n p.cancel('Cancelled.')\n process.exit(0)\n }\n\n // Features\n const features = await p.multiselect({\n message: 'Select features (space to toggle, enter to confirm)',\n options: Object.entries(FEATURES).map(([key, { label }]) => ({\n value: key,\n label,\n })),\n initialValues: ['store', 'query', 'forms'],\n required: false,\n })\n\n if (p.isCancel(features)) {\n p.cancel('Cancelled.')\n process.exit(0)\n }\n\n // Package strategy\n const packageStrategy = await p.select({\n message: 'Package imports',\n options: [\n { value: 'meta', label: '@pyreon/meta (single barrel)', hint: 'one import for everything — simpler, tree-shaken at build' },\n { value: 'individual', label: 'Individual packages', hint: 'only install what you selected — smaller node_modules' },\n ],\n })\n\n if (p.isCancel(packageStrategy)) {\n p.cancel('Cancelled.')\n process.exit(0)\n }\n\n // AI toolchain\n const aiToolchain = await p.confirm({\n message: 'Include AI toolchain? (MCP server, CLAUDE.md, doctor)',\n initialValue: true,\n })\n\n if (p.isCancel(aiToolchain)) {\n p.cancel('Cancelled.')\n process.exit(0)\n }\n\n const config: ProjectConfig = {\n name: name as string,\n targetDir,\n renderMode: renderMode as ProjectConfig['renderMode'],\n features: features as string[],\n packageStrategy: packageStrategy as ProjectConfig['packageStrategy'],\n aiToolchain: aiToolchain as boolean,\n }\n\n const s = p.spinner()\n s.start('Scaffolding project...')\n\n await scaffold(config)\n\n s.stop('Project created!')\n\n // Next steps\n p.note(\n [\n `cd ${config.name}`,\n 'bun install',\n 'bun run dev',\n ].join('\\n'),\n 'Next steps',\n )\n\n p.outro('Happy building!')\n}\n\n// ─── Scaffolding ────────────────────────────────────────────────────────────\n\nasync function scaffold(config: ProjectConfig) {\n // Copy full template as base\n await cp(TEMPLATE_DIR, config.targetDir, { recursive: true })\n\n // Generate customized files\n await writeFile(\n join(config.targetDir, 'package.json'),\n generatePackageJson(config),\n )\n\n await writeFile(\n join(config.targetDir, 'vite.config.ts'),\n generateViteConfig(config),\n )\n\n await writeFile(\n join(config.targetDir, 'src/entry-server.ts'),\n generateEntryServer(config),\n )\n\n await writeFile(\n join(config.targetDir, 'env.d.ts'),\n generateEnvDts(config),\n )\n\n // Create .gitignore (npm strips it from packages)\n await writeFile(\n join(config.targetDir, '.gitignore'),\n 'node_modules\\ndist\\n.DS_Store\\n*.local\\n.pyreon\\n',\n )\n\n // AI toolchain files\n if (config.aiToolchain) {\n await writeFile(\n join(config.targetDir, '.mcp.json'),\n JSON.stringify(\n {\n mcpServers: {\n pyreon: { command: 'bunx', args: ['@pyreon/mcp'] },\n },\n },\n null,\n 2,\n ),\n )\n } else {\n // Remove AI files from copied template\n const aiFiles = ['.mcp.json', 'CLAUDE.md']\n for (const f of aiFiles) {\n const path = join(config.targetDir, f)\n if (existsSync(path)) {\n const { unlink } = await import('node:fs/promises')\n await unlink(path)\n }\n }\n }\n\n // Remove feature-specific files if features not selected\n if (!config.features.includes('feature') && !config.features.includes('forms')) {\n await removeIfExists(join(config.targetDir, 'src/routes/posts/new.tsx'))\n await removeIfExists(join(config.targetDir, 'src/features'))\n }\n\n if (!config.features.includes('store')) {\n await removeIfExists(join(config.targetDir, 'src/stores'))\n }\n\n // Remove store import from layout if store not selected\n if (!config.features.includes('store')) {\n const layoutPath = join(config.targetDir, 'src/routes/_layout.tsx')\n if (existsSync(layoutPath)) {\n let layout = await readFile(layoutPath, 'utf-8')\n layout = layout\n .replace(/import .* from '\\.\\.\\/stores\\/app'\\n/g, '')\n .replace(/.*useAppStore.*\\n/g, '')\n .replace(/\\s*<button[\\s\\S]*?sidebar-toggle[\\s\\S]*?<\\/button>\\n/g, '')\n await writeFile(layoutPath, layout)\n }\n }\n}\n\n// ─── File generators ────────────────────────────────────────────────────────\n\n// Resolve the correct version range for a @pyreon/* package\nfunction pyreonVersion(pkg: string): string {\n // Core packages\n const core = ['core', 'reactivity', 'runtime-dom', 'runtime-server', 'server', 'head', 'router', 'vite-plugin', 'compiler', 'cli', 'mcp']\n if (core.some((c) => pkg === `@pyreon/${c}`)) return '^0.7.0'\n // Zero framework packages\n if (pkg === '@pyreon/zero' || pkg === '@pyreon/meta' || pkg === '@pyreon/zero-cli' || pkg === '@pyreon/create-zero') return '^0.2.0'\n // Fundamentals\n const fundamentals = ['store', 'form', 'validation', 'query', 'table', 'virtual', 'i18n', 'feature', 'machine', 'permissions', 'flow', 'code']\n if (fundamentals.some((f) => pkg === `@pyreon/${f}`)) return '^0.6.0'\n // UI system\n return '^0.2.0'\n}\n\nfunction generatePackageJson(config: ProjectConfig): string {\n const deps: Record<string, string> = {\n '@pyreon/core': pyreonVersion('@pyreon/core'),\n '@pyreon/head': pyreonVersion('@pyreon/head'),\n '@pyreon/reactivity': pyreonVersion('@pyreon/reactivity'),\n '@pyreon/router': pyreonVersion('@pyreon/router'),\n '@pyreon/runtime-dom': pyreonVersion('@pyreon/runtime-dom'),\n '@pyreon/runtime-server': pyreonVersion('@pyreon/runtime-server'),\n '@pyreon/server': pyreonVersion('@pyreon/server'),\n '@pyreon/zero': pyreonVersion('@pyreon/zero'),\n }\n\n if (config.packageStrategy === 'meta') {\n // Single barrel — includes all fundamentals + UI system\n deps['@pyreon/meta'] = pyreonVersion('@pyreon/meta')\n // Still need non-pyreon deps for selected features\n for (const key of config.features) {\n const feature = FEATURES[key as FeatureKey]\n if (feature) {\n for (const dep of feature.deps) {\n if (!dep.startsWith('@pyreon/')) {\n if (dep.startsWith('@tanstack/')) {\n deps[dep] = dep.includes('query') ? '^5.90.0' : dep.includes('table') ? '^8.21.0' : '^3.13.0'\n } else if (dep === 'zod') {\n deps[dep] = '^4.0.0'\n }\n }\n }\n }\n }\n } else {\n // Individual packages — only install what's selected\n const allDeps = new Set<string>()\n for (const key of config.features) {\n const feature = FEATURES[key as FeatureKey]\n if (feature) {\n for (const dep of feature.deps) allDeps.add(dep)\n }\n }\n for (const dep of allDeps) {\n if (dep.startsWith('@pyreon/')) {\n deps[dep] = pyreonVersion(dep)\n } else if (dep.startsWith('@tanstack/')) {\n deps[dep] = dep.includes('query') ? '^5.90.0' : dep.includes('table') ? '^8.21.0' : '^3.13.0'\n } else if (dep === 'zod') {\n deps[dep] = '^4.0.0'\n }\n }\n }\n\n const devDeps: Record<string, string> = {\n '@pyreon/vite-plugin': pyreonVersion('@pyreon/vite-plugin'),\n '@pyreon/zero-cli': pyreonVersion('@pyreon/zero-cli'),\n 'typescript': '^5.9.3',\n 'vite': '^7.0.0',\n }\n\n if (config.aiToolchain) {\n devDeps['@pyreon/mcp'] = pyreonVersion('@pyreon/mcp')\n }\n\n const scripts: Record<string, string> = {\n dev: 'zero dev',\n build: 'zero build',\n preview: 'zero preview',\n doctor: 'zero doctor',\n 'doctor:fix': 'zero doctor --fix',\n 'doctor:ci': 'zero doctor --ci',\n }\n\n const pkg = {\n name: basename(config.name),\n version: '0.0.1',\n private: true,\n type: 'module',\n scripts,\n dependencies: Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a.localeCompare(b))),\n devDependencies: Object.fromEntries(Object.entries(devDeps).sort(([a], [b]) => a.localeCompare(b))),\n }\n\n return `${JSON.stringify(pkg, null, 2)}\\n`\n}\n\nfunction generateViteConfig(config: ProjectConfig): string {\n const modeMap = {\n 'ssr-stream': `mode: 'ssr', ssr: { mode: 'stream' }`,\n 'ssr-string': `mode: 'ssr'`,\n ssg: `mode: 'ssg'`,\n spa: `mode: 'spa'`,\n }\n\n return `import pyreon from '@pyreon/vite-plugin'\nimport zero from '@pyreon/zero'\nimport { fontPlugin } from '@pyreon/zero/font'\nimport { seoPlugin } from '@pyreon/zero/seo'\n\nexport default {\n plugins: [\n pyreon(),\n zero({ ${modeMap[config.renderMode]} }),\n\n // Google Fonts — self-hosted at build time, CDN in dev\n fontPlugin({\n google: ['Inter:wght@400;500;600;700;800', 'JetBrains Mono:wght@400'],\n fallbacks: {\n Inter: { fallback: 'Arial', sizeAdjust: 1.07, ascentOverride: 90 },\n },\n }),\n\n // Generate sitemap.xml and robots.txt at build time\n seoPlugin({\n sitemap: { origin: 'https://example.com' },\n robots: {\n rules: [{ userAgent: '*', allow: ['/'] }],\n sitemap: 'https://example.com/sitemap.xml',\n },\n }),\n ],\n}\n`\n}\n\nfunction generateEntryServer(config: ProjectConfig): string {\n const imports = [\n `import { routes } from 'virtual:zero/routes'`,\n `import { routeMiddleware } from 'virtual:zero/route-middleware'`,\n `import { createServer } from '@pyreon/zero'`,\n `import {\\n cacheMiddleware,\\n securityHeaders,\\n varyEncoding,\\n} from '@pyreon/zero/cache'`,\n ]\n\n const modeMap = {\n 'ssr-stream': `stream`,\n 'ssr-string': `string`,\n ssg: `string`,\n spa: `string`,\n }\n\n return `${imports.join('\\n')}\n\nexport default createServer({\n routes,\n routeMiddleware,\n config: {\n ssr: { mode: '${modeMap[config.renderMode]}' },\n },\n middleware: [\n securityHeaders(),\n cacheMiddleware({ staleWhileRevalidate: 120 }),\n varyEncoding(),\n ],\n})\n`\n}\n\nfunction generateEnvDts(config: ProjectConfig): string {\n let content = `/// <reference types=\"vite/client\" />\n\ndeclare module 'virtual:zero/routes' {\n import type { RouteRecord } from '@pyreon/router'\n export const routes: RouteRecord[]\n}\n\ndeclare module 'virtual:zero/route-middleware' {\n import type { RouteMiddlewareEntry } from '@pyreon/zero'\n export const routeMiddleware: RouteMiddlewareEntry[]\n}\n`\n\n if (config.features.includes('query')) {\n content += `\ndeclare module 'virtual:zero/actions' {\n export {}\n}\n`\n }\n\n return content\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nasync function removeIfExists(path: string) {\n if (!existsSync(path)) return\n const { rm } = await import('node:fs/promises')\n await rm(path, { recursive: true })\n}\n\nmain().catch((err) => {\n console.error(err)\n process.exit(1)\n})\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/create-zero",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Create a new Pyreon Zero project",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Vit Bokisch",
|
|
@@ -25,5 +25,8 @@
|
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "vl_rolldown_build",
|
|
27
27
|
"typecheck": "tsc --noEmit"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"@clack/prompts": "^1.0.0"
|
|
28
31
|
}
|
|
29
32
|
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Project
|
|
2
|
+
|
|
3
|
+
This project uses Pyreon Zero, a signal-based full-stack meta-framework. Do NOT use React patterns.
|
|
4
|
+
|
|
5
|
+
## Reactivity
|
|
6
|
+
|
|
7
|
+
- Use `signal()` not `useState`
|
|
8
|
+
- Use `computed()` not `useMemo`
|
|
9
|
+
- Use `effect()` not `useEffect`
|
|
10
|
+
- Use `onMount` / `onUnmount` not `useEffect` with deps
|
|
11
|
+
- Use `signal.set(value)` or `signal.update(fn)` to write signals
|
|
12
|
+
- Read signals by calling them: `count()` not `count`
|
|
13
|
+
|
|
14
|
+
## JSX
|
|
15
|
+
|
|
16
|
+
- Use `class=` not `className`
|
|
17
|
+
- Use `for=` not `htmlFor`
|
|
18
|
+
- Reactive text: `{() => count()}`
|
|
19
|
+
- Conditional: `{() => show() ? <A /> : null}`
|
|
20
|
+
- Lists: `{() => items().map(item => <Item />)}`
|
|
21
|
+
- Events: `onClick={() => ...}` (standard DOM events)
|
|
22
|
+
- JSX import source is `@pyreon/core` (auto-configured, no manual import needed)
|
|
23
|
+
|
|
24
|
+
## File-Based Routing
|
|
25
|
+
|
|
26
|
+
- `src/routes/index.tsx` → `/`
|
|
27
|
+
- `src/routes/about.tsx` → `/about`
|
|
28
|
+
- `src/routes/[id].tsx` → `/:id`
|
|
29
|
+
- `src/routes/_layout.tsx` → layout wrapper
|
|
30
|
+
- `src/routes/_error.tsx` → error boundary
|
|
31
|
+
- `src/routes/_loading.tsx` → loading state
|
|
32
|
+
- `(group)/` → route group (no URL segment)
|
|
33
|
+
|
|
34
|
+
## Route Exports
|
|
35
|
+
|
|
36
|
+
- `default` — page component
|
|
37
|
+
- `loader` — server-side data fetching
|
|
38
|
+
- `guard` — navigation guard
|
|
39
|
+
- `middleware` — per-route server middleware
|
|
40
|
+
- `meta` — route metadata
|
|
41
|
+
- `renderMode` — per-route rendering mode override
|
|
42
|
+
|
|
43
|
+
## Data Patterns
|
|
44
|
+
|
|
45
|
+
- Use `defineStore()` from `@pyreon/store` for global state
|
|
46
|
+
- Use `useQuery()` from `@pyreon/query` for data fetching
|
|
47
|
+
- Use `useForm()` from `@pyreon/form` for forms
|
|
48
|
+
- Use `defineFeature()` from `@pyreon/feature` for schema-driven CRUD
|
|
49
|
+
- Use `defineAction()` from `@pyreon/zero/actions` for server mutations
|
|
50
|
+
|
|
51
|
+
## Commands
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
bun run dev # Start dev server
|
|
55
|
+
bun run build # Production build
|
|
56
|
+
bun run preview # Preview production build
|
|
57
|
+
bun run doctor # Check for React patterns
|
|
58
|
+
```
|
|
@@ -4,3 +4,13 @@ declare module 'virtual:zero/routes' {
|
|
|
4
4
|
import type { RouteRecord } from '@pyreon/router'
|
|
5
5
|
export const routes: RouteRecord[]
|
|
6
6
|
}
|
|
7
|
+
|
|
8
|
+
declare module 'virtual:zero/route-middleware' {
|
|
9
|
+
import type { RouteMiddlewareEntry } from '@pyreon/zero'
|
|
10
|
+
export const routeMiddleware: RouteMiddlewareEntry[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
declare module 'virtual:zero/api-routes' {
|
|
14
|
+
import type { ApiRouteEntry } from '@pyreon/zero'
|
|
15
|
+
export const apiRoutes: ApiRouteEntry[]
|
|
16
|
+
}
|
|
@@ -6,22 +6,24 @@
|
|
|
6
6
|
"scripts": {
|
|
7
7
|
"dev": "zero dev",
|
|
8
8
|
"build": "zero build",
|
|
9
|
-
"preview": "zero preview"
|
|
9
|
+
"preview": "zero preview",
|
|
10
|
+
"doctor": "zero doctor",
|
|
11
|
+
"doctor:fix": "zero doctor --fix",
|
|
12
|
+
"doctor:ci": "zero doctor --ci"
|
|
10
13
|
},
|
|
11
14
|
"dependencies": {
|
|
12
|
-
"@pyreon/
|
|
13
|
-
"@pyreon/
|
|
14
|
-
"@
|
|
15
|
-
"@
|
|
16
|
-
"@
|
|
17
|
-
"
|
|
18
|
-
"@pyreon/server": "latest",
|
|
19
|
-
"@pyreon/zero": "latest"
|
|
15
|
+
"@pyreon/meta": "^0.2.0",
|
|
16
|
+
"@pyreon/zero": "^0.2.0",
|
|
17
|
+
"@tanstack/query-core": "^5.90.0",
|
|
18
|
+
"@tanstack/table-core": "^8.21.0",
|
|
19
|
+
"@tanstack/virtual-core": "^3.13.0",
|
|
20
|
+
"zod": "^4.0.0"
|
|
20
21
|
},
|
|
21
22
|
"devDependencies": {
|
|
22
|
-
"@pyreon/
|
|
23
|
-
"
|
|
23
|
+
"@pyreon/mcp": "^0.7.0",
|
|
24
|
+
"@pyreon/vite-plugin": "^0.7.0",
|
|
25
|
+
"@pyreon/zero-cli": "^0.2.0",
|
|
24
26
|
"typescript": "^5.9.3",
|
|
25
|
-
"vite": "^
|
|
27
|
+
"vite": "^8.0.0"
|
|
26
28
|
}
|
|
27
29
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { apiRoutes } from 'virtual:zero/api-routes'
|
|
2
|
+
import { routeMiddleware } from 'virtual:zero/route-middleware'
|
|
1
3
|
import { routes } from 'virtual:zero/routes'
|
|
2
4
|
import { createServer } from '@pyreon/zero'
|
|
3
5
|
import {
|
|
@@ -5,13 +7,19 @@ import {
|
|
|
5
7
|
securityHeaders,
|
|
6
8
|
varyEncoding,
|
|
7
9
|
} from '@pyreon/zero/cache'
|
|
10
|
+
import { corsMiddleware } from '@pyreon/zero/cors'
|
|
11
|
+
import { rateLimitMiddleware } from '@pyreon/zero/rate-limit'
|
|
8
12
|
|
|
9
13
|
export default createServer({
|
|
10
14
|
routes,
|
|
15
|
+
routeMiddleware,
|
|
16
|
+
apiRoutes,
|
|
11
17
|
config: {
|
|
12
18
|
ssr: { mode: 'stream' },
|
|
13
19
|
},
|
|
14
20
|
middleware: [
|
|
21
|
+
corsMiddleware(),
|
|
22
|
+
rateLimitMiddleware({ max: 100, window: 60, include: ['/api/*'] }),
|
|
15
23
|
securityHeaders(),
|
|
16
24
|
cacheMiddleware({ staleWhileRevalidate: 120 }),
|
|
17
25
|
varyEncoding(),
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { defineFeature } from '@pyreon/feature'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
export const posts = defineFeature({
|
|
5
|
+
name: 'posts',
|
|
6
|
+
schema: z.object({
|
|
7
|
+
title: z.string().min(3, 'Title must be at least 3 characters'),
|
|
8
|
+
body: z.string().min(10, 'Body must be at least 10 characters'),
|
|
9
|
+
published: z.boolean(),
|
|
10
|
+
}),
|
|
11
|
+
api: '/api/posts',
|
|
12
|
+
})
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useHead } from '@pyreon/head'
|
|
2
|
+
import type { MiddlewareContext } from '@pyreon/server'
|
|
2
3
|
import type { LoaderContext } from '@pyreon/zero'
|
|
3
4
|
|
|
4
5
|
export const meta = {
|
|
@@ -25,26 +26,15 @@ export function guard() {
|
|
|
25
26
|
}
|
|
26
27
|
|
|
27
28
|
/**
|
|
28
|
-
* Per-route middleware — runs on the server before
|
|
29
|
+
* Per-route middleware — runs on the server before rendering.
|
|
29
30
|
* Great for logging, auth checks, rate limiting per-route.
|
|
31
|
+
*
|
|
32
|
+
* Uses @pyreon/server's Middleware signature: (ctx) => Response | void
|
|
33
|
+
* Return a Response to short-circuit, or void to continue.
|
|
30
34
|
*/
|
|
31
|
-
export const middleware =
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
) => {
|
|
35
|
-
const start = Date.now()
|
|
36
|
-
const response = await next(request)
|
|
37
|
-
const duration = Date.now() - start
|
|
38
|
-
|
|
39
|
-
// Add server timing header
|
|
40
|
-
const headers = new Headers(response.headers)
|
|
41
|
-
headers.set('Server-Timing', `route;dur=${duration}`)
|
|
42
|
-
|
|
43
|
-
return new Response(response.body, {
|
|
44
|
-
status: response.status,
|
|
45
|
-
statusText: response.statusText,
|
|
46
|
-
headers,
|
|
47
|
-
})
|
|
35
|
+
export const middleware = (ctx: MiddlewareContext) => {
|
|
36
|
+
// Add server timing header to track route performance
|
|
37
|
+
ctx.headers.set('Server-Timing', `route;desc="Dashboard"`)
|
|
48
38
|
}
|
|
49
39
|
|
|
50
40
|
export async function loader(_ctx: LoaderContext) {
|
|
@@ -1,9 +1,17 @@
|
|
|
1
|
+
import { QueryClient, QueryClientProvider } from '@pyreon/query'
|
|
1
2
|
import { Link } from '@pyreon/zero/link'
|
|
2
3
|
import { ThemeToggle } from '@pyreon/zero/theme'
|
|
4
|
+
import { useAppStore } from '../stores/app'
|
|
5
|
+
|
|
6
|
+
const queryClient = new QueryClient({
|
|
7
|
+
defaultOptions: { queries: { staleTime: 30000 } },
|
|
8
|
+
})
|
|
3
9
|
|
|
4
10
|
export function layout(props: { children: any }) {
|
|
11
|
+
const { sidebarOpen, toggleSidebar } = useAppStore()
|
|
12
|
+
|
|
5
13
|
return (
|
|
6
|
-
|
|
14
|
+
<QueryClientProvider client={queryClient}>
|
|
7
15
|
<header class="app-header">
|
|
8
16
|
<div class="app-header-inner">
|
|
9
17
|
<Link href="/" class="app-logo">
|
|
@@ -34,6 +42,14 @@ export function layout(props: { children: any }) {
|
|
|
34
42
|
>
|
|
35
43
|
Dashboard
|
|
36
44
|
</Link>
|
|
45
|
+
<button
|
|
46
|
+
type="button"
|
|
47
|
+
class="sidebar-toggle"
|
|
48
|
+
onClick={toggleSidebar}
|
|
49
|
+
title="Toggle sidebar"
|
|
50
|
+
>
|
|
51
|
+
{() => (sidebarOpen() ? '◀' : '▶')}
|
|
52
|
+
</button>
|
|
37
53
|
<ThemeToggle class="theme-toggle" />
|
|
38
54
|
</nav>
|
|
39
55
|
</div>
|
|
@@ -44,6 +60,6 @@ export function layout(props: { children: any }) {
|
|
|
44
60
|
<footer class="app-footer">
|
|
45
61
|
Built with Pyreon Zero — signal-based, blazing fast.
|
|
46
62
|
</footer>
|
|
47
|
-
|
|
63
|
+
</QueryClientProvider>
|
|
48
64
|
)
|
|
49
65
|
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ApiContext } from '@pyreon/zero'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* API route example — /api/posts
|
|
5
|
+
*
|
|
6
|
+
* API routes are plain .ts files in src/routes/api/ that export
|
|
7
|
+
* HTTP method handlers: GET, POST, PUT, PATCH, DELETE, OPTIONS.
|
|
8
|
+
*
|
|
9
|
+
* They run on the server and return Response objects directly.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const POSTS = [
|
|
13
|
+
{ id: 1, title: 'Getting Started with Pyreon Zero', published: true },
|
|
14
|
+
{ id: 2, title: 'Understanding Signals', published: true },
|
|
15
|
+
{ id: 3, title: 'Server-Side Rendering Made Simple', published: false },
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
export function GET(_ctx: ApiContext) {
|
|
19
|
+
return Response.json(POSTS.filter((p) => p.published))
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function POST(ctx: ApiContext) {
|
|
23
|
+
const body = (await ctx.request.json()) as {
|
|
24
|
+
title: string
|
|
25
|
+
published?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!body.title) {
|
|
29
|
+
return Response.json({ error: 'Title is required' }, { status: 400 })
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const post = {
|
|
33
|
+
id: POSTS.length + 1,
|
|
34
|
+
title: body.title,
|
|
35
|
+
published: body.published ?? false,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
POSTS.push(post)
|
|
39
|
+
return Response.json(post, { status: 201 })
|
|
40
|
+
}
|
|
@@ -31,21 +31,21 @@ export default function Counter() {
|
|
|
31
31
|
<button
|
|
32
32
|
type="button"
|
|
33
33
|
class="btn btn-secondary"
|
|
34
|
-
onclick={() => count(
|
|
34
|
+
onclick={() => count.update((n) => n - 1)}
|
|
35
35
|
>
|
|
36
36
|
-
|
|
37
37
|
</button>
|
|
38
38
|
<button
|
|
39
39
|
type="button"
|
|
40
40
|
class="btn btn-primary"
|
|
41
|
-
onclick={() => count(0)}
|
|
41
|
+
onclick={() => count.set(0)}
|
|
42
42
|
>
|
|
43
43
|
Reset
|
|
44
44
|
</button>
|
|
45
45
|
<button
|
|
46
46
|
type="button"
|
|
47
47
|
class="btn btn-secondary"
|
|
48
|
-
onclick={() => count(
|
|
48
|
+
onclick={() => count.update((n) => n + 1)}
|
|
49
49
|
>
|
|
50
50
|
+
|
|
51
51
|
</button>
|
|
@@ -70,6 +70,13 @@ export default function PostsIndex() {
|
|
|
70
70
|
Each post is loaded via a <code>loader</code> function — server-side
|
|
71
71
|
data fetching that runs before the route renders.
|
|
72
72
|
</p>
|
|
73
|
+
<Link
|
|
74
|
+
href="/posts/new"
|
|
75
|
+
class="btn"
|
|
76
|
+
style="margin-top: var(--space-md);"
|
|
77
|
+
>
|
|
78
|
+
+ New Post
|
|
79
|
+
</Link>
|
|
73
80
|
</div>
|
|
74
81
|
|
|
75
82
|
<div class="posts-grid">
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useHead } from '@pyreon/head'
|
|
2
|
+
import { Link } from '@pyreon/zero/link'
|
|
3
|
+
import { posts } from '../../features/posts'
|
|
4
|
+
|
|
5
|
+
export default function NewPostPage() {
|
|
6
|
+
useHead({
|
|
7
|
+
title: 'New Post — Pyreon Zero',
|
|
8
|
+
meta: [{ name: 'description', content: 'Create a new post.' }],
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
const form = posts.useForm({
|
|
12
|
+
onSuccess: () => {
|
|
13
|
+
window.location.href = '/posts'
|
|
14
|
+
},
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<div class="post-form">
|
|
19
|
+
<Link
|
|
20
|
+
href="/posts"
|
|
21
|
+
style="color: var(--c-text-muted); font-size: 0.85rem; display: inline-block; margin-bottom: var(--space-lg);"
|
|
22
|
+
>
|
|
23
|
+
← Back to Posts
|
|
24
|
+
</Link>
|
|
25
|
+
|
|
26
|
+
<h1>New Post</h1>
|
|
27
|
+
<p style="color: var(--c-text-muted); margin-bottom: var(--space-xl);">
|
|
28
|
+
This form is powered by <code>@pyreon/feature</code> +{' '}
|
|
29
|
+
<code>@pyreon/form</code> + <code>@pyreon/validation</code> with
|
|
30
|
+
automatic Zod schema validation.
|
|
31
|
+
</p>
|
|
32
|
+
|
|
33
|
+
<form onSubmit={(e: Event) => form.handleSubmit(e)}>
|
|
34
|
+
<div class="form-field">
|
|
35
|
+
<label for="title">Title</label>
|
|
36
|
+
<input id="title" {...form.register('title', {})} />
|
|
37
|
+
</div>
|
|
38
|
+
<div class="form-field">
|
|
39
|
+
<label for="body">Body</label>
|
|
40
|
+
<textarea id="body" rows={6} {...form.register('body', {})} />
|
|
41
|
+
</div>
|
|
42
|
+
<div class="form-field">
|
|
43
|
+
<label class="checkbox-label">
|
|
44
|
+
<input
|
|
45
|
+
type="checkbox"
|
|
46
|
+
{...form.register('published', { type: 'checkbox' })}
|
|
47
|
+
/>
|
|
48
|
+
Published
|
|
49
|
+
</label>
|
|
50
|
+
</div>
|
|
51
|
+
<button type="submit" class="btn" disabled={form.isSubmitting()}>
|
|
52
|
+
{() => (form.isSubmitting() ? 'Creating...' : 'Create Post')}
|
|
53
|
+
</button>
|
|
54
|
+
</form>
|
|
55
|
+
</div>
|
|
56
|
+
)
|
|
57
|
+
}
|