@pyreon/zero 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (99) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +53 -0
  3. package/lib/cache.js +80 -0
  4. package/lib/cache.js.map +1 -0
  5. package/lib/client.js +58 -0
  6. package/lib/client.js.map +1 -0
  7. package/lib/config.js +35 -0
  8. package/lib/config.js.map +1 -0
  9. package/lib/font.js +251 -0
  10. package/lib/font.js.map +1 -0
  11. package/lib/fs-router-BkbIWqek.js +30 -0
  12. package/lib/fs-router-BkbIWqek.js.map +1 -0
  13. package/lib/fs-router-jfd1QGLB.js +261 -0
  14. package/lib/fs-router-jfd1QGLB.js.map +1 -0
  15. package/lib/image-plugin.js +289 -0
  16. package/lib/image-plugin.js.map +1 -0
  17. package/lib/image.js +113 -0
  18. package/lib/image.js.map +1 -0
  19. package/lib/index.js +1665 -0
  20. package/lib/index.js.map +1 -0
  21. package/lib/link.js +186 -0
  22. package/lib/link.js.map +1 -0
  23. package/lib/script.js +102 -0
  24. package/lib/script.js.map +1 -0
  25. package/lib/seo.js +136 -0
  26. package/lib/seo.js.map +1 -0
  27. package/lib/theme.js +165 -0
  28. package/lib/theme.js.map +1 -0
  29. package/lib/types/adapters/bun.d.ts +6 -0
  30. package/lib/types/adapters/bun.d.ts.map +1 -0
  31. package/lib/types/adapters/index.d.ts +10 -0
  32. package/lib/types/adapters/index.d.ts.map +1 -0
  33. package/lib/types/adapters/node.d.ts +6 -0
  34. package/lib/types/adapters/node.d.ts.map +1 -0
  35. package/lib/types/adapters/static.d.ts +7 -0
  36. package/lib/types/adapters/static.d.ts.map +1 -0
  37. package/lib/types/app.d.ts +24 -0
  38. package/lib/types/app.d.ts.map +1 -0
  39. package/lib/types/cache.d.ts +54 -0
  40. package/lib/types/cache.d.ts.map +1 -0
  41. package/lib/types/client.d.ts +19 -0
  42. package/lib/types/client.d.ts.map +1 -0
  43. package/lib/types/config.d.ts +18 -0
  44. package/lib/types/config.d.ts.map +1 -0
  45. package/lib/types/entry-server.d.ts +26 -0
  46. package/lib/types/entry-server.d.ts.map +1 -0
  47. package/lib/types/font.d.ts +119 -0
  48. package/lib/types/font.d.ts.map +1 -0
  49. package/lib/types/fs-router.d.ts +33 -0
  50. package/lib/types/fs-router.d.ts.map +1 -0
  51. package/lib/types/image-plugin.d.ts +79 -0
  52. package/lib/types/image-plugin.d.ts.map +1 -0
  53. package/lib/types/image.d.ts +50 -0
  54. package/lib/types/image.d.ts.map +1 -0
  55. package/lib/types/index.d.ts +27 -0
  56. package/lib/types/index.d.ts.map +1 -0
  57. package/lib/types/isr.d.ts +9 -0
  58. package/lib/types/isr.d.ts.map +1 -0
  59. package/lib/types/link.d.ts +116 -0
  60. package/lib/types/link.d.ts.map +1 -0
  61. package/lib/types/script.d.ts +34 -0
  62. package/lib/types/script.d.ts.map +1 -0
  63. package/lib/types/seo.d.ts +88 -0
  64. package/lib/types/seo.d.ts.map +1 -0
  65. package/lib/types/theme.d.ts +38 -0
  66. package/lib/types/theme.d.ts.map +1 -0
  67. package/lib/types/types.d.ts +104 -0
  68. package/lib/types/types.d.ts.map +1 -0
  69. package/lib/types/utils/use-intersection-observer.d.ts +10 -0
  70. package/lib/types/utils/use-intersection-observer.d.ts.map +1 -0
  71. package/lib/types/utils/with-headers.d.ts +6 -0
  72. package/lib/types/utils/with-headers.d.ts.map +1 -0
  73. package/lib/types/vite-plugin.d.ts +17 -0
  74. package/lib/types/vite-plugin.d.ts.map +1 -0
  75. package/package.json +100 -0
  76. package/src/adapters/bun.ts +65 -0
  77. package/src/adapters/index.ts +29 -0
  78. package/src/adapters/node.ts +113 -0
  79. package/src/adapters/static.ts +17 -0
  80. package/src/app.ts +62 -0
  81. package/src/cache.ts +149 -0
  82. package/src/client.ts +43 -0
  83. package/src/config.ts +36 -0
  84. package/src/entry-server.ts +51 -0
  85. package/src/font.ts +461 -0
  86. package/src/fs-router.ts +380 -0
  87. package/src/image-plugin.ts +452 -0
  88. package/src/image.tsx +167 -0
  89. package/src/index.ts +119 -0
  90. package/src/isr.ts +95 -0
  91. package/src/link.tsx +266 -0
  92. package/src/script.tsx +133 -0
  93. package/src/seo.ts +281 -0
  94. package/src/sharp.d.ts +20 -0
  95. package/src/theme.tsx +162 -0
  96. package/src/types.ts +130 -0
  97. package/src/utils/use-intersection-observer.ts +36 -0
  98. package/src/utils/with-headers.ts +16 -0
  99. package/src/vite-plugin.ts +92 -0
@@ -0,0 +1,104 @@
1
+ import type { ComponentFn } from '@pyreon/core';
2
+ import type { NavigationGuard } from '@pyreon/router';
3
+ import type { Middleware } from '@pyreon/server';
4
+ /** What a route file (e.g. `src/routes/index.tsx`) can export. */
5
+ export interface RouteModule {
6
+ /** Default export is the page component. */
7
+ default?: ComponentFn;
8
+ /** Layout wrapper — wraps this route and all children. */
9
+ layout?: ComponentFn;
10
+ /** Loading component shown while lazy-loading or during Suspense. */
11
+ loading?: ComponentFn;
12
+ /** Error component shown when the route errors. */
13
+ error?: ComponentFn;
14
+ /** Server-side data loader. */
15
+ loader?: (ctx: LoaderContext) => Promise<unknown>;
16
+ /** Per-route middleware. */
17
+ middleware?: Middleware | Middleware[];
18
+ /** Navigation guard — can redirect or block navigation. */
19
+ guard?: NavigationGuard;
20
+ /** Route metadata. */
21
+ meta?: RouteMeta;
22
+ /** Rendering mode override for this route. */
23
+ renderMode?: RenderMode;
24
+ }
25
+ /** Context passed to route loaders. */
26
+ export interface LoaderContext {
27
+ params: Record<string, string>;
28
+ query: Record<string, string>;
29
+ signal: AbortSignal;
30
+ request: Request;
31
+ }
32
+ /** Per-route metadata. */
33
+ export interface RouteMeta {
34
+ title?: string;
35
+ description?: string;
36
+ [key: string]: unknown;
37
+ }
38
+ export type RenderMode = 'ssr' | 'ssg' | 'spa' | 'isr';
39
+ export interface ISRConfig {
40
+ /** Revalidation interval in seconds. */
41
+ revalidate: number;
42
+ }
43
+ export interface ZeroConfig {
44
+ /** Default rendering mode. Default: "ssr" */
45
+ mode?: RenderMode;
46
+ /** Vite config overrides. */
47
+ vite?: Record<string, unknown>;
48
+ /** SSR options. */
49
+ ssr?: {
50
+ /** Streaming mode. Default: "string" */
51
+ mode?: 'string' | 'stream';
52
+ };
53
+ /** SSG options — only used when mode is "ssg". */
54
+ ssg?: {
55
+ /** Paths to prerender (or function returning paths). */
56
+ paths?: string[] | (() => string[] | Promise<string[]>);
57
+ };
58
+ /** ISR config — only used when mode is "isr". */
59
+ isr?: ISRConfig;
60
+ /** Deploy adapter. Default: "node" */
61
+ adapter?: 'node' | 'bun' | 'static';
62
+ /** Base URL path. Default: "/" */
63
+ base?: string;
64
+ /** App-level middleware applied to all routes. */
65
+ middleware?: Middleware[];
66
+ /** Server port for dev/preview. Default: 3000 */
67
+ port?: number;
68
+ }
69
+ /** Internal representation of a file-system route before conversion to RouteRecord. */
70
+ export interface FileRoute {
71
+ /** File path relative to routes dir (e.g. "users/[id].tsx") */
72
+ filePath: string;
73
+ /** Parsed URL path pattern (e.g. "/users/:id") */
74
+ urlPath: string;
75
+ /** Directory path for grouping (e.g. "users" or "" for root) */
76
+ dirPath: string;
77
+ /** Route segment depth for nesting. */
78
+ depth: number;
79
+ /** Whether this is a layout file. */
80
+ isLayout: boolean;
81
+ /** Whether this is an error boundary file. */
82
+ isError: boolean;
83
+ /** Whether this is a loading fallback file. */
84
+ isLoading: boolean;
85
+ /** Whether this is a catch-all route. */
86
+ isCatchAll: boolean;
87
+ /** Resolved rendering mode. */
88
+ renderMode: RenderMode;
89
+ }
90
+ export interface Adapter {
91
+ name: string;
92
+ /** Build the production server/output for this adapter. */
93
+ build(options: AdapterBuildOptions): Promise<void>;
94
+ }
95
+ export interface AdapterBuildOptions {
96
+ /** Path to the built server entry. */
97
+ serverEntry: string;
98
+ /** Path to the client build output. */
99
+ clientOutDir: string;
100
+ /** Final output directory. */
101
+ outDir: string;
102
+ config: ZeroConfig;
103
+ }
104
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Observes an element and calls `onIntersect` once it enters the viewport.
3
+ * Automatically disconnects after the first intersection.
4
+ *
5
+ * @param getElement - Getter for the target element (may be undefined before mount).
6
+ * @param onIntersect - Callback fired when the element becomes visible.
7
+ * @param rootMargin - IntersectionObserver rootMargin. Default: "200px".
8
+ */
9
+ export declare function useIntersectionObserver(getElement: () => HTMLElement | undefined, onIntersect: () => void, rootMargin?: string): void;
10
+ //# sourceMappingURL=use-intersection-observer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-intersection-observer.d.ts","sourceRoot":"","sources":["../../../src/utils/use-intersection-observer.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,MAAM,WAAW,GAAG,SAAS,EACzC,WAAW,EAAE,MAAM,IAAI,EACvB,UAAU,SAAU,QAsBrB"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Clone a Response with modified headers.
3
+ * Avoids repeating the `new Response(body, { status, statusText, headers })` pattern.
4
+ */
5
+ export declare function withHeaders(response: Response, modify: (headers: Headers) => void): Response;
6
+ //# sourceMappingURL=with-headers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"with-headers.d.ts","sourceRoot":"","sources":["../../../src/utils/with-headers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,WAAW,CACzB,QAAQ,EAAE,QAAQ,EAClB,MAAM,EAAE,CAAC,OAAO,EAAE,OAAO,KAAK,IAAI,GACjC,QAAQ,CAQV"}
@@ -0,0 +1,17 @@
1
+ import type { Plugin } from 'vite';
2
+ import type { ZeroConfig } from './types';
3
+ /**
4
+ * Zero Vite plugin — adds file-based routing and zero-config conventions
5
+ * on top of @pyreon/vite-plugin.
6
+ *
7
+ * @example
8
+ * // vite.config.ts
9
+ * import pyreon from "@pyreon/vite-plugin"
10
+ * import zero from "@pyreon/zero"
11
+ *
12
+ * export default {
13
+ * plugins: [pyreon(), zero()],
14
+ * }
15
+ */
16
+ export declare function zeroPlugin(userConfig?: ZeroConfig): Plugin;
17
+ //# sourceMappingURL=vite-plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"vite-plugin.d.ts","sourceRoot":"","sources":["../../src/vite-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,MAAM,CAAA;AAGlC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAA;AAKzC;;;;;;;;;;;;GAYG;AACH,wBAAgB,UAAU,CAAC,UAAU,GAAE,UAAe,GAAG,MAAM,CAsE9D"}
package/package.json ADDED
@@ -0,0 +1,100 @@
1
+ {
2
+ "name": "@pyreon/zero",
3
+ "version": "0.1.0",
4
+ "description": "Pyreon Zero — zero-config full-stack framework powered by Pyreon and Vite",
5
+ "license": "MIT",
6
+ "author": "Vit Bokisch",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/pyreon/zero",
10
+ "directory": "packages/zero"
11
+ },
12
+ "type": "module",
13
+ "files": [
14
+ "lib",
15
+ "!lib/analysis",
16
+ "src",
17
+ "!src/tests",
18
+ "LICENSE",
19
+ "README.md"
20
+ ],
21
+ "main": "./lib/index.js",
22
+ "module": "./lib/index.js",
23
+ "types": "./lib/types/index.d.ts",
24
+ "exports": {
25
+ ".": {
26
+ "bun": "./src/index.ts",
27
+ "import": "./lib/index.js",
28
+ "types": "./lib/types/index.d.ts"
29
+ },
30
+ "./client": {
31
+ "bun": "./src/client.ts",
32
+ "import": "./lib/client.js",
33
+ "types": "./lib/types/client.d.ts"
34
+ },
35
+ "./config": {
36
+ "bun": "./src/config.ts",
37
+ "import": "./lib/config.js",
38
+ "types": "./lib/types/config.d.ts"
39
+ },
40
+ "./image": {
41
+ "bun": "./src/image.tsx",
42
+ "import": "./lib/image.js",
43
+ "types": "./lib/types/image.d.ts"
44
+ },
45
+ "./link": {
46
+ "bun": "./src/link.tsx",
47
+ "import": "./lib/link.js",
48
+ "types": "./lib/types/link.d.ts"
49
+ },
50
+ "./script": {
51
+ "bun": "./src/script.tsx",
52
+ "import": "./lib/script.js",
53
+ "types": "./lib/types/script.d.ts"
54
+ },
55
+ "./font": {
56
+ "bun": "./src/font.ts",
57
+ "import": "./lib/font.js",
58
+ "types": "./lib/types/font.d.ts"
59
+ },
60
+ "./cache": {
61
+ "bun": "./src/cache.ts",
62
+ "import": "./lib/cache.js",
63
+ "types": "./lib/types/cache.d.ts"
64
+ },
65
+ "./seo": {
66
+ "bun": "./src/seo.ts",
67
+ "import": "./lib/seo.js",
68
+ "types": "./lib/types/seo.d.ts"
69
+ },
70
+ "./theme": {
71
+ "bun": "./src/theme.tsx",
72
+ "import": "./lib/theme.js",
73
+ "types": "./lib/types/theme.d.ts"
74
+ },
75
+ "./image-plugin": {
76
+ "bun": "./src/image-plugin.ts",
77
+ "import": "./lib/image-plugin.js",
78
+ "types": "./lib/types/image-plugin.d.ts"
79
+ }
80
+ },
81
+ "scripts": {
82
+ "build": "vl_rolldown_build && tsc",
83
+ "dev": "vl_rolldown_build-watch",
84
+ "test": "vitest run",
85
+ "typecheck": "tsc --noEmit"
86
+ },
87
+ "dependencies": {
88
+ "@pyreon/core": "^0.2.1",
89
+ "@pyreon/head": "^0.2.1",
90
+ "@pyreon/router": "^0.2.1",
91
+ "@pyreon/runtime-dom": "^0.2.1",
92
+ "@pyreon/runtime-server": "^0.2.1",
93
+ "@pyreon/server": "^0.2.1",
94
+ "@pyreon/vite-plugin": "^0.2.1",
95
+ "vite": "^8.0.0"
96
+ },
97
+ "peerDependencies": {
98
+ "@pyreon/reactivity": "^0.2.1"
99
+ }
100
+ }
@@ -0,0 +1,65 @@
1
+ import type { Adapter, AdapterBuildOptions } from '../types'
2
+
3
+ /**
4
+ * Bun adapter — generates a standalone Bun.serve() entry.
5
+ */
6
+ export function bunAdapter(): Adapter {
7
+ return {
8
+ name: 'bun',
9
+ async build(options: AdapterBuildOptions) {
10
+ const { writeFile, cp, mkdir } = await import('node:fs/promises')
11
+ const { join } = await import('node:path')
12
+
13
+ const outDir = options.outDir
14
+ await mkdir(outDir, { recursive: true })
15
+
16
+ // Copy server and client builds
17
+ await cp(options.clientOutDir, join(outDir, 'client'), {
18
+ recursive: true,
19
+ })
20
+ await cp(join(options.serverEntry, '..'), join(outDir, 'server'), {
21
+ recursive: true,
22
+ })
23
+
24
+ const port = options.config.port ?? 3000
25
+ const serverEntry = `
26
+ const handler = (await import("./server/entry-server.js")).default
27
+ const clientDir = new URL("./client/", import.meta.url).pathname
28
+
29
+ Bun.serve({
30
+ port: ${port},
31
+ async fetch(req) {
32
+ const url = new URL(req.url)
33
+
34
+ // Try static files first
35
+ if (req.method === "GET") {
36
+ const filePath = clientDir + (url.pathname === "/" ? "index.html" : url.pathname)
37
+ // Prevent path traversal — ensure resolved path stays within clientDir
38
+ const resolved = Bun.resolveSync(filePath, ".")
39
+ if (!resolved.startsWith(Bun.resolveSync(clientDir, "."))) {
40
+ return new Response("Forbidden", { status: 403 })
41
+ }
42
+ const file = Bun.file(filePath)
43
+ if (await file.exists()) {
44
+ return new Response(file, {
45
+ headers: {
46
+ "cache-control": filePath.endsWith(".js") || filePath.endsWith(".css")
47
+ ? "public, max-age=31536000, immutable"
48
+ : "public, max-age=3600",
49
+ },
50
+ })
51
+ }
52
+ }
53
+
54
+ // Fall through to SSR handler
55
+ return handler(req)
56
+ },
57
+ })
58
+
59
+ console.log("\\n ⚡ Zero production server running on http://localhost:${port}\\n")
60
+ `.trimStart()
61
+
62
+ await writeFile(join(outDir, 'index.ts'), serverEntry)
63
+ },
64
+ }
65
+ }
@@ -0,0 +1,29 @@
1
+ export { bunAdapter } from './bun'
2
+ export { nodeAdapter } from './node'
3
+ export { staticAdapter } from './static'
4
+
5
+ import type { Adapter, ZeroConfig } from '../types'
6
+ import { bunAdapter } from './bun'
7
+ import { nodeAdapter } from './node'
8
+ import { staticAdapter } from './static'
9
+
10
+ /**
11
+ * Resolve the adapter from config.
12
+ * Returns a built-in adapter or throws if unknown.
13
+ */
14
+ export function resolveAdapter(config: ZeroConfig): Adapter {
15
+ const name = config.adapter ?? 'node'
16
+
17
+ switch (name) {
18
+ case 'node':
19
+ return nodeAdapter()
20
+ case 'bun':
21
+ return bunAdapter()
22
+ case 'static':
23
+ return staticAdapter()
24
+ default:
25
+ throw new Error(
26
+ `[zero] Unknown adapter: "${name}". Use "node", "bun", or "static".`,
27
+ )
28
+ }
29
+ }
@@ -0,0 +1,113 @@
1
+ import type { Adapter, AdapterBuildOptions } from '../types'
2
+
3
+ /**
4
+ * Node.js adapter — generates a standalone server entry using node:http.
5
+ */
6
+ export function nodeAdapter(): Adapter {
7
+ return {
8
+ name: 'node',
9
+ async build(options: AdapterBuildOptions) {
10
+ const { writeFile, cp, mkdir } = await import('node:fs/promises')
11
+ const { join } = await import('node:path')
12
+
13
+ const outDir = options.outDir
14
+ await mkdir(outDir, { recursive: true })
15
+
16
+ // Copy server and client builds
17
+ await cp(options.clientOutDir, join(outDir, 'client'), {
18
+ recursive: true,
19
+ })
20
+ await cp(join(options.serverEntry, '..'), join(outDir, 'server'), {
21
+ recursive: true,
22
+ })
23
+
24
+ // Generate standalone server entry
25
+ const port = options.config.port ?? 3000
26
+ const serverEntry = `
27
+ import { createServer } from "node:http"
28
+ import { readFile } from "node:fs/promises"
29
+ import { join, extname } from "node:path"
30
+ import { fileURLToPath } from "node:url"
31
+
32
+ const __dirname = fileURLToPath(new URL(".", import.meta.url))
33
+ const handler = (await import("./server/entry-server.js")).default
34
+ const clientDir = join(__dirname, "client")
35
+
36
+ const MIME_TYPES = {
37
+ ".html": "text/html",
38
+ ".js": "application/javascript",
39
+ ".css": "text/css",
40
+ ".json": "application/json",
41
+ ".png": "image/png",
42
+ ".jpg": "image/jpeg",
43
+ ".svg": "image/svg+xml",
44
+ ".woff2": "font/woff2",
45
+ ".woff": "font/woff",
46
+ ".ico": "image/x-icon",
47
+ }
48
+
49
+ const server = createServer(async (req, res) => {
50
+ const url = new URL(req.url ?? "/", "http://localhost")
51
+
52
+ // Try to serve static files first
53
+ if (req.method === "GET") {
54
+ try {
55
+ const filePath = join(clientDir, url.pathname === "/" ? "index.html" : url.pathname)
56
+ // Prevent path traversal — ensure resolved path stays within clientDir
57
+ const { resolve } = await import("node:path")
58
+ const resolved = resolve(filePath)
59
+ if (!resolved.startsWith(resolve(clientDir))) {
60
+ res.writeHead(403)
61
+ res.end("Forbidden")
62
+ return
63
+ }
64
+ const ext = extname(filePath)
65
+ if (ext && ext !== ".html") {
66
+ const data = await readFile(filePath)
67
+ const mime = MIME_TYPES[ext] || "application/octet-stream"
68
+ res.writeHead(200, {
69
+ "content-type": mime,
70
+ "cache-control": ext === ".js" || ext === ".css"
71
+ ? "public, max-age=31536000, immutable"
72
+ : "public, max-age=3600",
73
+ })
74
+ res.end(data)
75
+ return
76
+ }
77
+ } catch {}
78
+ }
79
+
80
+ // Fall through to SSR handler
81
+ const headers = {}
82
+ for (const [key, value] of Object.entries(req.headers)) {
83
+ if (value) headers[key] = Array.isArray(value) ? value.join(", ") : value
84
+ }
85
+
86
+ const request = new Request(url.href, {
87
+ method: req.method,
88
+ headers,
89
+ })
90
+
91
+ const response = await handler(request)
92
+ const body = await response.text()
93
+
94
+ const responseHeaders = {}
95
+ response.headers.forEach((v, k) => { responseHeaders[k] = v })
96
+
97
+ res.writeHead(response.status, responseHeaders)
98
+ res.end(body)
99
+ })
100
+
101
+ server.listen(${port}, () => {
102
+ console.log("\\n ⚡ Zero production server running on http://localhost:${port}\\n")
103
+ })
104
+ `.trimStart()
105
+
106
+ await writeFile(join(outDir, 'index.js'), serverEntry)
107
+ await writeFile(
108
+ join(outDir, 'package.json'),
109
+ JSON.stringify({ type: 'module' }, null, 2),
110
+ )
111
+ },
112
+ }
113
+ }
@@ -0,0 +1,17 @@
1
+ import type { Adapter, AdapterBuildOptions } from '../types'
2
+
3
+ /**
4
+ * Static adapter — just copies the client build output.
5
+ * Used with SSG mode where all pages are pre-rendered at build time.
6
+ */
7
+ export function staticAdapter(): Adapter {
8
+ return {
9
+ name: 'static',
10
+ async build(options: AdapterBuildOptions) {
11
+ const { cp, mkdir } = await import('node:fs/promises')
12
+
13
+ await mkdir(options.outDir, { recursive: true })
14
+ await cp(options.clientOutDir, options.outDir, { recursive: true })
15
+ },
16
+ }
17
+ }
package/src/app.ts ADDED
@@ -0,0 +1,62 @@
1
+ import type { ComponentFn, Props } from '@pyreon/core'
2
+ import { Fragment, h } from '@pyreon/core'
3
+ import { HeadProvider } from '@pyreon/head'
4
+ import type { RouteRecord } from '@pyreon/router'
5
+ import { createRouter, RouterProvider, RouterView } from '@pyreon/router'
6
+
7
+ // ─── App assembly ────────────────────────────────────────────────────────────
8
+
9
+ export interface CreateAppOptions {
10
+ /** Route definitions (from file-based routing or manual). */
11
+ routes: RouteRecord[]
12
+
13
+ /** Router mode. Default: "history" for SSR, "hash" for SPA. */
14
+ routerMode?: 'hash' | 'history'
15
+
16
+ /** Initial URL for SSR. */
17
+ url?: string
18
+
19
+ /** Root layout component wrapping all routes. */
20
+ layout?: ComponentFn
21
+
22
+ /** Global error component. */
23
+ errorComponent?: ComponentFn
24
+ }
25
+
26
+ /**
27
+ * Create a full Zero app — assembles router, head provider, and root layout.
28
+ *
29
+ * Used internally by entry-server and entry-client.
30
+ */
31
+ export function createApp(options: CreateAppOptions) {
32
+ const router = createRouter({
33
+ routes: options.routes,
34
+ mode: options.routerMode ?? 'history',
35
+ url: options.url,
36
+ scrollBehavior: 'top',
37
+ })
38
+
39
+ const Layout = options.layout ?? DefaultLayout
40
+
41
+ function App() {
42
+ return h(
43
+ HeadProvider,
44
+ null,
45
+ h(
46
+ RouterProvider as ComponentFn<Props>,
47
+ { router },
48
+ h(Layout, null, h(RouterView as ComponentFn<Props>, null)),
49
+ ),
50
+ )
51
+ }
52
+
53
+ return { App, router }
54
+ }
55
+
56
+ function DefaultLayout(props: Props) {
57
+ return h(
58
+ Fragment,
59
+ null,
60
+ ...(Array.isArray(props.children) ? props.children : [props.children]),
61
+ )
62
+ }