@pyreon/zero 0.11.5 → 0.11.7
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/README.md +22 -22
- 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.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/link.js.map +1 -1
- package/lib/script.js.map +1 -1
- package/lib/seo.js.map +1 -1
- package/lib/theme.js.map +1 -1
- package/package.json +12 -12
- package/src/actions.ts +17 -17
- package/src/adapters/bun.ts +7 -7
- package/src/adapters/index.ts +11 -11
- package/src/adapters/node.ts +8 -8
- package/src/adapters/static.ts +3 -3
- package/src/api-routes.ts +21 -21
- package/src/app.ts +8 -8
- package/src/cache.ts +14 -14
- package/src/client.ts +8 -8
- package/src/compression.ts +19 -19
- package/src/config.ts +6 -6
- package/src/cors.ts +20 -20
- package/src/entry-server.ts +13 -13
- package/src/error-overlay.ts +9 -9
- package/src/font.ts +41 -41
- package/src/fs-router.ts +39 -39
- package/src/image-plugin.ts +45 -45
- package/src/image.tsx +39 -39
- package/src/index.ts +36 -56
- package/src/isr.ts +8 -8
- package/src/link.tsx +23 -23
- package/src/rate-limit.ts +15 -15
- package/src/script.tsx +20 -20
- package/src/seo.ts +46 -46
- package/src/sharp.d.ts +1 -1
- package/src/testing.ts +7 -7
- package/src/theme.tsx +18 -18
- package/src/types.ts +6 -6
- package/src/utils/use-intersection-observer.ts +2 -2
- package/src/vite-plugin.ts +21 -21
package/src/api-routes.ts
CHANGED
|
@@ -1,9 +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 =
|
|
6
|
+
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'
|
|
7
7
|
|
|
8
8
|
/** Context passed to API route handlers. */
|
|
9
9
|
export interface ApiContext {
|
|
@@ -48,8 +48,8 @@ export interface ApiRouteEntry {
|
|
|
48
48
|
* Returns extracted params or null if no match.
|
|
49
49
|
*/
|
|
50
50
|
export function matchApiRoute(pattern: string, path: string): Record<string, string> | null {
|
|
51
|
-
const patternParts = pattern.split(
|
|
52
|
-
const pathParts = path.split(
|
|
51
|
+
const patternParts = pattern.split('/').filter(Boolean)
|
|
52
|
+
const pathParts = path.split('/').filter(Boolean)
|
|
53
53
|
const params: Record<string, string> = {}
|
|
54
54
|
|
|
55
55
|
for (let i = 0; i < patternParts.length; i++) {
|
|
@@ -57,9 +57,9 @@ export function matchApiRoute(pattern: string, path: string): Record<string, str
|
|
|
57
57
|
if (!pp) continue
|
|
58
58
|
|
|
59
59
|
// Catch-all: :param*
|
|
60
|
-
if (pp.endsWith(
|
|
60
|
+
if (pp.endsWith('*')) {
|
|
61
61
|
const paramName = pp.slice(1, -1)
|
|
62
|
-
params[paramName] = pathParts.slice(i).join(
|
|
62
|
+
params[paramName] = pathParts.slice(i).join('/')
|
|
63
63
|
return params
|
|
64
64
|
}
|
|
65
65
|
|
|
@@ -67,7 +67,7 @@ export function matchApiRoute(pattern: string, path: string): Record<string, str
|
|
|
67
67
|
if (i >= pathParts.length) return null
|
|
68
68
|
|
|
69
69
|
// Dynamic segment: :param
|
|
70
|
-
if (pp.startsWith(
|
|
70
|
+
if (pp.startsWith(':')) {
|
|
71
71
|
params[pp.slice(1)] = pathParts[i]!
|
|
72
72
|
continue
|
|
73
73
|
}
|
|
@@ -81,7 +81,7 @@ export function matchApiRoute(pattern: string, path: string): Record<string, str
|
|
|
81
81
|
|
|
82
82
|
// ─── Middleware ───────────────────────────────────────────────────────────────
|
|
83
83
|
|
|
84
|
-
const HTTP_METHODS: HttpMethod[] = [
|
|
84
|
+
const HTTP_METHODS: HttpMethod[] = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS']
|
|
85
85
|
|
|
86
86
|
/**
|
|
87
87
|
* Create a middleware that dispatches API route requests.
|
|
@@ -98,12 +98,12 @@ export function createApiMiddleware(routes: ApiRouteEntry[]): Middleware {
|
|
|
98
98
|
|
|
99
99
|
if (!handler) {
|
|
100
100
|
// Route matched but method not supported
|
|
101
|
-
const allowed = HTTP_METHODS.filter((m) => route.module[m]).join(
|
|
101
|
+
const allowed = HTTP_METHODS.filter((m) => route.module[m]).join(', ')
|
|
102
102
|
return new Response(null, {
|
|
103
103
|
status: 405,
|
|
104
104
|
headers: {
|
|
105
105
|
Allow: allowed,
|
|
106
|
-
|
|
106
|
+
'Content-Type': 'application/json',
|
|
107
107
|
},
|
|
108
108
|
})
|
|
109
109
|
}
|
|
@@ -126,12 +126,12 @@ export function createApiMiddleware(routes: ApiRouteEntry[]): Middleware {
|
|
|
126
126
|
* API routes are `.ts` or `.js` files inside an `api/` directory.
|
|
127
127
|
*/
|
|
128
128
|
export function isApiRoute(filePath: string): boolean {
|
|
129
|
-
const normalized = filePath.replace(/\\/g,
|
|
129
|
+
const normalized = filePath.replace(/\\/g, '/')
|
|
130
130
|
return (
|
|
131
|
-
normalized.startsWith(
|
|
132
|
-
(normalized.endsWith(
|
|
133
|
-
!normalized.endsWith(
|
|
134
|
-
!normalized.endsWith(
|
|
131
|
+
normalized.startsWith('api/') &&
|
|
132
|
+
(normalized.endsWith('.ts') || normalized.endsWith('.js')) &&
|
|
133
|
+
!normalized.endsWith('.tsx') &&
|
|
134
|
+
!normalized.endsWith('.jsx')
|
|
135
135
|
)
|
|
136
136
|
}
|
|
137
137
|
|
|
@@ -147,18 +147,18 @@ export function isApiRoute(filePath: string): boolean {
|
|
|
147
147
|
export function apiFilePathToPattern(filePath: string): string {
|
|
148
148
|
let route = filePath
|
|
149
149
|
// Remove extension
|
|
150
|
-
for (const ext of [
|
|
150
|
+
for (const ext of ['.ts', '.js']) {
|
|
151
151
|
if (route.endsWith(ext)) {
|
|
152
152
|
route = route.slice(0, -ext.length)
|
|
153
153
|
break
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
-
const segments = route.split(
|
|
157
|
+
const segments = route.split('/')
|
|
158
158
|
const urlSegments: string[] = []
|
|
159
159
|
|
|
160
160
|
for (const seg of segments) {
|
|
161
|
-
if (seg ===
|
|
161
|
+
if (seg === 'index') continue
|
|
162
162
|
|
|
163
163
|
// Catch-all: [...param]
|
|
164
164
|
const catchAll = seg.match(/^\[\.\.\.(\w+)\]$/)
|
|
@@ -177,7 +177,7 @@ export function apiFilePathToPattern(filePath: string): string {
|
|
|
177
177
|
urlSegments.push(seg)
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
-
return `/${urlSegments.join(
|
|
180
|
+
return `/${urlSegments.join('/')}`
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
/**
|
|
@@ -188,7 +188,7 @@ export function generateApiRouteModule(files: string[], routesDir: string): stri
|
|
|
188
188
|
const apiFiles = files.filter(isApiRoute)
|
|
189
189
|
|
|
190
190
|
if (apiFiles.length === 0) {
|
|
191
|
-
return
|
|
191
|
+
return 'export const apiRoutes = []\n'
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
const imports: string[] = []
|
|
@@ -205,5 +205,5 @@ export function generateApiRouteModule(files: string[], routesDir: string): stri
|
|
|
205
205
|
entries.push(` { pattern: ${JSON.stringify(pattern)}, module: ${name} }`)
|
|
206
206
|
}
|
|
207
207
|
|
|
208
|
-
return [...imports,
|
|
208
|
+
return [...imports, '', 'export const apiRoutes = [', entries.join(',\n'), ']'].join('\n')
|
|
209
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
|
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
|
//
|
|
@@ -38,8 +38,8 @@ const SCRIPT_EXT = /\.(js|css|mjs)$/i
|
|
|
38
38
|
/** @internal Exported for testing */
|
|
39
39
|
export function matchGlob(pattern: string, path: string): boolean {
|
|
40
40
|
// Escape regex special chars, then convert glob wildcards
|
|
41
|
-
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g,
|
|
42
|
-
const regex = escaped.replace(/\*/g,
|
|
41
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
42
|
+
const regex = escaped.replace(/\*/g, '.*').replace(/\?/g, '.')
|
|
43
43
|
return new RegExp(`^${regex}$`).test(path)
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -62,7 +62,7 @@ function resolveControl(
|
|
|
62
62
|
if (pageDuration > 0) {
|
|
63
63
|
return `public, max-age=${pageDuration}, stale-while-revalidate=${swr}`
|
|
64
64
|
}
|
|
65
|
-
return
|
|
65
|
+
return 'no-cache'
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
@@ -97,13 +97,13 @@ export function cacheMiddleware(config: CacheConfig = {}): Middleware {
|
|
|
97
97
|
|
|
98
98
|
for (const rule of rules) {
|
|
99
99
|
if (matchGlob(rule.match, path)) {
|
|
100
|
-
ctx.headers.set(
|
|
100
|
+
ctx.headers.set('Cache-Control', rule.control)
|
|
101
101
|
return
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
const control = resolveControl(path, immutableDuration, staticDuration, pageDuration, swr)
|
|
106
|
-
ctx.headers.set(
|
|
106
|
+
ctx.headers.set('Cache-Control', control)
|
|
107
107
|
}
|
|
108
108
|
}
|
|
109
109
|
|
|
@@ -113,11 +113,11 @@ export function cacheMiddleware(config: CacheConfig = {}): Middleware {
|
|
|
113
113
|
*/
|
|
114
114
|
export function securityHeaders(): Middleware {
|
|
115
115
|
return (ctx: MiddlewareContext) => {
|
|
116
|
-
ctx.headers.set(
|
|
117
|
-
ctx.headers.set(
|
|
118
|
-
ctx.headers.set(
|
|
119
|
-
ctx.headers.set(
|
|
120
|
-
ctx.headers.set(
|
|
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=()')
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
@@ -128,9 +128,9 @@ export function securityHeaders(): Middleware {
|
|
|
128
128
|
*/
|
|
129
129
|
export function varyEncoding(): Middleware {
|
|
130
130
|
return (ctx: MiddlewareContext) => {
|
|
131
|
-
const existing = ctx.headers.get(
|
|
132
|
-
if (!existing?.includes(
|
|
133
|
-
ctx.headers.set(
|
|
131
|
+
const existing = ctx.headers.get('Vary')
|
|
132
|
+
if (!existing?.includes('Accept-Encoding')) {
|
|
133
|
+
ctx.headers.set('Vary', existing ? `${existing}, Accept-Encoding` : 'Accept-Encoding')
|
|
134
134
|
}
|
|
135
135
|
}
|
|
136
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
|
/**
|
|
@@ -23,10 +23,10 @@ export interface CompressionConfig {
|
|
|
23
23
|
* compressionMiddleware({ threshold: 512, encodings: ["gzip"] })
|
|
24
24
|
*/
|
|
25
25
|
export function compressionMiddleware(config: CompressionConfig = {}): Middleware {
|
|
26
|
-
const { threshold = 1024, encodings = [
|
|
26
|
+
const { threshold = 1024, encodings = ['gzip', 'deflate'] } = config
|
|
27
27
|
|
|
28
28
|
return (ctx: MiddlewareContext) => {
|
|
29
|
-
const acceptEncoding = ctx.req.headers.get(
|
|
29
|
+
const acceptEncoding = ctx.req.headers.get('accept-encoding') ?? ''
|
|
30
30
|
|
|
31
31
|
// Find the best supported encoding
|
|
32
32
|
const encoding = encodings.find((enc) => acceptEncoding.includes(enc))
|
|
@@ -35,7 +35,7 @@ export function compressionMiddleware(config: CompressionConfig = {}): Middlewar
|
|
|
35
35
|
// Store the encoding choice for post-processing
|
|
36
36
|
ctx.locals.__compressionEncoding = encoding
|
|
37
37
|
ctx.locals.__compressionThreshold = threshold
|
|
38
|
-
ctx.headers.append(
|
|
38
|
+
ctx.headers.append('Vary', 'Accept-Encoding')
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -49,16 +49,16 @@ export function compressionMiddleware(config: CompressionConfig = {}): Middlewar
|
|
|
49
49
|
*/
|
|
50
50
|
export async function compressResponse(
|
|
51
51
|
response: Response,
|
|
52
|
-
encoding:
|
|
52
|
+
encoding: 'gzip' | 'deflate',
|
|
53
53
|
threshold: number,
|
|
54
54
|
): Promise<Response> {
|
|
55
|
-
const contentType = response.headers.get(
|
|
55
|
+
const contentType = response.headers.get('content-type') ?? ''
|
|
56
56
|
|
|
57
57
|
// Only compress text-based content
|
|
58
58
|
if (!isCompressible(contentType)) return response
|
|
59
59
|
|
|
60
60
|
// Skip if already encoded
|
|
61
|
-
if (response.headers.get(
|
|
61
|
+
if (response.headers.get('content-encoding')) return response
|
|
62
62
|
|
|
63
63
|
const body = await response.arrayBuffer()
|
|
64
64
|
|
|
@@ -68,9 +68,9 @@ export async function compressResponse(
|
|
|
68
68
|
const compressed = await compress(body, encoding)
|
|
69
69
|
|
|
70
70
|
const headers = new Headers(response.headers)
|
|
71
|
-
headers.set(
|
|
72
|
-
headers.delete(
|
|
73
|
-
headers.append(
|
|
71
|
+
headers.set('Content-Encoding', encoding)
|
|
72
|
+
headers.delete('Content-Length')
|
|
73
|
+
headers.append('Vary', 'Accept-Encoding')
|
|
74
74
|
|
|
75
75
|
return new Response(compressed, {
|
|
76
76
|
status: response.status,
|
|
@@ -80,12 +80,12 @@ export async function compressResponse(
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
const COMPRESSIBLE_TYPES = [
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
'text/',
|
|
84
|
+
'application/json',
|
|
85
|
+
'application/javascript',
|
|
86
|
+
'application/xml',
|
|
87
|
+
'application/xhtml+xml',
|
|
88
|
+
'image/svg+xml',
|
|
89
89
|
]
|
|
90
90
|
|
|
91
91
|
/** Check if a content type is compressible. Exported for testing. */
|
|
@@ -93,8 +93,8 @@ export function isCompressible(contentType: string): boolean {
|
|
|
93
93
|
return COMPRESSIBLE_TYPES.some((t) => contentType.includes(t))
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
-
async function compress(data: ArrayBuffer, encoding:
|
|
97
|
-
const format = encoding ===
|
|
96
|
+
async function compress(data: ArrayBuffer, encoding: 'gzip' | 'deflate'): Promise<ArrayBuffer> {
|
|
97
|
+
const format = encoding === 'gzip' ? 'gzip' : 'deflate'
|
|
98
98
|
const stream = new Blob([data]).stream().pipeThrough(new CompressionStream(format))
|
|
99
99
|
return new Response(stream).arrayBuffer()
|
|
100
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,15 +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,
|
|
23
|
+
): Required<Pick<ZeroConfig, 'mode' | 'base' | 'port' | 'adapter'>> & ZeroConfig {
|
|
24
24
|
return {
|
|
25
|
-
mode:
|
|
26
|
-
base:
|
|
25
|
+
mode: 'ssr',
|
|
26
|
+
base: '/',
|
|
27
27
|
port: 3000,
|
|
28
|
-
adapter:
|
|
28
|
+
adapter: 'node',
|
|
29
29
|
...userConfig,
|
|
30
30
|
ssr: {
|
|
31
|
-
mode:
|
|
31
|
+
mode: 'string',
|
|
32
32
|
...userConfig.ssr,
|
|
33
33
|
},
|
|
34
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,45 +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(
|
|
60
|
+
ctx.headers.set('Access-Control-Expose-Headers', exposedHeaders.join(', '))
|
|
61
61
|
}
|
|
62
|
-
if (resolvedOrigin !==
|
|
63
|
-
ctx.headers.append(
|
|
62
|
+
if (resolvedOrigin !== '*') {
|
|
63
|
+
ctx.headers.append('Vary', 'Origin')
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
// Handle preflight
|
|
67
|
-
if (ctx.req.method ===
|
|
67
|
+
if (ctx.req.method === 'OPTIONS') {
|
|
68
68
|
return new Response(null, {
|
|
69
69
|
status: 204,
|
|
70
70
|
headers: {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
...(credentials ? {
|
|
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' } : {}),
|
|
76
76
|
},
|
|
77
77
|
})
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
function resolveOrigin(config: CorsConfig[
|
|
83
|
-
if (config ===
|
|
84
|
-
if (typeof config ===
|
|
82
|
+
function resolveOrigin(config: CorsConfig['origin'], requestOrigin: string): string | null {
|
|
83
|
+
if (config === '*') return '*'
|
|
84
|
+
if (typeof config === 'string') {
|
|
85
85
|
return config === requestOrigin ? config : null
|
|
86
86
|
}
|
|
87
|
-
if (typeof config ===
|
|
87
|
+
if (typeof config === 'function') {
|
|
88
88
|
return config(requestOrigin) ? requestOrigin : null
|
|
89
89
|
}
|
|
90
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
|
|
|
@@ -44,14 +44,14 @@ function createRouteMiddlewareDispatcher(entries: RouteMiddlewareEntry[]): Middl
|
|
|
44
44
|
|
|
45
45
|
/** Simple URL pattern matcher supporting :param and :param* segments. */
|
|
46
46
|
export function matchPattern(pattern: string, path: string): boolean {
|
|
47
|
-
const patternParts = pattern.split(
|
|
48
|
-
const pathParts = path.split(
|
|
47
|
+
const patternParts = pattern.split('/').filter(Boolean)
|
|
48
|
+
const pathParts = path.split('/').filter(Boolean)
|
|
49
49
|
|
|
50
50
|
for (let i = 0; i < patternParts.length; i++) {
|
|
51
51
|
const pp = patternParts[i]
|
|
52
52
|
if (!pp) continue
|
|
53
|
-
if (pp.endsWith(
|
|
54
|
-
if (pp.startsWith(
|
|
53
|
+
if (pp.endsWith('*')) return true // catch-all matches everything after
|
|
54
|
+
if (pp.startsWith(':')) continue // dynamic segment matches anything
|
|
55
55
|
if (pp !== pathParts[i]) return false
|
|
56
56
|
}
|
|
57
57
|
|
|
@@ -89,14 +89,14 @@ export function createServer(options: CreateServerOptions) {
|
|
|
89
89
|
|
|
90
90
|
const { App } = createApp({
|
|
91
91
|
routes: options.routes,
|
|
92
|
-
routerMode:
|
|
92
|
+
routerMode: 'history',
|
|
93
93
|
})
|
|
94
94
|
|
|
95
95
|
return createHandler({
|
|
96
96
|
App,
|
|
97
97
|
routes: options.routes,
|
|
98
98
|
middleware: allMiddleware,
|
|
99
|
-
mode: config.ssr?.mode ??
|
|
99
|
+
mode: config.ssr?.mode ?? 'string',
|
|
100
100
|
...(options.template ? { template: options.template } : {}),
|
|
101
101
|
...(options.clientEntry ? { clientEntry: options.clientEntry } : {}),
|
|
102
102
|
})
|
package/src/error-overlay.ts
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
* Renders a styled HTML page with the error stack trace.
|
|
4
4
|
*/
|
|
5
5
|
export function renderErrorOverlay(error: Error): string {
|
|
6
|
-
const title = escapeHtml(error.message ||
|
|
7
|
-
const stack = escapeHtml(error.stack ||
|
|
6
|
+
const title = escapeHtml(error.message || 'Unknown error')
|
|
7
|
+
const stack = escapeHtml(error.stack || '')
|
|
8
8
|
|
|
9
9
|
return `<!DOCTYPE html>
|
|
10
10
|
<html lang="en">
|
|
@@ -96,17 +96,17 @@ export function renderErrorOverlay(error: Error): string {
|
|
|
96
96
|
|
|
97
97
|
function escapeHtml(str: string): string {
|
|
98
98
|
return str
|
|
99
|
-
.replace(/&/g,
|
|
100
|
-
.replace(/</g,
|
|
101
|
-
.replace(/>/g,
|
|
102
|
-
.replace(/"/g,
|
|
99
|
+
.replace(/&/g, '&')
|
|
100
|
+
.replace(/</g, '<')
|
|
101
|
+
.replace(/>/g, '>')
|
|
102
|
+
.replace(/"/g, '"')
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
function formatStack(stack: string): string {
|
|
106
106
|
return stack
|
|
107
|
-
.split(
|
|
107
|
+
.split('\n')
|
|
108
108
|
.map((line) => {
|
|
109
|
-
if (line.includes(
|
|
109
|
+
if (line.includes('at ')) {
|
|
110
110
|
const fileMatch = line.match(/\(([^)]+)\)/)
|
|
111
111
|
if (fileMatch) {
|
|
112
112
|
return line.replace(fileMatch[0], `(<span class="file">${fileMatch[1]}</span>)`)
|
|
@@ -114,5 +114,5 @@ function formatStack(stack: string): string {
|
|
|
114
114
|
}
|
|
115
115
|
return line
|
|
116
116
|
})
|
|
117
|
-
.join(
|
|
117
|
+
.join('\n')
|
|
118
118
|
}
|