@pyreon/create-zero 0.5.0 → 0.11.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/bin/create-zero.js +1 -1
- package/lib/index.js +2 -2
- package/lib/index.js.map +1 -1
- package/package.json +5 -4
- package/templates/default/env.d.ts +6 -6
- package/templates/default/index.html +1 -1
- package/templates/default/src/entry-client.ts +3 -3
- package/templates/default/src/entry-server.ts +9 -13
- package/templates/default/src/features/posts.ts +6 -6
- package/templates/default/src/routes/(admin)/dashboard.tsx +28 -33
- package/templates/default/src/routes/_error.tsx +5 -10
- package/templates/default/src/routes/_layout.tsx +8 -18
- package/templates/default/src/routes/about.tsx +25 -44
- package/templates/default/src/routes/api/health.ts +1 -1
- package/templates/default/src/routes/api/posts.ts +5 -5
- package/templates/default/src/routes/counter.tsx +18 -27
- package/templates/default/src/routes/index.tsx +20 -22
- package/templates/default/src/routes/posts/[id].tsx +34 -34
- package/templates/default/src/routes/posts/index.tsx +29 -46
- package/templates/default/src/routes/posts/new.tsx +12 -16
- package/templates/default/src/stores/app.ts +2 -2
- package/templates/default/vite.config.ts +10 -10
package/bin/create-zero.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import(
|
|
2
|
+
import("../lib/index.js")
|
package/lib/index.js
CHANGED
|
@@ -299,8 +299,8 @@ function generatePackageJson(config) {
|
|
|
299
299
|
const devDeps = {
|
|
300
300
|
"@pyreon/vite-plugin": pyreonVersion("@pyreon/vite-plugin"),
|
|
301
301
|
"@pyreon/zero-cli": pyreonVersion("@pyreon/zero-cli"),
|
|
302
|
-
|
|
303
|
-
|
|
302
|
+
typescript: "^5.9.3",
|
|
303
|
+
vite: "^7.0.0"
|
|
304
304
|
};
|
|
305
305
|
if (config.aiToolchain) devDeps["@pyreon/mcp"] = pyreonVersion("@pyreon/mcp");
|
|
306
306
|
const pkg = {
|
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'\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.11'\n // Zero framework packages\n if (pkg === '@pyreon/zero' || pkg === '@pyreon/meta' || pkg === '@pyreon/zero-cli' || pkg === '@pyreon/create-zero') return '^0.4.1'\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.10.0'\n // UI system\n return '^0.4.1'\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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"}
|
|
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 =\n argName ??\n (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 {\n value: \"ssr-stream\",\n label: \"SSR Streaming\",\n hint: \"recommended — progressive HTML with Suspense\",\n },\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 {\n value: \"meta\",\n label: \"@pyreon/meta (single barrel)\",\n hint: \"one import for everything — simpler, tree-shaken at build\",\n },\n {\n value: \"individual\",\n label: \"Individual packages\",\n hint: \"only install what you selected — smaller node_modules\",\n },\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([`cd ${config.name}`, \"bun install\", \"bun run dev\"].join(\"\\n\"), \"Next steps\")\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(join(config.targetDir, \"package.json\"), generatePackageJson(config))\n\n await writeFile(join(config.targetDir, \"vite.config.ts\"), generateViteConfig(config))\n\n await writeFile(join(config.targetDir, \"src/entry-server.ts\"), generateEntryServer(config))\n\n await writeFile(join(config.targetDir, \"env.d.ts\"), generateEnvDts(config))\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 = [\n \"core\",\n \"reactivity\",\n \"runtime-dom\",\n \"runtime-server\",\n \"server\",\n \"head\",\n \"router\",\n \"vite-plugin\",\n \"compiler\",\n \"cli\",\n \"mcp\",\n ]\n if (core.some((c) => pkg === `@pyreon/${c}`)) return \"^0.7.11\"\n // Zero framework packages\n if (\n pkg === \"@pyreon/zero\" ||\n pkg === \"@pyreon/meta\" ||\n pkg === \"@pyreon/zero-cli\" ||\n pkg === \"@pyreon/create-zero\"\n )\n return \"^0.4.1\"\n // Fundamentals\n const fundamentals = [\n \"store\",\n \"form\",\n \"validation\",\n \"query\",\n \"table\",\n \"virtual\",\n \"i18n\",\n \"feature\",\n \"machine\",\n \"permissions\",\n \"flow\",\n \"code\",\n ]\n if (fundamentals.some((f) => pkg === `@pyreon/${f}`)) return \"^0.10.0\"\n // UI system\n return \"^0.4.1\"\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\")\n ? \"^5.90.0\"\n : dep.includes(\"table\")\n ? \"^8.21.0\"\n : \"^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\")\n ? \"^5.90.0\"\n : dep.includes(\"table\")\n ? \"^8.21.0\"\n : \"^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(\n Object.entries(devDeps).sort(([a], [b]) => a.localeCompare(b)),\n ),\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,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/create-zero",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "Create a new Pyreon Zero project",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Vit Bokisch",
|
|
7
7
|
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
|
-
"url": "https://github.com/pyreon/
|
|
10
|
-
"directory": "packages/create-zero"
|
|
9
|
+
"url": "https://github.com/pyreon/pyreon.git",
|
|
10
|
+
"directory": "packages/zero/create-zero"
|
|
11
11
|
},
|
|
12
12
|
"type": "module",
|
|
13
13
|
"main": "./lib/index.js",
|
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
],
|
|
25
25
|
"scripts": {
|
|
26
26
|
"build": "vl_rolldown_build",
|
|
27
|
-
"typecheck": "tsc --noEmit"
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"lint": "biome check ."
|
|
28
29
|
},
|
|
29
30
|
"dependencies": {
|
|
30
31
|
"@clack/prompts": "^1.0.0"
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
/// <reference types="vite/client" />
|
|
2
2
|
|
|
3
|
-
declare module
|
|
4
|
-
import type { RouteRecord } from
|
|
3
|
+
declare module "virtual:zero/routes" {
|
|
4
|
+
import type { RouteRecord } from "@pyreon/router"
|
|
5
5
|
export const routes: RouteRecord[]
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
declare module
|
|
9
|
-
import type { RouteMiddlewareEntry } from
|
|
8
|
+
declare module "virtual:zero/route-middleware" {
|
|
9
|
+
import type { RouteMiddlewareEntry } from "@pyreon/zero"
|
|
10
10
|
export const routeMiddleware: RouteMiddlewareEntry[]
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
-
declare module
|
|
14
|
-
import type { ApiRouteEntry } from
|
|
13
|
+
declare module "virtual:zero/api-routes" {
|
|
14
|
+
import type { ApiRouteEntry } from "@pyreon/zero"
|
|
15
15
|
export const apiRoutes: ApiRouteEntry[]
|
|
16
16
|
}
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
<meta name="theme-color" content="#0a0a0b" />
|
|
7
7
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
8
8
|
<!-- Prevent flash of wrong theme -->
|
|
9
|
-
<script>(
|
|
9
|
+
<script>(()=> {try{var t=localStorage.getItem("zero-theme");var r=t==="light"?"light":t==="dark"?"dark":window.matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light";document.documentElement.dataset.theme=r}catch(e){}})()</script>
|
|
10
10
|
<!--pyreon-head-->
|
|
11
11
|
</head>
|
|
12
12
|
<body>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import { routes } from
|
|
3
|
-
import { startClient } from
|
|
1
|
+
import "./global.css"
|
|
2
|
+
import { routes } from "virtual:zero/routes"
|
|
3
|
+
import { startClient } from "@pyreon/zero/client"
|
|
4
4
|
|
|
5
5
|
startClient({ routes })
|
|
@@ -1,25 +1,21 @@
|
|
|
1
|
-
import { apiRoutes } from
|
|
2
|
-
import { routeMiddleware } from
|
|
3
|
-
import { routes } from
|
|
4
|
-
import { createServer } from
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
varyEncoding,
|
|
9
|
-
} from '@pyreon/zero/cache'
|
|
10
|
-
import { corsMiddleware } from '@pyreon/zero/cors'
|
|
11
|
-
import { rateLimitMiddleware } from '@pyreon/zero/rate-limit'
|
|
1
|
+
import { apiRoutes } from "virtual:zero/api-routes"
|
|
2
|
+
import { routeMiddleware } from "virtual:zero/route-middleware"
|
|
3
|
+
import { routes } from "virtual:zero/routes"
|
|
4
|
+
import { createServer } from "@pyreon/zero"
|
|
5
|
+
import { cacheMiddleware, securityHeaders, varyEncoding } from "@pyreon/zero/cache"
|
|
6
|
+
import { corsMiddleware } from "@pyreon/zero/cors"
|
|
7
|
+
import { rateLimitMiddleware } from "@pyreon/zero/rate-limit"
|
|
12
8
|
|
|
13
9
|
export default createServer({
|
|
14
10
|
routes,
|
|
15
11
|
routeMiddleware,
|
|
16
12
|
apiRoutes,
|
|
17
13
|
config: {
|
|
18
|
-
ssr: { mode:
|
|
14
|
+
ssr: { mode: "stream" },
|
|
19
15
|
},
|
|
20
16
|
middleware: [
|
|
21
17
|
corsMiddleware(),
|
|
22
|
-
rateLimitMiddleware({ max: 100, window: 60, include: [
|
|
18
|
+
rateLimitMiddleware({ max: 100, window: 60, include: ["/api/*"] }),
|
|
23
19
|
securityHeaders(),
|
|
24
20
|
cacheMiddleware({ staleWhileRevalidate: 120 }),
|
|
25
21
|
varyEncoding(),
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { defineFeature } from
|
|
2
|
-
import { z } from
|
|
1
|
+
import { defineFeature } from "@pyreon/feature"
|
|
2
|
+
import { z } from "zod"
|
|
3
3
|
|
|
4
4
|
export const posts = defineFeature({
|
|
5
|
-
name:
|
|
5
|
+
name: "posts",
|
|
6
6
|
schema: z.object({
|
|
7
|
-
title: z.string().min(3,
|
|
8
|
-
body: z.string().min(10,
|
|
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
9
|
published: z.boolean(),
|
|
10
10
|
}),
|
|
11
|
-
api:
|
|
11
|
+
api: "/api/posts",
|
|
12
12
|
})
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { useHead } from
|
|
2
|
-
import type { MiddlewareContext } from
|
|
3
|
-
import type { LoaderContext } from
|
|
1
|
+
import { useHead } from "@pyreon/head"
|
|
2
|
+
import type { MiddlewareContext } from "@pyreon/server"
|
|
3
|
+
import type { LoaderContext } from "@pyreon/zero"
|
|
4
4
|
|
|
5
5
|
export const meta = {
|
|
6
|
-
title:
|
|
6
|
+
title: "Dashboard — Pyreon Zero",
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -16,11 +16,10 @@ export const meta = {
|
|
|
16
16
|
export function guard() {
|
|
17
17
|
// Simulate auth check — in a real app, check session/token
|
|
18
18
|
const isAuthenticated =
|
|
19
|
-
typeof window !==
|
|
20
|
-
localStorage.getItem('zero-demo-auth') === 'true'
|
|
19
|
+
typeof window !== "undefined" && localStorage.getItem("zero-demo-auth") === "true"
|
|
21
20
|
|
|
22
21
|
if (!isAuthenticated) {
|
|
23
|
-
return
|
|
22
|
+
return "/about" // Redirect unauthenticated users
|
|
24
23
|
}
|
|
25
24
|
return true // Allow navigation
|
|
26
25
|
}
|
|
@@ -34,16 +33,16 @@ export function guard() {
|
|
|
34
33
|
*/
|
|
35
34
|
export const middleware = (ctx: MiddlewareContext) => {
|
|
36
35
|
// Add server timing header to track route performance
|
|
37
|
-
ctx.headers.set(
|
|
36
|
+
ctx.headers.set("Server-Timing", `route;desc="Dashboard"`)
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
export async function loader(_ctx: LoaderContext) {
|
|
41
40
|
return {
|
|
42
|
-
user:
|
|
41
|
+
user: "Demo User",
|
|
43
42
|
stats: {
|
|
44
43
|
views: 12_847,
|
|
45
44
|
routes: 6,
|
|
46
|
-
buildTime:
|
|
45
|
+
buildTime: "1.2s",
|
|
47
46
|
},
|
|
48
47
|
}
|
|
49
48
|
}
|
|
@@ -57,8 +56,8 @@ export default function Dashboard() {
|
|
|
57
56
|
<span class="badge">Protected Route</span>
|
|
58
57
|
<h1 style="margin-top: var(--space-md);">Dashboard</h1>
|
|
59
58
|
<p>
|
|
60
|
-
This route is protected by a <code>guard</code> function and uses
|
|
61
|
-
|
|
59
|
+
This route is protected by a <code>guard</code> function and uses per-route{" "}
|
|
60
|
+
<code>middleware</code> for server-side logging.
|
|
62
61
|
</p>
|
|
63
62
|
</div>
|
|
64
63
|
|
|
@@ -77,31 +76,27 @@ export default function Dashboard() {
|
|
|
77
76
|
</div>
|
|
78
77
|
</div>
|
|
79
78
|
|
|
80
|
-
<div
|
|
81
|
-
class="code-block"
|
|
82
|
-
style="max-width: 520px; margin: var(--space-2xl) auto 0;"
|
|
83
|
-
>
|
|
79
|
+
<div class="code-block" style="max-width: 520px; margin: var(--space-2xl) auto 0;">
|
|
84
80
|
<div class="code-block-header">
|
|
85
81
|
<span>dashboard.tsx</span>
|
|
86
82
|
</div>
|
|
87
83
|
<pre>
|
|
88
84
|
<code>
|
|
89
|
-
<span class="cm">{
|
|
90
|
-
<span class="kw">export function</span>{
|
|
91
|
-
<span class="
|
|
92
|
-
<span class="kw">
|
|
93
|
-
<span class="
|
|
94
|
-
|
|
95
|
-
{
|
|
96
|
-
<span class="
|
|
97
|
-
|
|
98
|
-
{
|
|
99
|
-
<span class="
|
|
100
|
-
|
|
101
|
-
<span class="
|
|
102
|
-
<span class="cm">{'// logging, auth, rate limiting...'}</span>
|
|
85
|
+
<span class="cm">{"// Navigation guard — protect routes"}</span>
|
|
86
|
+
<span class="kw">export function</span> <span class="fn">guard</span>() {"{"}
|
|
87
|
+
<span class="kw">if</span> (!isAuthenticated) {"{"}
|
|
88
|
+
<span class="kw">return</span> <span class="str">"/login"</span>{" "}
|
|
89
|
+
<span class="cm">{"// redirect"}</span>
|
|
90
|
+
{"}"}
|
|
91
|
+
<span class="kw">return</span> <span class="str">true</span>{" "}
|
|
92
|
+
<span class="cm">{"// allow"}</span>
|
|
93
|
+
{"}"}
|
|
94
|
+
<span class="cm">{"// Per-route server middleware"}</span>
|
|
95
|
+
<span class="kw">export const</span> <span class="fn">middleware</span> = (req, next)
|
|
96
|
+
=> {"{"}
|
|
97
|
+
<span class="cm">{"// logging, auth, rate limiting..."}</span>
|
|
103
98
|
<span class="kw">return</span> <span class="fn">next</span>(req)
|
|
104
|
-
{
|
|
99
|
+
{"}"}
|
|
105
100
|
</code>
|
|
106
101
|
</pre>
|
|
107
102
|
</div>
|
|
@@ -111,8 +106,8 @@ export default function Dashboard() {
|
|
|
111
106
|
type="button"
|
|
112
107
|
class="btn btn-secondary"
|
|
113
108
|
onClick={() => {
|
|
114
|
-
localStorage.removeItem(
|
|
115
|
-
window.location.href =
|
|
109
|
+
localStorage.removeItem("zero-demo-auth")
|
|
110
|
+
window.location.href = "/about"
|
|
116
111
|
}}
|
|
117
112
|
>
|
|
118
113
|
Log out (clear demo auth)
|
|
@@ -1,22 +1,17 @@
|
|
|
1
|
-
import { useHead } from
|
|
2
|
-
import { Link } from
|
|
1
|
+
import { useHead } from "@pyreon/head"
|
|
2
|
+
import { Link } from "@pyreon/zero/link"
|
|
3
3
|
|
|
4
4
|
export default function ErrorPage() {
|
|
5
|
-
useHead({ title:
|
|
5
|
+
useHead({ title: "Something went wrong — Zero" })
|
|
6
6
|
|
|
7
7
|
return (
|
|
8
8
|
<div class="error-page">
|
|
9
9
|
<div class="error-code">500</div>
|
|
10
10
|
<h1>Something went wrong</h1>
|
|
11
11
|
<p style="color: var(--c-text-secondary); max-width: 400px;">
|
|
12
|
-
An unexpected error occurred. Try refreshing the page or navigating back
|
|
13
|
-
home.
|
|
12
|
+
An unexpected error occurred. Try refreshing the page or navigating back home.
|
|
14
13
|
</p>
|
|
15
|
-
<Link
|
|
16
|
-
href="/"
|
|
17
|
-
class="btn btn-primary"
|
|
18
|
-
style="margin-top: var(--space-md);"
|
|
19
|
-
>
|
|
14
|
+
<Link href="/" class="btn btn-primary" style="margin-top: var(--space-md);">
|
|
20
15
|
Back to Home
|
|
21
16
|
</Link>
|
|
22
17
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { QueryClient, QueryClientProvider } from
|
|
2
|
-
import { Link } from
|
|
3
|
-
import { ThemeToggle } from
|
|
4
|
-
import { useAppStore } from
|
|
1
|
+
import { QueryClient, QueryClientProvider } from "@pyreon/query"
|
|
2
|
+
import { Link } from "@pyreon/zero/link"
|
|
3
|
+
import { ThemeToggle } from "@pyreon/zero/theme"
|
|
4
|
+
import { useAppStore } from "../stores/app"
|
|
5
5
|
|
|
6
6
|
const queryClient = new QueryClient({
|
|
7
7
|
defaultOptions: { queries: { staleTime: 30000 } },
|
|
@@ -23,11 +23,7 @@ export function layout(props: { children: any }) {
|
|
|
23
23
|
<Link href="/" prefetch="viewport" exactActiveClass="nav-active">
|
|
24
24
|
Home
|
|
25
25
|
</Link>
|
|
26
|
-
<Link
|
|
27
|
-
href="/counter"
|
|
28
|
-
prefetch="hover"
|
|
29
|
-
exactActiveClass="nav-active"
|
|
30
|
-
>
|
|
26
|
+
<Link href="/counter" prefetch="hover" exactActiveClass="nav-active">
|
|
31
27
|
Counter
|
|
32
28
|
</Link>
|
|
33
29
|
<Link href="/posts" prefetch="hover" activeClass="nav-active">
|
|
@@ -36,11 +32,7 @@ export function layout(props: { children: any }) {
|
|
|
36
32
|
<Link href="/about" prefetch="hover" exactActiveClass="nav-active">
|
|
37
33
|
About
|
|
38
34
|
</Link>
|
|
39
|
-
<Link
|
|
40
|
-
href="/dashboard"
|
|
41
|
-
prefetch="hover"
|
|
42
|
-
exactActiveClass="nav-active"
|
|
43
|
-
>
|
|
35
|
+
<Link href="/dashboard" prefetch="hover" exactActiveClass="nav-active">
|
|
44
36
|
Dashboard
|
|
45
37
|
</Link>
|
|
46
38
|
<button
|
|
@@ -49,7 +41,7 @@ export function layout(props: { children: any }) {
|
|
|
49
41
|
onClick={toggleSidebar}
|
|
50
42
|
title="Toggle sidebar"
|
|
51
43
|
>
|
|
52
|
-
{() => (sidebarOpen() ?
|
|
44
|
+
{() => (sidebarOpen() ? "◀" : "▶")}
|
|
53
45
|
</button>
|
|
54
46
|
<ThemeToggle class="theme-toggle" />
|
|
55
47
|
</nav>
|
|
@@ -58,9 +50,7 @@ export function layout(props: { children: any }) {
|
|
|
58
50
|
|
|
59
51
|
<main class="app-main">{props.children}</main>
|
|
60
52
|
|
|
61
|
-
<footer class="app-footer">
|
|
62
|
-
Built with Pyreon Zero — signal-based, blazing fast.
|
|
63
|
-
</footer>
|
|
53
|
+
<footer class="app-footer">Built with Pyreon Zero — signal-based, blazing fast.</footer>
|
|
64
54
|
</QueryClientProvider>
|
|
65
55
|
)
|
|
66
56
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { useHead } from
|
|
2
|
-
import { Link } from
|
|
1
|
+
import { useHead } from "@pyreon/head"
|
|
2
|
+
import { Link } from "@pyreon/zero/link"
|
|
3
3
|
|
|
4
4
|
export const meta = {
|
|
5
|
-
title:
|
|
6
|
-
description:
|
|
5
|
+
title: "About — Pyreon Zero",
|
|
6
|
+
description: "Learn about the Pyreon Zero meta-framework.",
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export default function About() {
|
|
10
10
|
useHead({
|
|
11
11
|
title: meta.title,
|
|
12
|
-
meta: [{ name:
|
|
12
|
+
meta: [{ name: "description", content: meta.description }],
|
|
13
13
|
})
|
|
14
14
|
|
|
15
15
|
return (
|
|
@@ -17,8 +17,8 @@ export default function About() {
|
|
|
17
17
|
<div class="page-header">
|
|
18
18
|
<h1>About Zero</h1>
|
|
19
19
|
<p>
|
|
20
|
-
A signal-based meta-framework built on Vite — designed for developers
|
|
21
|
-
|
|
20
|
+
A signal-based meta-framework built on Vite — designed for developers who care about
|
|
21
|
+
performance and simplicity.
|
|
22
22
|
</p>
|
|
23
23
|
</div>
|
|
24
24
|
|
|
@@ -43,20 +43,18 @@ export default function About() {
|
|
|
43
43
|
</h2>
|
|
44
44
|
<div style="color: var(--c-text-secondary); line-height: 1.8; display: flex; flex-direction: column; gap: var(--space-lg);">
|
|
45
45
|
<p>
|
|
46
|
-
Most frameworks make you choose between developer experience and
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
no reconciliation, no wasted work.
|
|
46
|
+
Most frameworks make you choose between developer experience and runtime performance.
|
|
47
|
+
Zero doesn't. Pyreon's signal-based reactivity means your components compile to surgical
|
|
48
|
+
DOM updates — no diffing, no reconciliation, no wasted work.
|
|
50
49
|
</p>
|
|
51
50
|
<p>
|
|
52
|
-
On top of that, Zero gives you file-based routing, server-side
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
and SEO utilities — all built in, all zero-config.
|
|
51
|
+
On top of that, Zero gives you file-based routing, server-side rendering, static
|
|
52
|
+
generation, incremental regeneration, font optimization, image optimization, smart
|
|
53
|
+
caching, link prefetching, and SEO utilities — all built in, all zero-config.
|
|
56
54
|
</p>
|
|
57
55
|
<p>
|
|
58
|
-
Deploy to Node, Bun, Vercel, Cloudflare, Netlify, or export as a
|
|
59
|
-
|
|
56
|
+
Deploy to Node, Bun, Vercel, Cloudflare, Netlify, or export as a static site. One
|
|
57
|
+
codebase, any target.
|
|
60
58
|
</p>
|
|
61
59
|
</div>
|
|
62
60
|
</section>
|
|
@@ -67,30 +65,15 @@ export default function About() {
|
|
|
67
65
|
</h2>
|
|
68
66
|
<div style="display: grid; gap: var(--space-sm);">
|
|
69
67
|
{[
|
|
70
|
-
[
|
|
71
|
-
[
|
|
72
|
-
[
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
],
|
|
76
|
-
[
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
],
|
|
80
|
-
[
|
|
81
|
-
'<Image>',
|
|
82
|
-
'Lazy loading, responsive srcset, blur-up placeholders',
|
|
83
|
-
],
|
|
84
|
-
['<Link>', 'Prefetch on hover or viewport entry for instant nav'],
|
|
85
|
-
[
|
|
86
|
-
'Font Plugin',
|
|
87
|
-
'Google Fonts optimization, preconnect, font-display:swap',
|
|
88
|
-
],
|
|
89
|
-
[
|
|
90
|
-
'Cache MW',
|
|
91
|
-
'Immutable hashed assets, stale-while-revalidate pages',
|
|
92
|
-
],
|
|
93
|
-
['SEO Tools', 'Sitemap, robots.txt, JSON-LD structured data'],
|
|
68
|
+
["Pyreon", "Signal-based UI framework with JSX"],
|
|
69
|
+
["Vite", "Lightning-fast dev server and optimized builds"],
|
|
70
|
+
["File Router", "Drop a file, get a route — layouts, guards, loaders"],
|
|
71
|
+
["SSR / SSG / ISR", "Every rendering strategy, per-route overrides"],
|
|
72
|
+
["<Image>", "Lazy loading, responsive srcset, blur-up placeholders"],
|
|
73
|
+
["<Link>", "Prefetch on hover or viewport entry for instant nav"],
|
|
74
|
+
["Font Plugin", "Google Fonts optimization, preconnect, font-display:swap"],
|
|
75
|
+
["Cache MW", "Immutable hashed assets, stale-while-revalidate pages"],
|
|
76
|
+
["SEO Tools", "Sitemap, robots.txt, JSON-LD structured data"],
|
|
94
77
|
].map(([name, desc]) => (
|
|
95
78
|
<div
|
|
96
79
|
class="card"
|
|
@@ -99,9 +82,7 @@ export default function About() {
|
|
|
99
82
|
<code style="min-width: 130px; font-weight: 600; color: var(--c-accent);">
|
|
100
83
|
{name}
|
|
101
84
|
</code>
|
|
102
|
-
<span style="color: var(--c-text-secondary); font-size: 0.9rem;">
|
|
103
|
-
{desc}
|
|
104
|
-
</span>
|
|
85
|
+
<span style="color: var(--c-text-secondary); font-size: 0.9rem;">{desc}</span>
|
|
105
86
|
</div>
|
|
106
87
|
))}
|
|
107
88
|
</div>
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ApiContext } from
|
|
1
|
+
import type { ApiContext } from "@pyreon/zero"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* API route example — /api/posts
|
|
@@ -10,9 +10,9 @@ import type { ApiContext } from '@pyreon/zero'
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const POSTS = [
|
|
13
|
-
{ id: 1, title:
|
|
14
|
-
{ id: 2, title:
|
|
15
|
-
{ id: 3, title:
|
|
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
16
|
]
|
|
17
17
|
|
|
18
18
|
export function GET(_ctx: ApiContext) {
|
|
@@ -26,7 +26,7 @@ export async function POST(ctx: ApiContext) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
if (!body.title) {
|
|
29
|
-
return Response.json({ error:
|
|
29
|
+
return Response.json({ error: "Title is required" }, { status: 400 })
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
const post = {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { useHead } from
|
|
2
|
-
import { computed, signal } from
|
|
1
|
+
import { useHead } from "@pyreon/head"
|
|
2
|
+
import { computed, signal } from "@pyreon/reactivity"
|
|
3
3
|
|
|
4
4
|
export const meta = {
|
|
5
|
-
title:
|
|
5
|
+
title: "Counter — Pyreon Zero",
|
|
6
6
|
description: "See Pyreon's signal-based reactivity in action.",
|
|
7
7
|
}
|
|
8
8
|
|
|
@@ -19,13 +19,13 @@ export default function Counter() {
|
|
|
19
19
|
<span class="badge">Interactive Demo</span>
|
|
20
20
|
<h1 style="margin-top: var(--space-md);">Signal Reactivity</h1>
|
|
21
21
|
<p>
|
|
22
|
-
Fine-grained reactivity with zero virtual DOM. Only the exact text
|
|
23
|
-
|
|
22
|
+
Fine-grained reactivity with zero virtual DOM. Only the exact text nodes that display
|
|
23
|
+
these values are updated — nothing else re-renders.
|
|
24
24
|
</p>
|
|
25
25
|
</div>
|
|
26
26
|
|
|
27
27
|
<div class="counter-demo">
|
|
28
|
-
<div class="counter-display">{count}</div>
|
|
28
|
+
<div class="counter-display">{() => count()}</div>
|
|
29
29
|
|
|
30
30
|
<div class="counter-controls">
|
|
31
31
|
<button
|
|
@@ -35,11 +35,7 @@ export default function Counter() {
|
|
|
35
35
|
>
|
|
36
36
|
-
|
|
37
37
|
</button>
|
|
38
|
-
<button
|
|
39
|
-
type="button"
|
|
40
|
-
class="btn btn-primary"
|
|
41
|
-
onClick={() => count.set(0)}
|
|
42
|
-
>
|
|
38
|
+
<button type="button" class="btn btn-primary" onClick={() => count.set(0)}>
|
|
43
39
|
Reset
|
|
44
40
|
</button>
|
|
45
41
|
<button
|
|
@@ -53,38 +49,33 @@ export default function Counter() {
|
|
|
53
49
|
|
|
54
50
|
<div class="counter-meta">
|
|
55
51
|
<div>
|
|
56
|
-
count() → <strong>{count}</strong>
|
|
52
|
+
count() → <strong>{() => count()}</strong>
|
|
57
53
|
</div>
|
|
58
54
|
<div>
|
|
59
|
-
doubled() → <strong>{doubled}</strong>
|
|
55
|
+
doubled() → <strong>{() => doubled()}</strong>
|
|
60
56
|
</div>
|
|
61
57
|
<div>
|
|
62
|
-
isEven() → <strong>{() => (isEven() ?
|
|
58
|
+
isEven() → <strong>{() => (isEven() ? "true" : "false")}</strong>
|
|
63
59
|
</div>
|
|
64
60
|
</div>
|
|
65
61
|
</div>
|
|
66
62
|
|
|
67
|
-
<div
|
|
68
|
-
class="code-block"
|
|
69
|
-
style="max-width: 520px; margin: var(--space-2xl) auto 0;"
|
|
70
|
-
>
|
|
63
|
+
<div class="code-block" style="max-width: 520px; margin: var(--space-2xl) auto 0;">
|
|
71
64
|
<div class="code-block-header">
|
|
72
65
|
<span>counter.tsx</span>
|
|
73
66
|
</div>
|
|
74
67
|
<pre>
|
|
75
68
|
<code>
|
|
76
|
-
<span class="kw">import</span> {
|
|
77
|
-
<span class="kw">from</span>{' '}
|
|
69
|
+
<span class="kw">import</span> {"{"} signal, computed {"}"} <span class="kw">from</span>{" "}
|
|
78
70
|
<span class="str">"@pyreon/reactivity"</span>
|
|
79
|
-
<span class="kw">const</span> <span class="fn">count</span> ={
|
|
80
|
-
<span class="fn">signal</span>(<span class="str">0</span>)
|
|
81
|
-
<span class="
|
|
82
|
-
<span class="fn">computed</span>(() =>{' '}
|
|
71
|
+
<span class="kw">const</span> <span class="fn">count</span> ={" "}
|
|
72
|
+
<span class="fn">signal</span>(<span class="str">0</span>)<span class="kw">const</span>{" "}
|
|
73
|
+
<span class="fn">doubled</span> = <span class="fn">computed</span>(() =>{" "}
|
|
83
74
|
<span class="fn">count</span>() * <span class="str">2</span>)
|
|
84
|
-
<span class="cm">{
|
|
85
|
-
<span class="cm">{
|
|
75
|
+
<span class="cm">{"// Just reference the signal in JSX —"}</span>
|
|
76
|
+
<span class="cm">{"// only this text node updates"}</span>
|
|
86
77
|
<span class="tag"><span></span>
|
|
87
|
-
{
|
|
78
|
+
{"{"}count{"}"}
|
|
88
79
|
<span class="tag"></span></span>
|
|
89
80
|
</code>
|
|
90
81
|
</pre>
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import { useHead } from
|
|
2
|
-
import { Link } from
|
|
1
|
+
import { useHead } from "@pyreon/head"
|
|
2
|
+
import { Link } from "@pyreon/zero/link"
|
|
3
3
|
|
|
4
4
|
export const meta = {
|
|
5
|
-
title:
|
|
6
|
-
description:
|
|
5
|
+
title: "Pyreon Zero",
|
|
6
|
+
description: "The signal-based meta-framework. Build fast, stay fast.",
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
export default function Home() {
|
|
10
10
|
useHead({
|
|
11
|
-
title:
|
|
12
|
-
meta: [{ name:
|
|
11
|
+
title: "Pyreon Zero — The Signal-Based Meta-Framework",
|
|
12
|
+
meta: [{ name: "description", content: meta.description }],
|
|
13
13
|
})
|
|
14
14
|
|
|
15
15
|
return (
|
|
@@ -22,9 +22,8 @@ export default function Home() {
|
|
|
22
22
|
<span class="gradient">Stay fast.</span>
|
|
23
23
|
</h1>
|
|
24
24
|
<p>
|
|
25
|
-
Pyreon Zero is a signal-based full-stack framework powered by Vite.
|
|
26
|
-
|
|
27
|
-
don't.
|
|
25
|
+
Pyreon Zero is a signal-based full-stack framework powered by Vite. File-based routing,
|
|
26
|
+
SSR, SSG, ISR — everything you need, nothing you don't.
|
|
28
27
|
</p>
|
|
29
28
|
<div class="hero-actions">
|
|
30
29
|
<Link href="/counter" class="btn btn-primary">
|
|
@@ -75,8 +74,8 @@ export default function Home() {
|
|
|
75
74
|
</div>
|
|
76
75
|
<h3>Signal-Based Reactivity</h3>
|
|
77
76
|
<p>
|
|
78
|
-
Fine-grained reactivity with zero virtual DOM overhead. Only the
|
|
79
|
-
|
|
77
|
+
Fine-grained reactivity with zero virtual DOM overhead. Only the exact DOM nodes that
|
|
78
|
+
need updating are touched.
|
|
80
79
|
</p>
|
|
81
80
|
</div>
|
|
82
81
|
|
|
@@ -98,8 +97,8 @@ export default function Home() {
|
|
|
98
97
|
</div>
|
|
99
98
|
<h3>File-Based Routing</h3>
|
|
100
99
|
<p>
|
|
101
|
-
Drop a file in <code>src/routes/</code> and it's a route. Layouts,
|
|
102
|
-
|
|
100
|
+
Drop a file in <code>src/routes/</code> and it's a route. Layouts, dynamic params,
|
|
101
|
+
catch-alls, and route groups built in.
|
|
103
102
|
</p>
|
|
104
103
|
</div>
|
|
105
104
|
|
|
@@ -123,8 +122,8 @@ export default function Home() {
|
|
|
123
122
|
</div>
|
|
124
123
|
<h3>SSR / SSG / ISR / SPA</h3>
|
|
125
124
|
<p>
|
|
126
|
-
Every rendering strategy out of the box. Per-route overrides let you
|
|
127
|
-
|
|
125
|
+
Every rendering strategy out of the box. Per-route overrides let you mix SSR pages with
|
|
126
|
+
static marketing pages.
|
|
128
127
|
</p>
|
|
129
128
|
</div>
|
|
130
129
|
|
|
@@ -146,9 +145,8 @@ export default function Home() {
|
|
|
146
145
|
</div>
|
|
147
146
|
<h3>Font & Image Optimization</h3>
|
|
148
147
|
<p>
|
|
149
|
-
Automatic Google Fonts inlining, font-display swap, and an{
|
|
150
|
-
|
|
151
|
-
placeholders.
|
|
148
|
+
Automatic Google Fonts inlining, font-display swap, and an <code>{"<Image>"}</code>{" "}
|
|
149
|
+
component with lazy loading and blur-up placeholders.
|
|
152
150
|
</p>
|
|
153
151
|
</div>
|
|
154
152
|
|
|
@@ -170,8 +168,8 @@ export default function Home() {
|
|
|
170
168
|
</div>
|
|
171
169
|
<h3>Smart Caching & Security</h3>
|
|
172
170
|
<p>
|
|
173
|
-
Built-in middleware for immutable asset caching,
|
|
174
|
-
|
|
171
|
+
Built-in middleware for immutable asset caching, stale-while-revalidate, security
|
|
172
|
+
headers, and compression hints.
|
|
175
173
|
</p>
|
|
176
174
|
</div>
|
|
177
175
|
|
|
@@ -195,8 +193,8 @@ export default function Home() {
|
|
|
195
193
|
</div>
|
|
196
194
|
<h3>Deploy Anywhere</h3>
|
|
197
195
|
<p>
|
|
198
|
-
Adapters for Node, Bun, Vercel, Cloudflare, and Netlify. Or export
|
|
199
|
-
|
|
196
|
+
Adapters for Node, Bun, Vercel, Cloudflare, and Netlify. Or export as a fully static
|
|
197
|
+
site.
|
|
200
198
|
</p>
|
|
201
199
|
</div>
|
|
202
200
|
</section>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { useHead } from
|
|
2
|
-
import { useLoaderData } from
|
|
3
|
-
import type { LoaderContext } from
|
|
4
|
-
import { Link } from
|
|
1
|
+
import { useHead } from "@pyreon/head"
|
|
2
|
+
import { useLoaderData } from "@pyreon/router"
|
|
3
|
+
import type { LoaderContext } from "@pyreon/zero"
|
|
4
|
+
import { Link } from "@pyreon/zero/link"
|
|
5
5
|
|
|
6
6
|
interface Post {
|
|
7
7
|
id: number
|
|
@@ -13,52 +13,52 @@ interface Post {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
const POSTS: Record<string, Post> = {
|
|
16
|
-
|
|
16
|
+
"1": {
|
|
17
17
|
id: 1,
|
|
18
|
-
title:
|
|
19
|
-
excerpt:
|
|
18
|
+
title: "Getting Started with Pyreon Zero",
|
|
19
|
+
excerpt: "Learn how to build your first app.",
|
|
20
20
|
body: "Pyreon Zero makes it incredibly easy to build modern web applications. Start by creating a new project with `bun create zero my-app`, then drop your first route file into `src/routes/`. The file-based router automatically picks up new files and generates the route tree for you.\n\nEvery route can export a `loader` for server-side data fetching, a `guard` for navigation protection, and `meta` for SEO metadata. The component itself uses JSX with Pyreon's signal-based reactivity — no virtual DOM overhead, just surgical DOM updates.",
|
|
21
|
-
author:
|
|
22
|
-
date:
|
|
21
|
+
author: "Zero Team",
|
|
22
|
+
date: "2026-03-01",
|
|
23
23
|
},
|
|
24
|
-
|
|
24
|
+
"2": {
|
|
25
25
|
id: 2,
|
|
26
|
-
title:
|
|
27
|
-
excerpt:
|
|
26
|
+
title: "Understanding Signals",
|
|
27
|
+
excerpt: "Deep dive into fine-grained reactivity.",
|
|
28
28
|
body: "Signals are the foundation of Pyreon's reactivity system. Unlike the virtual DOM approach used by React, signals track exactly which DOM nodes depend on which pieces of state. When a signal changes, only the precise text nodes or attributes that reference it are updated.\n\nThis means your app does zero diffing, zero reconciliation, and zero unnecessary re-renders. A counter component that updates a number on screen? Only that one text node is touched. Everything else stays completely untouched.",
|
|
29
|
-
author:
|
|
30
|
-
date:
|
|
29
|
+
author: "Zero Team",
|
|
30
|
+
date: "2026-03-05",
|
|
31
31
|
},
|
|
32
|
-
|
|
32
|
+
"3": {
|
|
33
33
|
id: 3,
|
|
34
|
-
title:
|
|
35
|
-
excerpt:
|
|
36
|
-
body:
|
|
37
|
-
author:
|
|
38
|
-
date:
|
|
34
|
+
title: "Server-Side Rendering Made Simple",
|
|
35
|
+
excerpt: "Pick the right strategy for every page.",
|
|
36
|
+
body: "Zero supports four rendering strategies out of the box: SSR (server-side rendering), SSG (static site generation), ISR (incremental static regeneration), and SPA (single-page application). You can set a default mode in your config and override it per-route.\n\nSSR renders fresh HTML on every request. SSG pre-renders pages at build time. ISR combines both — serving cached static pages while revalidating in the background. SPA skips server rendering entirely for fully client-side pages.",
|
|
37
|
+
author: "Zero Team",
|
|
38
|
+
date: "2026-03-08",
|
|
39
39
|
},
|
|
40
|
-
|
|
40
|
+
"4": {
|
|
41
41
|
id: 4,
|
|
42
|
-
title:
|
|
43
|
-
excerpt:
|
|
42
|
+
title: "Deploying to Production",
|
|
43
|
+
excerpt: "From build to production with any platform.",
|
|
44
44
|
body: "Zero's adapter system makes deployment straightforward. Choose your target platform — Node.js, Bun, Vercel, Cloudflare Workers, or Netlify — and Zero generates the right output for that platform.\n\nThe Node adapter creates a standalone HTTP server. The Bun adapter leverages Bun.serve() for maximum performance. The static adapter exports a fully pre-rendered site ready for any CDN or static hosting platform.",
|
|
45
|
-
author:
|
|
46
|
-
date:
|
|
45
|
+
author: "Zero Team",
|
|
46
|
+
date: "2026-03-10",
|
|
47
47
|
},
|
|
48
|
-
|
|
48
|
+
"5": {
|
|
49
49
|
id: 5,
|
|
50
|
-
title:
|
|
51
|
-
excerpt:
|
|
52
|
-
body:
|
|
53
|
-
author:
|
|
54
|
-
date:
|
|
50
|
+
title: "Optimizing Performance",
|
|
51
|
+
excerpt: "Built-in performance features.",
|
|
52
|
+
body: "Zero includes a comprehensive performance toolkit. The font plugin automatically optimizes Google Fonts with preconnect hints and font-display:swap. The <Image> component provides lazy loading with IntersectionObserver, responsive srcset, and blur-up placeholders.\n\nThe <Link> component prefetches routes on hover or viewport entry, making navigation feel instant. Built-in cache middleware sets optimal Cache-Control headers — immutable caching for hashed assets, stale-while-revalidate for pages.",
|
|
53
|
+
author: "Zero Team",
|
|
54
|
+
date: "2026-03-12",
|
|
55
55
|
},
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
export async function loader(ctx: LoaderContext) {
|
|
59
59
|
await new Promise((r) => setTimeout(r, 50))
|
|
60
60
|
const post = POSTS[ctx.params.id]
|
|
61
|
-
if (!post) throw new Error(
|
|
61
|
+
if (!post) throw new Error("Post not found")
|
|
62
62
|
return { post }
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -68,7 +68,7 @@ export default function PostDetail() {
|
|
|
68
68
|
|
|
69
69
|
useHead({
|
|
70
70
|
title: `${post.title} — Pyreon Zero`,
|
|
71
|
-
meta: [{ name:
|
|
71
|
+
meta: [{ name: "description", content: post.excerpt }],
|
|
72
72
|
})
|
|
73
73
|
|
|
74
74
|
return (
|
|
@@ -88,7 +88,7 @@ export default function PostDetail() {
|
|
|
88
88
|
</div>
|
|
89
89
|
|
|
90
90
|
<div class="post-body">
|
|
91
|
-
{post.body.split(
|
|
91
|
+
{post.body.split("\n\n").map((paragraph) => (
|
|
92
92
|
<p style="margin-bottom: var(--space-lg);">{paragraph}</p>
|
|
93
93
|
))}
|
|
94
94
|
</div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { useHead } from
|
|
2
|
-
import { useLoaderData } from
|
|
3
|
-
import type { LoaderContext } from
|
|
4
|
-
import { Link } from
|
|
1
|
+
import { useHead } from "@pyreon/head"
|
|
2
|
+
import { useLoaderData } from "@pyreon/router"
|
|
3
|
+
import type { LoaderContext } from "@pyreon/zero"
|
|
4
|
+
import { Link } from "@pyreon/zero/link"
|
|
5
5
|
|
|
6
6
|
interface Post {
|
|
7
7
|
id: number
|
|
@@ -12,33 +12,29 @@ interface Post {
|
|
|
12
12
|
const POSTS: Post[] = [
|
|
13
13
|
{
|
|
14
14
|
id: 1,
|
|
15
|
-
title:
|
|
16
|
-
excerpt:
|
|
17
|
-
'Learn how to build your first app with file-based routing, SSR, and signals.',
|
|
15
|
+
title: "Getting Started with Pyreon Zero",
|
|
16
|
+
excerpt: "Learn how to build your first app with file-based routing, SSR, and signals.",
|
|
18
17
|
},
|
|
19
18
|
{
|
|
20
19
|
id: 2,
|
|
21
|
-
title:
|
|
22
|
-
excerpt:
|
|
23
|
-
'Deep dive into fine-grained reactivity — how signals replace the virtual DOM.',
|
|
20
|
+
title: "Understanding Signals",
|
|
21
|
+
excerpt: "Deep dive into fine-grained reactivity — how signals replace the virtual DOM.",
|
|
24
22
|
},
|
|
25
23
|
{
|
|
26
24
|
id: 3,
|
|
27
|
-
title:
|
|
28
|
-
excerpt:
|
|
29
|
-
'SSR, SSG, ISR — pick the right strategy for every page in your app.',
|
|
25
|
+
title: "Server-Side Rendering Made Simple",
|
|
26
|
+
excerpt: "SSR, SSG, ISR — pick the right strategy for every page in your app.",
|
|
30
27
|
},
|
|
31
28
|
{
|
|
32
29
|
id: 4,
|
|
33
|
-
title:
|
|
34
|
-
excerpt:
|
|
35
|
-
'From bun build to production with Node, Bun, Vercel, or Cloudflare adapters.',
|
|
30
|
+
title: "Deploying to Production",
|
|
31
|
+
excerpt: "From bun build to production with Node, Bun, Vercel, or Cloudflare adapters.",
|
|
36
32
|
},
|
|
37
33
|
{
|
|
38
34
|
id: 5,
|
|
39
|
-
title:
|
|
35
|
+
title: "Optimizing Performance",
|
|
40
36
|
excerpt:
|
|
41
|
-
|
|
37
|
+
"Font loading, image optimization, smart caching, and link prefetching out of the box.",
|
|
42
38
|
},
|
|
43
39
|
]
|
|
44
40
|
|
|
@@ -52,10 +48,10 @@ export default function PostsIndex() {
|
|
|
52
48
|
const data = useLoaderData<{ posts: Post[] }>()
|
|
53
49
|
|
|
54
50
|
useHead({
|
|
55
|
-
title:
|
|
51
|
+
title: "Posts — Pyreon Zero",
|
|
56
52
|
meta: [
|
|
57
53
|
{
|
|
58
|
-
name:
|
|
54
|
+
name: "description",
|
|
59
55
|
content: "Example posts showcasing Zero's data loading.",
|
|
60
56
|
},
|
|
61
57
|
],
|
|
@@ -67,25 +63,17 @@ export default function PostsIndex() {
|
|
|
67
63
|
<span class="badge">Data Loading</span>
|
|
68
64
|
<h1 style="margin-top: var(--space-md);">Posts</h1>
|
|
69
65
|
<p>
|
|
70
|
-
Each post is loaded via a <code>loader</code> function — server-side
|
|
71
|
-
|
|
66
|
+
Each post is loaded via a <code>loader</code> function — server-side data fetching that
|
|
67
|
+
runs before the route renders.
|
|
72
68
|
</p>
|
|
73
|
-
<Link
|
|
74
|
-
href="/posts/new"
|
|
75
|
-
class="btn"
|
|
76
|
-
style="margin-top: var(--space-md);"
|
|
77
|
-
>
|
|
69
|
+
<Link href="/posts/new" class="btn" style="margin-top: var(--space-md);">
|
|
78
70
|
+ New Post
|
|
79
71
|
</Link>
|
|
80
72
|
</div>
|
|
81
73
|
|
|
82
74
|
<div class="posts-grid">
|
|
83
75
|
{data.posts.map((post) => (
|
|
84
|
-
<Link
|
|
85
|
-
href={`/posts/${post.id}`}
|
|
86
|
-
class="card post-card"
|
|
87
|
-
prefetch="hover"
|
|
88
|
-
>
|
|
76
|
+
<Link href={`/posts/${post.id}`} class="card post-card" prefetch="hover">
|
|
89
77
|
<span class="post-id">#{post.id}</span>
|
|
90
78
|
<div>
|
|
91
79
|
<h3>{post.title}</h3>
|
|
@@ -95,25 +83,20 @@ export default function PostsIndex() {
|
|
|
95
83
|
))}
|
|
96
84
|
</div>
|
|
97
85
|
|
|
98
|
-
<div
|
|
99
|
-
class="code-block"
|
|
100
|
-
style="max-width: 520px; margin: var(--space-2xl) auto 0;"
|
|
101
|
-
>
|
|
86
|
+
<div class="code-block" style="max-width: 520px; margin: var(--space-2xl) auto 0;">
|
|
102
87
|
<div class="code-block-header">
|
|
103
88
|
<span>posts/index.tsx</span>
|
|
104
89
|
</div>
|
|
105
90
|
<pre>
|
|
106
91
|
<code>
|
|
107
|
-
<span class="cm">{
|
|
108
|
-
<span class="kw">export async function</span>{
|
|
109
|
-
<span class="
|
|
110
|
-
<span class="
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
{
|
|
114
|
-
<span class="
|
|
115
|
-
<span class="kw">const</span> data ={' '}
|
|
116
|
-
<span class="fn">useLoaderData</span>()
|
|
92
|
+
<span class="cm">{"// Export a loader — runs on the server"}</span>
|
|
93
|
+
<span class="kw">export async function</span> <span class="fn">loader</span>(ctx) {"{"}
|
|
94
|
+
<span class="kw">const</span> posts = <span class="kw">await</span> db.posts.
|
|
95
|
+
<span class="fn">findMany</span>()
|
|
96
|
+
<span class="kw">return</span> {"{"} posts {"}"}
|
|
97
|
+
{"}"}
|
|
98
|
+
<span class="cm">{"// Access data in the component"}</span>
|
|
99
|
+
<span class="kw">const</span> data = <span class="fn">useLoaderData</span>()
|
|
117
100
|
</code>
|
|
118
101
|
</pre>
|
|
119
102
|
</div>
|
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { useHead } from
|
|
2
|
-
import { Link } from
|
|
3
|
-
import { posts } from
|
|
1
|
+
import { useHead } from "@pyreon/head"
|
|
2
|
+
import { Link } from "@pyreon/zero/link"
|
|
3
|
+
import { posts } from "../../features/posts"
|
|
4
4
|
|
|
5
5
|
export default function NewPostPage() {
|
|
6
6
|
useHead({
|
|
7
|
-
title:
|
|
8
|
-
meta: [{ name:
|
|
7
|
+
title: "New Post — Pyreon Zero",
|
|
8
|
+
meta: [{ name: "description", content: "Create a new post." }],
|
|
9
9
|
})
|
|
10
10
|
|
|
11
11
|
const form = posts.useForm({
|
|
12
12
|
onSuccess: () => {
|
|
13
|
-
window.location.href =
|
|
13
|
+
window.location.href = "/posts"
|
|
14
14
|
},
|
|
15
15
|
})
|
|
16
16
|
|
|
@@ -25,31 +25,27 @@ export default function NewPostPage() {
|
|
|
25
25
|
|
|
26
26
|
<h1>New Post</h1>
|
|
27
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/
|
|
30
|
-
automatic Zod schema validation.
|
|
28
|
+
This form is powered by <code>@pyreon/feature</code> + <code>@pyreon/form</code> +{" "}
|
|
29
|
+
<code>@pyreon/validation</code> with automatic Zod schema validation.
|
|
31
30
|
</p>
|
|
32
31
|
|
|
33
32
|
<form onSubmit={(e: Event) => form.handleSubmit(e)}>
|
|
34
33
|
<div class="form-field">
|
|
35
34
|
<label for="title">Title</label>
|
|
36
|
-
<input id="title" {...(form.register(
|
|
35
|
+
<input id="title" {...(form.register("title") as any)} />
|
|
37
36
|
</div>
|
|
38
37
|
<div class="form-field">
|
|
39
38
|
<label for="body">Body</label>
|
|
40
|
-
<textarea id="body" rows={6} {...(form.register(
|
|
39
|
+
<textarea id="body" rows={6} {...(form.register("body") as any)} />
|
|
41
40
|
</div>
|
|
42
41
|
<div class="form-field">
|
|
43
42
|
<label class="checkbox-label">
|
|
44
|
-
<input
|
|
45
|
-
type="checkbox"
|
|
46
|
-
{...(form.register('published', { type: 'checkbox' }) as any)}
|
|
47
|
-
/>
|
|
43
|
+
<input type="checkbox" {...(form.register("published", { type: "checkbox" }) as any)} />
|
|
48
44
|
Published
|
|
49
45
|
</label>
|
|
50
46
|
</div>
|
|
51
47
|
<button type="submit" class="btn" disabled={form.isSubmitting()}>
|
|
52
|
-
{() => (form.isSubmitting() ?
|
|
48
|
+
{() => (form.isSubmitting() ? "Creating..." : "Create Post")}
|
|
53
49
|
</button>
|
|
54
50
|
</form>
|
|
55
51
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { defineStore, signal } from
|
|
1
|
+
import { defineStore, signal } from "@pyreon/store"
|
|
2
2
|
|
|
3
|
-
export const useAppStore = defineStore(
|
|
3
|
+
export const useAppStore = defineStore("app", () => {
|
|
4
4
|
const sidebarOpen = signal(true)
|
|
5
5
|
const toggleSidebar = () => sidebarOpen.update((v) => !v)
|
|
6
6
|
return { sidebarOpen, toggleSidebar }
|
|
@@ -1,28 +1,28 @@
|
|
|
1
|
-
import pyreon from
|
|
2
|
-
import zero from
|
|
3
|
-
import { fontPlugin } from
|
|
4
|
-
import { seoPlugin } from
|
|
1
|
+
import pyreon from "@pyreon/vite-plugin"
|
|
2
|
+
import zero from "@pyreon/zero"
|
|
3
|
+
import { fontPlugin } from "@pyreon/zero/font"
|
|
4
|
+
import { seoPlugin } from "@pyreon/zero/seo"
|
|
5
5
|
|
|
6
6
|
export default {
|
|
7
7
|
plugins: [
|
|
8
8
|
pyreon(),
|
|
9
|
-
zero({ mode:
|
|
9
|
+
zero({ mode: "ssr", ssr: { mode: "stream" } }),
|
|
10
10
|
|
|
11
11
|
// Google Fonts — self-hosted at build time, CDN in dev
|
|
12
12
|
fontPlugin({
|
|
13
|
-
google: [
|
|
13
|
+
google: ["Inter:wght@400;500;600;700;800", "JetBrains Mono:wght@400"],
|
|
14
14
|
// Size-adjusted fallbacks to eliminate CLS while fonts load
|
|
15
15
|
fallbacks: {
|
|
16
|
-
Inter: { fallback:
|
|
16
|
+
Inter: { fallback: "Arial", sizeAdjust: 1.07, ascentOverride: 90 },
|
|
17
17
|
},
|
|
18
18
|
}),
|
|
19
19
|
|
|
20
20
|
// Generate sitemap.xml and robots.txt at build time
|
|
21
21
|
seoPlugin({
|
|
22
|
-
sitemap: { origin:
|
|
22
|
+
sitemap: { origin: "https://example.com" },
|
|
23
23
|
robots: {
|
|
24
|
-
rules: [{ userAgent:
|
|
25
|
-
sitemap:
|
|
24
|
+
rules: [{ userAgent: "*", allow: ["/"] }],
|
|
25
|
+
sitemap: "https://example.com/sitemap.xml",
|
|
26
26
|
},
|
|
27
27
|
}),
|
|
28
28
|
],
|