@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
package/src/cache.ts ADDED
@@ -0,0 +1,149 @@
1
+ import type { Middleware, MiddlewareContext } from '@pyreon/server'
2
+
3
+ // ─── Cache control middleware ───────────────────────────────────────────────
4
+ //
5
+ // Smart caching middleware that sets appropriate cache headers based on
6
+ // asset type, URL patterns, and build hashes.
7
+ //
8
+ // Strategies:
9
+ // - Immutable: hashed assets (JS/CSS bundles) — cached forever
10
+ // - Static: images, fonts, media — long cache with revalidation
11
+ // - Dynamic: HTML pages — short or no cache, stale-while-revalidate
12
+ // - API: JSON responses — no cache by default
13
+
14
+ export interface CacheConfig {
15
+ /** Cache duration for immutable hashed assets (seconds). Default: 31536000 (1 year) */
16
+ immutable?: number
17
+ /** Cache duration for static assets like images/fonts (seconds). Default: 86400 (1 day) */
18
+ static?: number
19
+ /** Cache duration for pages (seconds). Default: 0 (no cache) */
20
+ pages?: number
21
+ /** Stale-while-revalidate window for pages (seconds). Default: 60 */
22
+ staleWhileRevalidate?: number
23
+ /** Custom rules by URL pattern. */
24
+ rules?: CacheRule[]
25
+ }
26
+
27
+ export interface CacheRule {
28
+ /** URL pattern to match (glob-style). e.g. "/api/*" */
29
+ match: string
30
+ /** Cache-Control header value. */
31
+ control: string
32
+ }
33
+
34
+ const HASHED_ASSET = /\.[a-f0-9]{8,}\.\w+$/
35
+ const STATIC_EXT =
36
+ /\.(png|jpe?g|gif|svg|webp|avif|ico|woff2?|ttf|otf|eot|mp4|webm|ogg|mp3|wav)$/i
37
+ const SCRIPT_EXT = /\.(js|css|mjs)$/i
38
+
39
+ /** @internal Exported for testing */
40
+ export function matchGlob(pattern: string, path: string): boolean {
41
+ // Escape regex special chars, then convert glob wildcards
42
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&')
43
+ const regex = escaped.replace(/\*/g, '.*').replace(/\?/g, '.')
44
+ return new RegExp(`^${regex}$`).test(path)
45
+ }
46
+
47
+ function resolveControl(
48
+ path: string,
49
+ immutableDuration: number,
50
+ staticDuration: number,
51
+ pageDuration: number,
52
+ swr: number,
53
+ ): string {
54
+ if (HASHED_ASSET.test(path)) {
55
+ return `public, max-age=${immutableDuration}, immutable`
56
+ }
57
+ if (SCRIPT_EXT.test(path)) {
58
+ return `public, max-age=3600, stale-while-revalidate=${swr}`
59
+ }
60
+ if (STATIC_EXT.test(path)) {
61
+ return `public, max-age=${staticDuration}, stale-while-revalidate=${swr}`
62
+ }
63
+ if (pageDuration > 0) {
64
+ return `public, max-age=${pageDuration}, stale-while-revalidate=${swr}`
65
+ }
66
+ return 'no-cache'
67
+ }
68
+
69
+ /**
70
+ * Cache control middleware for Zero.
71
+ * Sets Cache-Control headers on the response based on asset type.
72
+ *
73
+ * @example
74
+ * import { cacheMiddleware } from "@pyreon/zero/cache"
75
+ *
76
+ * export default createHandler({
77
+ * routes,
78
+ * middleware: [
79
+ * cacheMiddleware({
80
+ * pages: 60,
81
+ * staleWhileRevalidate: 300,
82
+ * rules: [
83
+ * { match: "/api/*", control: "no-store" },
84
+ * ],
85
+ * }),
86
+ * ],
87
+ * })
88
+ */
89
+ export function cacheMiddleware(config: CacheConfig = {}): Middleware {
90
+ const immutableDuration = config.immutable ?? 31536000
91
+ const staticDuration = config.static ?? 86400
92
+ const pageDuration = config.pages ?? 0
93
+ const swr = config.staleWhileRevalidate ?? 60
94
+ const rules = config.rules ?? []
95
+
96
+ return (ctx: MiddlewareContext) => {
97
+ const path = ctx.url.pathname
98
+
99
+ for (const rule of rules) {
100
+ if (matchGlob(rule.match, path)) {
101
+ ctx.headers.set('Cache-Control', rule.control)
102
+ return
103
+ }
104
+ }
105
+
106
+ const control = resolveControl(
107
+ path,
108
+ immutableDuration,
109
+ staticDuration,
110
+ pageDuration,
111
+ swr,
112
+ )
113
+ ctx.headers.set('Cache-Control', control)
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Security headers middleware.
119
+ * Adds common security headers to all responses.
120
+ */
121
+ export function securityHeaders(): Middleware {
122
+ return (ctx: MiddlewareContext) => {
123
+ ctx.headers.set('X-Content-Type-Options', 'nosniff')
124
+ ctx.headers.set('X-Frame-Options', 'DENY')
125
+ ctx.headers.set('X-XSS-Protection', '1; mode=block')
126
+ ctx.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin')
127
+ ctx.headers.set(
128
+ 'Permissions-Policy',
129
+ 'camera=(), microphone=(), geolocation=()',
130
+ )
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Compression detection middleware.
136
+ * Sets Vary: Accept-Encoding header so caches can serve compressed variants.
137
+ * Actual compression is handled by the runtime (Bun/Node) or reverse proxy.
138
+ */
139
+ export function varyEncoding(): Middleware {
140
+ return (ctx: MiddlewareContext) => {
141
+ const existing = ctx.headers.get('Vary')
142
+ if (!existing?.includes('Accept-Encoding')) {
143
+ ctx.headers.set(
144
+ 'Vary',
145
+ existing ? `${existing}, Accept-Encoding` : 'Accept-Encoding',
146
+ )
147
+ }
148
+ }
149
+ }
package/src/client.ts ADDED
@@ -0,0 +1,43 @@
1
+ import type { ComponentFn } from '@pyreon/core'
2
+ import { h } from '@pyreon/core'
3
+ import type { RouteRecord } from '@pyreon/router'
4
+ import { hydrateRoot, mount } from '@pyreon/runtime-dom'
5
+ import { createApp } from './app'
6
+
7
+ // ─── Client entry factory ───────────────────────────────────────────────────
8
+
9
+ export interface StartClientOptions {
10
+ /** Route definitions. */
11
+ routes: RouteRecord[]
12
+ /** Root layout component. */
13
+ layout?: ComponentFn
14
+ }
15
+
16
+ /**
17
+ * Start the client-side app — hydrates SSR content or mounts fresh for SPA.
18
+ *
19
+ * @example
20
+ * import { routes } from "virtual:zero/routes"
21
+ * import { startClient } from "@pyreon/zero/client"
22
+ *
23
+ * startClient({ routes })
24
+ */
25
+ export function startClient(options: StartClientOptions) {
26
+ const container = document.getElementById('app')
27
+ if (!container) throw new Error('[zero] Missing #app container element')
28
+
29
+ const { App } = createApp({
30
+ routes: options.routes,
31
+ routerMode: 'history',
32
+ layout: options.layout,
33
+ })
34
+
35
+ const vnode = h(App, null)
36
+
37
+ // If container has SSR content, hydrate. Otherwise mount fresh.
38
+ if (container.childNodes.length > 0) {
39
+ return hydrateRoot(container, vnode)
40
+ }
41
+
42
+ return mount(vnode, container)
43
+ }
package/src/config.ts ADDED
@@ -0,0 +1,36 @@
1
+ import type { ZeroConfig } from './types'
2
+
3
+ /**
4
+ * Define a Zero configuration.
5
+ * Used in `zero.config.ts` at the project root.
6
+ *
7
+ * @example
8
+ * import { defineConfig } from "@pyreon/zero/config"
9
+ *
10
+ * export default defineConfig({
11
+ * mode: "ssr",
12
+ * ssr: { mode: "stream" },
13
+ * port: 3000,
14
+ * })
15
+ */
16
+ export function defineConfig(config: ZeroConfig): ZeroConfig {
17
+ return config
18
+ }
19
+
20
+ /** Merge user config with defaults. */
21
+ export function resolveConfig(
22
+ userConfig: ZeroConfig = {},
23
+ ): Required<Pick<ZeroConfig, 'mode' | 'base' | 'port' | 'adapter'>> &
24
+ ZeroConfig {
25
+ return {
26
+ mode: 'ssr',
27
+ base: '/',
28
+ port: 3000,
29
+ adapter: 'node',
30
+ ...userConfig,
31
+ ssr: {
32
+ mode: 'string',
33
+ ...userConfig.ssr,
34
+ },
35
+ }
36
+ }
@@ -0,0 +1,51 @@
1
+ import type { RouteRecord } from '@pyreon/router'
2
+ import type { Middleware } from '@pyreon/server'
3
+ import { createHandler } from '@pyreon/server'
4
+ import { createApp } from './app'
5
+ import type { ZeroConfig } from './types'
6
+
7
+ // ─── Server entry factory ───────────────────────────────────────────────────
8
+
9
+ export interface CreateServerOptions {
10
+ /** Route definitions. */
11
+ routes: RouteRecord[]
12
+ /** Zero config. */
13
+ config?: ZeroConfig
14
+ /** Additional middleware. */
15
+ middleware?: Middleware[]
16
+ /** HTML template override. */
17
+ template?: string
18
+ /** Client entry path. */
19
+ clientEntry?: string
20
+ }
21
+
22
+ /**
23
+ * Create the SSR request handler for production.
24
+ *
25
+ * @example
26
+ * import { routes } from "virtual:zero/routes"
27
+ * import { createServer } from "@pyreon/zero"
28
+ *
29
+ * export default createServer({ routes })
30
+ */
31
+ export function createServer(options: CreateServerOptions) {
32
+ const config = options.config ?? {}
33
+ const allMiddleware = [
34
+ ...(config.middleware ?? []),
35
+ ...(options.middleware ?? []),
36
+ ]
37
+
38
+ const { App } = createApp({
39
+ routes: options.routes,
40
+ routerMode: 'history',
41
+ })
42
+
43
+ return createHandler({
44
+ App,
45
+ routes: options.routes,
46
+ middleware: allMiddleware,
47
+ mode: config.ssr?.mode ?? 'string',
48
+ template: options.template,
49
+ clientEntry: options.clientEntry,
50
+ })
51
+ }