@pyreon/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/lib/cache.js.map +1 -1
- package/lib/client.js.map +1 -1
- package/lib/config.js.map +1 -1
- package/lib/font.js.map +1 -1
- package/lib/fs-router-BkbIWqek.js.map +1 -1
- package/lib/fs-router-n4VA4lxu.js.map +1 -1
- package/lib/image-plugin.js.map +1 -1
- package/lib/image.js +1 -1
- package/lib/image.js.map +1 -1
- package/lib/index.js +1 -1
- package/lib/index.js.map +1 -1
- package/lib/link.js +1 -1
- package/lib/link.js.map +1 -1
- package/lib/script.js +1 -1
- package/lib/script.js.map +1 -1
- package/lib/seo.js.map +1 -1
- package/lib/theme.js +1 -1
- package/lib/theme.js.map +1 -1
- package/package.json +14 -13
- package/src/actions.ts +20 -28
- package/src/adapters/bun.ts +7 -7
- package/src/adapters/index.ts +12 -14
- package/src/adapters/node.ts +8 -11
- package/src/adapters/static.ts +3 -3
- package/src/api-routes.ts +23 -50
- package/src/app.ts +9 -13
- package/src/cache.ts +16 -29
- package/src/client.ts +8 -8
- package/src/compression.ts +21 -28
- package/src/config.ts +6 -7
- package/src/cors.ts +20 -28
- package/src/entry-server.ts +15 -19
- package/src/error-overlay.ts +10 -13
- package/src/font.ts +44 -55
- package/src/fs-router.ts +44 -63
- package/src/image-plugin.ts +53 -79
- package/src/image.tsx +39 -41
- package/src/index.ts +36 -36
- package/src/isr.ts +8 -8
- package/src/link.tsx +27 -30
- package/src/rate-limit.ts +15 -15
- package/src/script.tsx +21 -22
- package/src/seo.ts +47 -57
- package/src/sharp.d.ts +2 -6
- package/src/testing.ts +8 -12
- package/src/theme.tsx +18 -20
- package/src/types.ts +6 -6
- package/src/utils/use-intersection-observer.ts +2 -2
- package/src/utils/with-headers.ts +1 -4
- package/src/vite-plugin.ts +21 -28
- package/lib/types/actions.d.ts +0 -57
- package/lib/types/actions.d.ts.map +0 -1
- package/lib/types/adapters/bun.d.ts +0 -6
- package/lib/types/adapters/bun.d.ts.map +0 -1
- package/lib/types/adapters/index.d.ts +0 -10
- package/lib/types/adapters/index.d.ts.map +0 -1
- package/lib/types/adapters/node.d.ts +0 -6
- package/lib/types/adapters/node.d.ts.map +0 -1
- package/lib/types/adapters/static.d.ts +0 -7
- package/lib/types/adapters/static.d.ts.map +0 -1
- package/lib/types/api-routes.d.ts +0 -66
- package/lib/types/api-routes.d.ts.map +0 -1
- package/lib/types/app.d.ts +0 -24
- package/lib/types/app.d.ts.map +0 -1
- package/lib/types/cache.d.ts +0 -54
- package/lib/types/cache.d.ts.map +0 -1
- package/lib/types/client.d.ts +0 -19
- package/lib/types/client.d.ts.map +0 -1
- package/lib/types/compression.d.ts +0 -33
- package/lib/types/compression.d.ts.map +0 -1
- package/lib/types/config.d.ts +0 -18
- package/lib/types/config.d.ts.map +0 -1
- package/lib/types/cors.d.ts +0 -32
- package/lib/types/cors.d.ts.map +0 -1
- package/lib/types/entry-server.d.ts +0 -34
- package/lib/types/entry-server.d.ts.map +0 -1
- package/lib/types/error-overlay.d.ts +0 -6
- package/lib/types/error-overlay.d.ts.map +0 -1
- package/lib/types/font.d.ts +0 -119
- package/lib/types/font.d.ts.map +0 -1
- package/lib/types/fs-router.d.ts +0 -38
- package/lib/types/fs-router.d.ts.map +0 -1
- package/lib/types/image-plugin.d.ts +0 -79
- package/lib/types/image-plugin.d.ts.map +0 -1
- package/lib/types/image.d.ts +0 -51
- package/lib/types/image.d.ts.map +0 -1
- package/lib/types/index.d.ts +0 -37
- package/lib/types/index.d.ts.map +0 -1
- package/lib/types/isr.d.ts +0 -9
- package/lib/types/isr.d.ts.map +0 -1
- package/lib/types/link.d.ts +0 -116
- package/lib/types/link.d.ts.map +0 -1
- package/lib/types/rate-limit.d.ts +0 -34
- package/lib/types/rate-limit.d.ts.map +0 -1
- package/lib/types/script.d.ts +0 -35
- package/lib/types/script.d.ts.map +0 -1
- package/lib/types/seo.d.ts +0 -88
- package/lib/types/seo.d.ts.map +0 -1
- package/lib/types/testing.d.ts +0 -85
- package/lib/types/testing.d.ts.map +0 -1
- package/lib/types/theme.d.ts +0 -39
- package/lib/types/theme.d.ts.map +0 -1
- package/lib/types/types.d.ts +0 -109
- package/lib/types/types.d.ts.map +0 -1
- package/lib/types/utils/use-intersection-observer.d.ts +0 -10
- package/lib/types/utils/use-intersection-observer.d.ts.map +0 -1
- package/lib/types/utils/with-headers.d.ts +0 -6
- package/lib/types/utils/with-headers.d.ts.map +0 -1
- package/lib/types/vite-plugin.d.ts +0 -17
- package/lib/types/vite-plugin.d.ts.map +0 -1
package/src/api-routes.ts
CHANGED
|
@@ -1,16 +1,9 @@
|
|
|
1
|
-
import type { Middleware, MiddlewareContext } from
|
|
1
|
+
import type { Middleware, MiddlewareContext } from "@pyreon/server"
|
|
2
2
|
|
|
3
3
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
4
4
|
|
|
5
5
|
/** HTTP methods supported by API routes. */
|
|
6
|
-
export type HttpMethod =
|
|
7
|
-
| 'GET'
|
|
8
|
-
| 'POST'
|
|
9
|
-
| 'PUT'
|
|
10
|
-
| 'PATCH'
|
|
11
|
-
| 'DELETE'
|
|
12
|
-
| 'HEAD'
|
|
13
|
-
| 'OPTIONS'
|
|
6
|
+
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "HEAD" | "OPTIONS"
|
|
14
7
|
|
|
15
8
|
/** Context passed to API route handlers. */
|
|
16
9
|
export interface ApiContext {
|
|
@@ -54,12 +47,9 @@ export interface ApiRouteEntry {
|
|
|
54
47
|
* Match a URL path against an API route pattern.
|
|
55
48
|
* Returns extracted params or null if no match.
|
|
56
49
|
*/
|
|
57
|
-
export function matchApiRoute(
|
|
58
|
-
pattern
|
|
59
|
-
path
|
|
60
|
-
): Record<string, string> | null {
|
|
61
|
-
const patternParts = pattern.split('/').filter(Boolean)
|
|
62
|
-
const pathParts = path.split('/').filter(Boolean)
|
|
50
|
+
export function matchApiRoute(pattern: string, path: string): Record<string, string> | null {
|
|
51
|
+
const patternParts = pattern.split("/").filter(Boolean)
|
|
52
|
+
const pathParts = path.split("/").filter(Boolean)
|
|
63
53
|
const params: Record<string, string> = {}
|
|
64
54
|
|
|
65
55
|
for (let i = 0; i < patternParts.length; i++) {
|
|
@@ -67,9 +57,9 @@ export function matchApiRoute(
|
|
|
67
57
|
if (!pp) continue
|
|
68
58
|
|
|
69
59
|
// Catch-all: :param*
|
|
70
|
-
if (pp.endsWith(
|
|
60
|
+
if (pp.endsWith("*")) {
|
|
71
61
|
const paramName = pp.slice(1, -1)
|
|
72
|
-
params[paramName] = pathParts.slice(i).join(
|
|
62
|
+
params[paramName] = pathParts.slice(i).join("/")
|
|
73
63
|
return params
|
|
74
64
|
}
|
|
75
65
|
|
|
@@ -77,7 +67,7 @@ export function matchApiRoute(
|
|
|
77
67
|
if (i >= pathParts.length) return null
|
|
78
68
|
|
|
79
69
|
// Dynamic segment: :param
|
|
80
|
-
if (pp.startsWith(
|
|
70
|
+
if (pp.startsWith(":")) {
|
|
81
71
|
params[pp.slice(1)] = pathParts[i]!
|
|
82
72
|
continue
|
|
83
73
|
}
|
|
@@ -91,15 +81,7 @@ export function matchApiRoute(
|
|
|
91
81
|
|
|
92
82
|
// ─── Middleware ───────────────────────────────────────────────────────────────
|
|
93
83
|
|
|
94
|
-
const HTTP_METHODS: HttpMethod[] = [
|
|
95
|
-
'GET',
|
|
96
|
-
'POST',
|
|
97
|
-
'PUT',
|
|
98
|
-
'PATCH',
|
|
99
|
-
'DELETE',
|
|
100
|
-
'HEAD',
|
|
101
|
-
'OPTIONS',
|
|
102
|
-
]
|
|
84
|
+
const HTTP_METHODS: HttpMethod[] = ["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]
|
|
103
85
|
|
|
104
86
|
/**
|
|
105
87
|
* Create a middleware that dispatches API route requests.
|
|
@@ -116,12 +98,12 @@ export function createApiMiddleware(routes: ApiRouteEntry[]): Middleware {
|
|
|
116
98
|
|
|
117
99
|
if (!handler) {
|
|
118
100
|
// Route matched but method not supported
|
|
119
|
-
const allowed = HTTP_METHODS.filter((m) => route.module[m]).join(
|
|
101
|
+
const allowed = HTTP_METHODS.filter((m) => route.module[m]).join(", ")
|
|
120
102
|
return new Response(null, {
|
|
121
103
|
status: 405,
|
|
122
104
|
headers: {
|
|
123
105
|
Allow: allowed,
|
|
124
|
-
|
|
106
|
+
"Content-Type": "application/json",
|
|
125
107
|
},
|
|
126
108
|
})
|
|
127
109
|
}
|
|
@@ -144,12 +126,12 @@ export function createApiMiddleware(routes: ApiRouteEntry[]): Middleware {
|
|
|
144
126
|
* API routes are `.ts` or `.js` files inside an `api/` directory.
|
|
145
127
|
*/
|
|
146
128
|
export function isApiRoute(filePath: string): boolean {
|
|
147
|
-
const normalized = filePath.replace(/\\/g,
|
|
129
|
+
const normalized = filePath.replace(/\\/g, "/")
|
|
148
130
|
return (
|
|
149
|
-
normalized.startsWith(
|
|
150
|
-
(normalized.endsWith(
|
|
151
|
-
!normalized.endsWith(
|
|
152
|
-
!normalized.endsWith(
|
|
131
|
+
normalized.startsWith("api/") &&
|
|
132
|
+
(normalized.endsWith(".ts") || normalized.endsWith(".js")) &&
|
|
133
|
+
!normalized.endsWith(".tsx") &&
|
|
134
|
+
!normalized.endsWith(".jsx")
|
|
153
135
|
)
|
|
154
136
|
}
|
|
155
137
|
|
|
@@ -165,18 +147,18 @@ export function isApiRoute(filePath: string): boolean {
|
|
|
165
147
|
export function apiFilePathToPattern(filePath: string): string {
|
|
166
148
|
let route = filePath
|
|
167
149
|
// Remove extension
|
|
168
|
-
for (const ext of [
|
|
150
|
+
for (const ext of [".ts", ".js"]) {
|
|
169
151
|
if (route.endsWith(ext)) {
|
|
170
152
|
route = route.slice(0, -ext.length)
|
|
171
153
|
break
|
|
172
154
|
}
|
|
173
155
|
}
|
|
174
156
|
|
|
175
|
-
const segments = route.split(
|
|
157
|
+
const segments = route.split("/")
|
|
176
158
|
const urlSegments: string[] = []
|
|
177
159
|
|
|
178
160
|
for (const seg of segments) {
|
|
179
|
-
if (seg ===
|
|
161
|
+
if (seg === "index") continue
|
|
180
162
|
|
|
181
163
|
// Catch-all: [...param]
|
|
182
164
|
const catchAll = seg.match(/^\[\.\.\.(\w+)\]$/)
|
|
@@ -195,21 +177,18 @@ export function apiFilePathToPattern(filePath: string): string {
|
|
|
195
177
|
urlSegments.push(seg)
|
|
196
178
|
}
|
|
197
179
|
|
|
198
|
-
return `/${urlSegments.join(
|
|
180
|
+
return `/${urlSegments.join("/")}`
|
|
199
181
|
}
|
|
200
182
|
|
|
201
183
|
/**
|
|
202
184
|
* Generate a virtual module that exports API route entries.
|
|
203
185
|
* Each entry maps a URL pattern to a module with HTTP method handlers.
|
|
204
186
|
*/
|
|
205
|
-
export function generateApiRouteModule(
|
|
206
|
-
files: string[],
|
|
207
|
-
routesDir: string,
|
|
208
|
-
): string {
|
|
187
|
+
export function generateApiRouteModule(files: string[], routesDir: string): string {
|
|
209
188
|
const apiFiles = files.filter(isApiRoute)
|
|
210
189
|
|
|
211
190
|
if (apiFiles.length === 0) {
|
|
212
|
-
return
|
|
191
|
+
return "export const apiRoutes = []\n"
|
|
213
192
|
}
|
|
214
193
|
|
|
215
194
|
const imports: string[] = []
|
|
@@ -226,11 +205,5 @@ export function generateApiRouteModule(
|
|
|
226
205
|
entries.push(` { pattern: ${JSON.stringify(pattern)}, module: ${name} }`)
|
|
227
206
|
}
|
|
228
207
|
|
|
229
|
-
return [
|
|
230
|
-
...imports,
|
|
231
|
-
'',
|
|
232
|
-
'export const apiRoutes = [',
|
|
233
|
-
entries.join(',\n'),
|
|
234
|
-
']',
|
|
235
|
-
].join('\n')
|
|
208
|
+
return [...imports, "", "export const apiRoutes = [", entries.join(",\n"), "]"].join("\n")
|
|
236
209
|
}
|
package/src/app.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { ComponentFn, Props } from
|
|
2
|
-
import { Fragment, h } from
|
|
3
|
-
import { HeadProvider } from
|
|
4
|
-
import type { RouteRecord } from
|
|
5
|
-
import { createRouter, RouterProvider, RouterView } from
|
|
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
6
|
|
|
7
7
|
// ─── App assembly ────────────────────────────────────────────────────────────
|
|
8
8
|
|
|
@@ -11,7 +11,7 @@ export interface CreateAppOptions {
|
|
|
11
11
|
routes: RouteRecord[]
|
|
12
12
|
|
|
13
13
|
/** Router mode. Default: "history" for SSR, "hash" for SPA. */
|
|
14
|
-
routerMode?:
|
|
14
|
+
routerMode?: "hash" | "history"
|
|
15
15
|
|
|
16
16
|
/** Initial URL for SSR. */
|
|
17
17
|
url?: string
|
|
@@ -31,9 +31,9 @@ export interface CreateAppOptions {
|
|
|
31
31
|
export function createApp(options: CreateAppOptions) {
|
|
32
32
|
const router = createRouter({
|
|
33
33
|
routes: options.routes,
|
|
34
|
-
mode: options.routerMode ??
|
|
34
|
+
mode: options.routerMode ?? "history",
|
|
35
35
|
...(options.url ? { url: options.url } : {}),
|
|
36
|
-
scrollBehavior:
|
|
36
|
+
scrollBehavior: "top",
|
|
37
37
|
})
|
|
38
38
|
|
|
39
39
|
const Layout = options.layout ?? DefaultLayout
|
|
@@ -54,9 +54,5 @@ export function createApp(options: CreateAppOptions) {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function DefaultLayout(props: Props) {
|
|
57
|
-
return h(
|
|
58
|
-
Fragment,
|
|
59
|
-
null,
|
|
60
|
-
...(Array.isArray(props.children) ? props.children : [props.children]),
|
|
61
|
-
)
|
|
57
|
+
return h(Fragment, null, ...(Array.isArray(props.children) ? props.children : [props.children]))
|
|
62
58
|
}
|
package/src/cache.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Middleware, MiddlewareContext } from
|
|
1
|
+
import type { Middleware, MiddlewareContext } from "@pyreon/server"
|
|
2
2
|
|
|
3
3
|
// ─── Cache control middleware ───────────────────────────────────────────────
|
|
4
4
|
//
|
|
@@ -32,15 +32,14 @@ export interface CacheRule {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
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
|
|
35
|
+
const STATIC_EXT = /\.(png|jpe?g|gif|svg|webp|avif|ico|woff2?|ttf|otf|eot|mp4|webm|ogg|mp3|wav)$/i
|
|
37
36
|
const SCRIPT_EXT = /\.(js|css|mjs)$/i
|
|
38
37
|
|
|
39
38
|
/** @internal Exported for testing */
|
|
40
39
|
export function matchGlob(pattern: string, path: string): boolean {
|
|
41
40
|
// Escape regex special chars, then convert glob wildcards
|
|
42
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g,
|
|
43
|
-
const regex = escaped.replace(/\*/g,
|
|
41
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
42
|
+
const regex = escaped.replace(/\*/g, ".*").replace(/\?/g, ".")
|
|
44
43
|
return new RegExp(`^${regex}$`).test(path)
|
|
45
44
|
}
|
|
46
45
|
|
|
@@ -63,7 +62,7 @@ function resolveControl(
|
|
|
63
62
|
if (pageDuration > 0) {
|
|
64
63
|
return `public, max-age=${pageDuration}, stale-while-revalidate=${swr}`
|
|
65
64
|
}
|
|
66
|
-
return
|
|
65
|
+
return "no-cache"
|
|
67
66
|
}
|
|
68
67
|
|
|
69
68
|
/**
|
|
@@ -98,19 +97,13 @@ export function cacheMiddleware(config: CacheConfig = {}): Middleware {
|
|
|
98
97
|
|
|
99
98
|
for (const rule of rules) {
|
|
100
99
|
if (matchGlob(rule.match, path)) {
|
|
101
|
-
ctx.headers.set(
|
|
100
|
+
ctx.headers.set("Cache-Control", rule.control)
|
|
102
101
|
return
|
|
103
102
|
}
|
|
104
103
|
}
|
|
105
104
|
|
|
106
|
-
const control = resolveControl(
|
|
107
|
-
|
|
108
|
-
immutableDuration,
|
|
109
|
-
staticDuration,
|
|
110
|
-
pageDuration,
|
|
111
|
-
swr,
|
|
112
|
-
)
|
|
113
|
-
ctx.headers.set('Cache-Control', control)
|
|
105
|
+
const control = resolveControl(path, immutableDuration, staticDuration, pageDuration, swr)
|
|
106
|
+
ctx.headers.set("Cache-Control", control)
|
|
114
107
|
}
|
|
115
108
|
}
|
|
116
109
|
|
|
@@ -120,14 +113,11 @@ export function cacheMiddleware(config: CacheConfig = {}): Middleware {
|
|
|
120
113
|
*/
|
|
121
114
|
export function securityHeaders(): Middleware {
|
|
122
115
|
return (ctx: MiddlewareContext) => {
|
|
123
|
-
ctx.headers.set(
|
|
124
|
-
ctx.headers.set(
|
|
125
|
-
ctx.headers.set(
|
|
126
|
-
ctx.headers.set(
|
|
127
|
-
ctx.headers.set(
|
|
128
|
-
'Permissions-Policy',
|
|
129
|
-
'camera=(), microphone=(), geolocation=()',
|
|
130
|
-
)
|
|
116
|
+
ctx.headers.set("X-Content-Type-Options", "nosniff")
|
|
117
|
+
ctx.headers.set("X-Frame-Options", "DENY")
|
|
118
|
+
ctx.headers.set("X-XSS-Protection", "1; mode=block")
|
|
119
|
+
ctx.headers.set("Referrer-Policy", "strict-origin-when-cross-origin")
|
|
120
|
+
ctx.headers.set("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
|
|
131
121
|
}
|
|
132
122
|
}
|
|
133
123
|
|
|
@@ -138,12 +128,9 @@ export function securityHeaders(): Middleware {
|
|
|
138
128
|
*/
|
|
139
129
|
export function varyEncoding(): Middleware {
|
|
140
130
|
return (ctx: MiddlewareContext) => {
|
|
141
|
-
const existing = ctx.headers.get(
|
|
142
|
-
if (!existing?.includes(
|
|
143
|
-
ctx.headers.set(
|
|
144
|
-
'Vary',
|
|
145
|
-
existing ? `${existing}, Accept-Encoding` : 'Accept-Encoding',
|
|
146
|
-
)
|
|
131
|
+
const existing = ctx.headers.get("Vary")
|
|
132
|
+
if (!existing?.includes("Accept-Encoding")) {
|
|
133
|
+
ctx.headers.set("Vary", existing ? `${existing}, Accept-Encoding` : "Accept-Encoding")
|
|
147
134
|
}
|
|
148
135
|
}
|
|
149
136
|
}
|
package/src/client.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { ComponentFn } from
|
|
2
|
-
import { h } from
|
|
3
|
-
import type { RouteRecord } from
|
|
4
|
-
import { hydrateRoot, mount } from
|
|
5
|
-
import { createApp } from
|
|
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
6
|
|
|
7
7
|
// ─── Client entry factory ───────────────────────────────────────────────────
|
|
8
8
|
|
|
@@ -23,12 +23,12 @@ export interface StartClientOptions {
|
|
|
23
23
|
* startClient({ routes })
|
|
24
24
|
*/
|
|
25
25
|
export function startClient(options: StartClientOptions) {
|
|
26
|
-
const container = document.getElementById(
|
|
27
|
-
if (!container) throw new Error(
|
|
26
|
+
const container = document.getElementById("app")
|
|
27
|
+
if (!container) throw new Error("[zero] Missing #app container element")
|
|
28
28
|
|
|
29
29
|
const { App } = createApp({
|
|
30
30
|
routes: options.routes,
|
|
31
|
-
routerMode:
|
|
31
|
+
routerMode: "history",
|
|
32
32
|
...(options.layout ? { layout: options.layout } : {}),
|
|
33
33
|
})
|
|
34
34
|
|
package/src/compression.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Middleware, MiddlewareContext } from
|
|
1
|
+
import type { Middleware, MiddlewareContext } from "@pyreon/server"
|
|
2
2
|
|
|
3
3
|
// ─── Compression middleware ─────────────────────────────────────────────────
|
|
4
4
|
|
|
@@ -6,7 +6,7 @@ export interface CompressionConfig {
|
|
|
6
6
|
/** Minimum response size in bytes to compress. Default: `1024` (1KB) */
|
|
7
7
|
threshold?: number
|
|
8
8
|
/** Encoding preference order. Default: `["gzip", "deflate"]` */
|
|
9
|
-
encodings?: (
|
|
9
|
+
encodings?: ("gzip" | "deflate")[]
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
/**
|
|
@@ -22,13 +22,11 @@ export interface CompressionConfig {
|
|
|
22
22
|
* compressionMiddleware() // gzip with 1KB threshold
|
|
23
23
|
* compressionMiddleware({ threshold: 512, encodings: ["gzip"] })
|
|
24
24
|
*/
|
|
25
|
-
export function compressionMiddleware(
|
|
26
|
-
|
|
27
|
-
): Middleware {
|
|
28
|
-
const { threshold = 1024, encodings = ['gzip', 'deflate'] } = config
|
|
25
|
+
export function compressionMiddleware(config: CompressionConfig = {}): Middleware {
|
|
26
|
+
const { threshold = 1024, encodings = ["gzip", "deflate"] } = config
|
|
29
27
|
|
|
30
28
|
return (ctx: MiddlewareContext) => {
|
|
31
|
-
const acceptEncoding = ctx.req.headers.get(
|
|
29
|
+
const acceptEncoding = ctx.req.headers.get("accept-encoding") ?? ""
|
|
32
30
|
|
|
33
31
|
// Find the best supported encoding
|
|
34
32
|
const encoding = encodings.find((enc) => acceptEncoding.includes(enc))
|
|
@@ -37,7 +35,7 @@ export function compressionMiddleware(
|
|
|
37
35
|
// Store the encoding choice for post-processing
|
|
38
36
|
ctx.locals.__compressionEncoding = encoding
|
|
39
37
|
ctx.locals.__compressionThreshold = threshold
|
|
40
|
-
ctx.headers.append(
|
|
38
|
+
ctx.headers.append("Vary", "Accept-Encoding")
|
|
41
39
|
}
|
|
42
40
|
}
|
|
43
41
|
|
|
@@ -51,16 +49,16 @@ export function compressionMiddleware(
|
|
|
51
49
|
*/
|
|
52
50
|
export async function compressResponse(
|
|
53
51
|
response: Response,
|
|
54
|
-
encoding:
|
|
52
|
+
encoding: "gzip" | "deflate",
|
|
55
53
|
threshold: number,
|
|
56
54
|
): Promise<Response> {
|
|
57
|
-
const contentType = response.headers.get(
|
|
55
|
+
const contentType = response.headers.get("content-type") ?? ""
|
|
58
56
|
|
|
59
57
|
// Only compress text-based content
|
|
60
58
|
if (!isCompressible(contentType)) return response
|
|
61
59
|
|
|
62
60
|
// Skip if already encoded
|
|
63
|
-
if (response.headers.get(
|
|
61
|
+
if (response.headers.get("content-encoding")) return response
|
|
64
62
|
|
|
65
63
|
const body = await response.arrayBuffer()
|
|
66
64
|
|
|
@@ -70,9 +68,9 @@ export async function compressResponse(
|
|
|
70
68
|
const compressed = await compress(body, encoding)
|
|
71
69
|
|
|
72
70
|
const headers = new Headers(response.headers)
|
|
73
|
-
headers.set(
|
|
74
|
-
headers.delete(
|
|
75
|
-
headers.append(
|
|
71
|
+
headers.set("Content-Encoding", encoding)
|
|
72
|
+
headers.delete("Content-Length")
|
|
73
|
+
headers.append("Vary", "Accept-Encoding")
|
|
76
74
|
|
|
77
75
|
return new Response(compressed, {
|
|
78
76
|
status: response.status,
|
|
@@ -82,12 +80,12 @@ export async function compressResponse(
|
|
|
82
80
|
}
|
|
83
81
|
|
|
84
82
|
const COMPRESSIBLE_TYPES = [
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
83
|
+
"text/",
|
|
84
|
+
"application/json",
|
|
85
|
+
"application/javascript",
|
|
86
|
+
"application/xml",
|
|
87
|
+
"application/xhtml+xml",
|
|
88
|
+
"image/svg+xml",
|
|
91
89
|
]
|
|
92
90
|
|
|
93
91
|
/** Check if a content type is compressible. Exported for testing. */
|
|
@@ -95,13 +93,8 @@ export function isCompressible(contentType: string): boolean {
|
|
|
95
93
|
return COMPRESSIBLE_TYPES.some((t) => contentType.includes(t))
|
|
96
94
|
}
|
|
97
95
|
|
|
98
|
-
async function compress(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
): Promise<ArrayBuffer> {
|
|
102
|
-
const format = encoding === 'gzip' ? 'gzip' : 'deflate'
|
|
103
|
-
const stream = new Blob([data])
|
|
104
|
-
.stream()
|
|
105
|
-
.pipeThrough(new CompressionStream(format))
|
|
96
|
+
async function compress(data: ArrayBuffer, encoding: "gzip" | "deflate"): Promise<ArrayBuffer> {
|
|
97
|
+
const format = encoding === "gzip" ? "gzip" : "deflate"
|
|
98
|
+
const stream = new Blob([data]).stream().pipeThrough(new CompressionStream(format))
|
|
106
99
|
return new Response(stream).arrayBuffer()
|
|
107
100
|
}
|
package/src/config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ZeroConfig } from
|
|
1
|
+
import type { ZeroConfig } from "./types"
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Define a Zero configuration.
|
|
@@ -20,16 +20,15 @@ export function defineConfig(config: ZeroConfig): ZeroConfig {
|
|
|
20
20
|
/** Merge user config with defaults. */
|
|
21
21
|
export function resolveConfig(
|
|
22
22
|
userConfig: ZeroConfig = {},
|
|
23
|
-
): Required<Pick<ZeroConfig,
|
|
24
|
-
ZeroConfig {
|
|
23
|
+
): Required<Pick<ZeroConfig, "mode" | "base" | "port" | "adapter">> & ZeroConfig {
|
|
25
24
|
return {
|
|
26
|
-
mode:
|
|
27
|
-
base:
|
|
25
|
+
mode: "ssr",
|
|
26
|
+
base: "/",
|
|
28
27
|
port: 3000,
|
|
29
|
-
adapter:
|
|
28
|
+
adapter: "node",
|
|
30
29
|
...userConfig,
|
|
31
30
|
ssr: {
|
|
32
|
-
mode:
|
|
31
|
+
mode: "string",
|
|
33
32
|
...userConfig.ssr,
|
|
34
33
|
},
|
|
35
34
|
}
|
package/src/cors.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Middleware, MiddlewareContext } from
|
|
1
|
+
import type { Middleware, MiddlewareContext } from "@pyreon/server"
|
|
2
2
|
|
|
3
3
|
// ─── CORS middleware ────────────────────────────────────────────────────────
|
|
4
4
|
|
|
@@ -17,8 +17,8 @@ export interface CorsConfig {
|
|
|
17
17
|
maxAge?: number
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const DEFAULT_METHODS = [
|
|
21
|
-
const DEFAULT_HEADERS = [
|
|
20
|
+
const DEFAULT_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]
|
|
21
|
+
const DEFAULT_HEADERS = ["Content-Type", "Authorization"]
|
|
22
22
|
|
|
23
23
|
/**
|
|
24
24
|
* CORS middleware — handles preflight requests and sets appropriate
|
|
@@ -37,7 +37,7 @@ const DEFAULT_HEADERS = ['Content-Type', 'Authorization']
|
|
|
37
37
|
*/
|
|
38
38
|
export function corsMiddleware(config: CorsConfig = {}): Middleware {
|
|
39
39
|
const {
|
|
40
|
-
origin =
|
|
40
|
+
origin = "*",
|
|
41
41
|
methods = DEFAULT_METHODS,
|
|
42
42
|
allowedHeaders = DEFAULT_HEADERS,
|
|
43
43
|
exposedHeaders = [],
|
|
@@ -46,53 +46,45 @@ export function corsMiddleware(config: CorsConfig = {}): Middleware {
|
|
|
46
46
|
} = config
|
|
47
47
|
|
|
48
48
|
return (ctx: MiddlewareContext) => {
|
|
49
|
-
const requestOrigin = ctx.req.headers.get(
|
|
49
|
+
const requestOrigin = ctx.req.headers.get("origin") ?? ""
|
|
50
50
|
const resolvedOrigin = resolveOrigin(origin, requestOrigin)
|
|
51
51
|
|
|
52
52
|
if (!resolvedOrigin) return
|
|
53
53
|
|
|
54
54
|
// Set CORS headers on all responses
|
|
55
|
-
ctx.headers.set(
|
|
55
|
+
ctx.headers.set("Access-Control-Allow-Origin", resolvedOrigin)
|
|
56
56
|
if (credentials) {
|
|
57
|
-
ctx.headers.set(
|
|
57
|
+
ctx.headers.set("Access-Control-Allow-Credentials", "true")
|
|
58
58
|
}
|
|
59
59
|
if (exposedHeaders.length > 0) {
|
|
60
|
-
ctx.headers.set(
|
|
61
|
-
'Access-Control-Expose-Headers',
|
|
62
|
-
exposedHeaders.join(', '),
|
|
63
|
-
)
|
|
60
|
+
ctx.headers.set("Access-Control-Expose-Headers", exposedHeaders.join(", "))
|
|
64
61
|
}
|
|
65
|
-
if (resolvedOrigin !==
|
|
66
|
-
ctx.headers.append(
|
|
62
|
+
if (resolvedOrigin !== "*") {
|
|
63
|
+
ctx.headers.append("Vary", "Origin")
|
|
67
64
|
}
|
|
68
65
|
|
|
69
66
|
// Handle preflight
|
|
70
|
-
if (ctx.req.method ===
|
|
67
|
+
if (ctx.req.method === "OPTIONS") {
|
|
71
68
|
return new Response(null, {
|
|
72
69
|
status: 204,
|
|
73
70
|
headers: {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
...(credentials
|
|
79
|
-
? { 'Access-Control-Allow-Credentials': 'true' }
|
|
80
|
-
: {}),
|
|
71
|
+
"Access-Control-Allow-Origin": resolvedOrigin,
|
|
72
|
+
"Access-Control-Allow-Methods": methods.join(", "),
|
|
73
|
+
"Access-Control-Allow-Headers": allowedHeaders.join(", "),
|
|
74
|
+
"Access-Control-Max-Age": String(maxAge),
|
|
75
|
+
...(credentials ? { "Access-Control-Allow-Credentials": "true" } : {}),
|
|
81
76
|
},
|
|
82
77
|
})
|
|
83
78
|
}
|
|
84
79
|
}
|
|
85
80
|
}
|
|
86
81
|
|
|
87
|
-
function resolveOrigin(
|
|
88
|
-
config
|
|
89
|
-
|
|
90
|
-
): string | null {
|
|
91
|
-
if (config === '*') return '*'
|
|
92
|
-
if (typeof config === 'string') {
|
|
82
|
+
function resolveOrigin(config: CorsConfig["origin"], requestOrigin: string): string | null {
|
|
83
|
+
if (config === "*") return "*"
|
|
84
|
+
if (typeof config === "string") {
|
|
93
85
|
return config === requestOrigin ? config : null
|
|
94
86
|
}
|
|
95
|
-
if (typeof config ===
|
|
87
|
+
if (typeof config === "function") {
|
|
96
88
|
return config(requestOrigin) ? requestOrigin : null
|
|
97
89
|
}
|
|
98
90
|
if (Array.isArray(config)) {
|
package/src/entry-server.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import type { RouteRecord } from
|
|
2
|
-
import type { Middleware, MiddlewareContext } from
|
|
3
|
-
import { createHandler } from
|
|
4
|
-
import type { ApiRouteEntry } from
|
|
5
|
-
import { createApiMiddleware } from
|
|
6
|
-
import { createApp } from
|
|
7
|
-
import type { RouteMiddlewareEntry, ZeroConfig } from
|
|
1
|
+
import type { RouteRecord } from "@pyreon/router"
|
|
2
|
+
import type { Middleware, MiddlewareContext } from "@pyreon/server"
|
|
3
|
+
import { createHandler } from "@pyreon/server"
|
|
4
|
+
import type { ApiRouteEntry } from "./api-routes"
|
|
5
|
+
import { createApiMiddleware } from "./api-routes"
|
|
6
|
+
import { createApp } from "./app"
|
|
7
|
+
import type { RouteMiddlewareEntry, ZeroConfig } from "./types"
|
|
8
8
|
|
|
9
9
|
// ─── Server entry factory ───────────────────────────────────────────────────
|
|
10
10
|
|
|
@@ -28,15 +28,11 @@ export interface CreateServerOptions {
|
|
|
28
28
|
/**
|
|
29
29
|
* Create a middleware that dispatches per-route middleware based on URL pattern matching.
|
|
30
30
|
*/
|
|
31
|
-
function createRouteMiddlewareDispatcher(
|
|
32
|
-
entries: RouteMiddlewareEntry[],
|
|
33
|
-
): Middleware {
|
|
31
|
+
function createRouteMiddlewareDispatcher(entries: RouteMiddlewareEntry[]): Middleware {
|
|
34
32
|
return async (ctx: MiddlewareContext) => {
|
|
35
33
|
for (const entry of entries) {
|
|
36
34
|
if (matchPattern(entry.pattern, ctx.path)) {
|
|
37
|
-
const mw = Array.isArray(entry.middleware)
|
|
38
|
-
? entry.middleware
|
|
39
|
-
: [entry.middleware]
|
|
35
|
+
const mw = Array.isArray(entry.middleware) ? entry.middleware : [entry.middleware]
|
|
40
36
|
for (const fn of mw) {
|
|
41
37
|
const result = await fn(ctx)
|
|
42
38
|
if (result) return result
|
|
@@ -48,14 +44,14 @@ function createRouteMiddlewareDispatcher(
|
|
|
48
44
|
|
|
49
45
|
/** Simple URL pattern matcher supporting :param and :param* segments. */
|
|
50
46
|
export function matchPattern(pattern: string, path: string): boolean {
|
|
51
|
-
const patternParts = pattern.split(
|
|
52
|
-
const pathParts = path.split(
|
|
47
|
+
const patternParts = pattern.split("/").filter(Boolean)
|
|
48
|
+
const pathParts = path.split("/").filter(Boolean)
|
|
53
49
|
|
|
54
50
|
for (let i = 0; i < patternParts.length; i++) {
|
|
55
51
|
const pp = patternParts[i]
|
|
56
52
|
if (!pp) continue
|
|
57
|
-
if (pp.endsWith(
|
|
58
|
-
if (pp.startsWith(
|
|
53
|
+
if (pp.endsWith("*")) return true // catch-all matches everything after
|
|
54
|
+
if (pp.startsWith(":")) continue // dynamic segment matches anything
|
|
59
55
|
if (pp !== pathParts[i]) return false
|
|
60
56
|
}
|
|
61
57
|
|
|
@@ -93,14 +89,14 @@ export function createServer(options: CreateServerOptions) {
|
|
|
93
89
|
|
|
94
90
|
const { App } = createApp({
|
|
95
91
|
routes: options.routes,
|
|
96
|
-
routerMode:
|
|
92
|
+
routerMode: "history",
|
|
97
93
|
})
|
|
98
94
|
|
|
99
95
|
return createHandler({
|
|
100
96
|
App,
|
|
101
97
|
routes: options.routes,
|
|
102
98
|
middleware: allMiddleware,
|
|
103
|
-
mode: config.ssr?.mode ??
|
|
99
|
+
mode: config.ssr?.mode ?? "string",
|
|
104
100
|
...(options.template ? { template: options.template } : {}),
|
|
105
101
|
...(options.clientEntry ? { clientEntry: options.clientEntry } : {}),
|
|
106
102
|
})
|