@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.
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/lib/cache.js +80 -0
- package/lib/cache.js.map +1 -0
- package/lib/client.js +58 -0
- package/lib/client.js.map +1 -0
- package/lib/config.js +35 -0
- package/lib/config.js.map +1 -0
- package/lib/font.js +251 -0
- package/lib/font.js.map +1 -0
- package/lib/fs-router-BkbIWqek.js +30 -0
- package/lib/fs-router-BkbIWqek.js.map +1 -0
- package/lib/fs-router-jfd1QGLB.js +261 -0
- package/lib/fs-router-jfd1QGLB.js.map +1 -0
- package/lib/image-plugin.js +289 -0
- package/lib/image-plugin.js.map +1 -0
- package/lib/image.js +113 -0
- package/lib/image.js.map +1 -0
- package/lib/index.js +1665 -0
- package/lib/index.js.map +1 -0
- package/lib/link.js +186 -0
- package/lib/link.js.map +1 -0
- package/lib/script.js +102 -0
- package/lib/script.js.map +1 -0
- package/lib/seo.js +136 -0
- package/lib/seo.js.map +1 -0
- package/lib/theme.js +165 -0
- package/lib/theme.js.map +1 -0
- package/lib/types/adapters/bun.d.ts +6 -0
- package/lib/types/adapters/bun.d.ts.map +1 -0
- package/lib/types/adapters/index.d.ts +10 -0
- package/lib/types/adapters/index.d.ts.map +1 -0
- package/lib/types/adapters/node.d.ts +6 -0
- package/lib/types/adapters/node.d.ts.map +1 -0
- package/lib/types/adapters/static.d.ts +7 -0
- package/lib/types/adapters/static.d.ts.map +1 -0
- package/lib/types/app.d.ts +24 -0
- package/lib/types/app.d.ts.map +1 -0
- package/lib/types/cache.d.ts +54 -0
- package/lib/types/cache.d.ts.map +1 -0
- package/lib/types/client.d.ts +19 -0
- package/lib/types/client.d.ts.map +1 -0
- package/lib/types/config.d.ts +18 -0
- package/lib/types/config.d.ts.map +1 -0
- package/lib/types/entry-server.d.ts +26 -0
- package/lib/types/entry-server.d.ts.map +1 -0
- package/lib/types/font.d.ts +119 -0
- package/lib/types/font.d.ts.map +1 -0
- package/lib/types/fs-router.d.ts +33 -0
- package/lib/types/fs-router.d.ts.map +1 -0
- package/lib/types/image-plugin.d.ts +79 -0
- package/lib/types/image-plugin.d.ts.map +1 -0
- package/lib/types/image.d.ts +50 -0
- package/lib/types/image.d.ts.map +1 -0
- package/lib/types/index.d.ts +27 -0
- package/lib/types/index.d.ts.map +1 -0
- package/lib/types/isr.d.ts +9 -0
- package/lib/types/isr.d.ts.map +1 -0
- package/lib/types/link.d.ts +116 -0
- package/lib/types/link.d.ts.map +1 -0
- package/lib/types/script.d.ts +34 -0
- package/lib/types/script.d.ts.map +1 -0
- package/lib/types/seo.d.ts +88 -0
- package/lib/types/seo.d.ts.map +1 -0
- package/lib/types/theme.d.ts +38 -0
- package/lib/types/theme.d.ts.map +1 -0
- package/lib/types/types.d.ts +104 -0
- package/lib/types/types.d.ts.map +1 -0
- package/lib/types/utils/use-intersection-observer.d.ts +10 -0
- package/lib/types/utils/use-intersection-observer.d.ts.map +1 -0
- package/lib/types/utils/with-headers.d.ts +6 -0
- package/lib/types/utils/with-headers.d.ts.map +1 -0
- package/lib/types/vite-plugin.d.ts +17 -0
- package/lib/types/vite-plugin.d.ts.map +1 -0
- package/package.json +100 -0
- package/src/adapters/bun.ts +65 -0
- package/src/adapters/index.ts +29 -0
- package/src/adapters/node.ts +113 -0
- package/src/adapters/static.ts +17 -0
- package/src/app.ts +62 -0
- package/src/cache.ts +149 -0
- package/src/client.ts +43 -0
- package/src/config.ts +36 -0
- package/src/entry-server.ts +51 -0
- package/src/font.ts +461 -0
- package/src/fs-router.ts +380 -0
- package/src/image-plugin.ts +452 -0
- package/src/image.tsx +167 -0
- package/src/index.ts +119 -0
- package/src/isr.ts +95 -0
- package/src/link.tsx +266 -0
- package/src/script.tsx +133 -0
- package/src/seo.ts +281 -0
- package/src/sharp.d.ts +20 -0
- package/src/theme.tsx +162 -0
- package/src/types.ts +130 -0
- package/src/utils/use-intersection-observer.ts +36 -0
- package/src/utils/with-headers.ts +16 -0
- 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
|
+
}
|