@pyreon/zero 0.24.5 → 0.24.6
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/package.json +10 -39
- package/src/actions.ts +0 -196
- package/src/adapters/bun.ts +0 -114
- package/src/adapters/cloudflare.ts +0 -166
- package/src/adapters/index.ts +0 -61
- package/src/adapters/netlify.ts +0 -154
- package/src/adapters/node.ts +0 -163
- package/src/adapters/static.ts +0 -42
- package/src/adapters/validate.ts +0 -23
- package/src/adapters/vercel.ts +0 -182
- package/src/adapters/warn-missing-env.ts +0 -49
- package/src/ai.ts +0 -623
- package/src/api-routes.ts +0 -219
- package/src/app.ts +0 -92
- package/src/cache.ts +0 -136
- package/src/client.ts +0 -143
- package/src/compression.ts +0 -116
- package/src/config.ts +0 -35
- package/src/cors.ts +0 -94
- package/src/csp.ts +0 -226
- package/src/entry-server.ts +0 -224
- package/src/env.ts +0 -344
- package/src/error-overlay.ts +0 -118
- package/src/favicon.ts +0 -841
- package/src/font.ts +0 -511
- package/src/fs-router.ts +0 -1519
- package/src/i18n-routing.ts +0 -533
- package/src/icon.tsx +0 -182
- package/src/icons-plugin.ts +0 -296
- package/src/image-plugin.ts +0 -751
- package/src/image-types.ts +0 -60
- package/src/image.tsx +0 -340
- package/src/index.ts +0 -92
- package/src/isr.ts +0 -394
- package/src/link.tsx +0 -304
- package/src/logger.ts +0 -144
- package/src/manifest.ts +0 -787
- package/src/meta.tsx +0 -354
- package/src/middleware.ts +0 -65
- package/src/not-found.ts +0 -44
- package/src/og-image.ts +0 -378
- package/src/rate-limit.ts +0 -140
- package/src/script.tsx +0 -260
- package/src/seo.ts +0 -617
- package/src/server.ts +0 -89
- package/src/sharp.d.ts +0 -22
- package/src/ssg-plugin.ts +0 -1582
- package/src/testing.ts +0 -146
- package/src/theme.tsx +0 -257
- package/src/types.ts +0 -624
- package/src/utils/use-intersection-observer.ts +0 -36
- package/src/utils/with-headers.ts +0 -13
- package/src/vercel-revalidate-handler.ts +0 -204
- package/src/vite-plugin.ts +0 -848
package/src/testing.ts
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
import type { Middleware, MiddlewareContext } from '@pyreon/server'
|
|
2
|
-
import type { ApiHandler, ApiRouteEntry } from './api-routes'
|
|
3
|
-
import { createApiMiddleware } from './api-routes'
|
|
4
|
-
|
|
5
|
-
// ─── Test helpers for Zero applications ─────────────────────────────────────
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Create a mock MiddlewareContext for testing middleware.
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* import { createTestContext } from "@pyreon/zero/testing"
|
|
12
|
-
*
|
|
13
|
-
* const ctx = createTestContext("/api/posts", { method: "POST", body: { title: "Hello" } })
|
|
14
|
-
* const result = await myMiddleware(ctx)
|
|
15
|
-
*/
|
|
16
|
-
export function createTestContext(
|
|
17
|
-
path: string,
|
|
18
|
-
options: {
|
|
19
|
-
method?: string
|
|
20
|
-
headers?: Record<string, string>
|
|
21
|
-
body?: unknown
|
|
22
|
-
} = {},
|
|
23
|
-
): MiddlewareContext {
|
|
24
|
-
const { method = 'GET', headers = {}, body } = options
|
|
25
|
-
const url = new URL(`http://localhost${path}`)
|
|
26
|
-
|
|
27
|
-
const requestHeaders: Record<string, string> = { ...headers }
|
|
28
|
-
let requestBody: string | undefined
|
|
29
|
-
|
|
30
|
-
if (body !== undefined) {
|
|
31
|
-
requestHeaders['Content-Type'] = 'application/json'
|
|
32
|
-
requestBody = JSON.stringify(body)
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const req = new Request(url.toString(), {
|
|
36
|
-
method,
|
|
37
|
-
headers: requestHeaders,
|
|
38
|
-
...(requestBody != null ? { body: requestBody } : {}),
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
return {
|
|
42
|
-
req,
|
|
43
|
-
url,
|
|
44
|
-
path,
|
|
45
|
-
headers: new Headers(),
|
|
46
|
-
locals: {},
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Test a middleware by running it with a mock context and returning
|
|
52
|
-
* the result along with the response headers it set.
|
|
53
|
-
*
|
|
54
|
-
* @example
|
|
55
|
-
* import { testMiddleware } from "@pyreon/zero/testing"
|
|
56
|
-
*
|
|
57
|
-
* const { response, headers } = await testMiddleware(
|
|
58
|
-
* corsMiddleware({ origin: "*" }),
|
|
59
|
-
* "/api/posts"
|
|
60
|
-
* )
|
|
61
|
-
* expect(headers.get("Access-Control-Allow-Origin")).toBe("*")
|
|
62
|
-
*/
|
|
63
|
-
export async function testMiddleware(
|
|
64
|
-
middleware: Middleware,
|
|
65
|
-
path: string,
|
|
66
|
-
options: {
|
|
67
|
-
method?: string
|
|
68
|
-
headers?: Record<string, string>
|
|
69
|
-
body?: unknown
|
|
70
|
-
} = {},
|
|
71
|
-
): Promise<{ response: Response | undefined; headers: Headers }> {
|
|
72
|
-
const ctx = createTestContext(path, options)
|
|
73
|
-
const response = (await middleware(ctx)) as Response | undefined
|
|
74
|
-
return { response, headers: ctx.headers }
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Create a test server for API routes. Returns a function that
|
|
79
|
-
* accepts Request objects and dispatches to the correct handler.
|
|
80
|
-
*
|
|
81
|
-
* @example
|
|
82
|
-
* import { createTestApiServer } from "@pyreon/zero/testing"
|
|
83
|
-
*
|
|
84
|
-
* const server = createTestApiServer([
|
|
85
|
-
* { pattern: "/api/posts", module: postsApi },
|
|
86
|
-
* { pattern: "/api/posts/:id", module: postByIdApi },
|
|
87
|
-
* ])
|
|
88
|
-
*
|
|
89
|
-
* const response = await server.request("/api/posts")
|
|
90
|
-
* expect(response.status).toBe(200)
|
|
91
|
-
*
|
|
92
|
-
* const data = await server.request("/api/posts", { method: "POST", body: { title: "Hi" } })
|
|
93
|
-
* expect(data.status).toBe(201)
|
|
94
|
-
*/
|
|
95
|
-
export function createTestApiServer(routes: ApiRouteEntry[]) {
|
|
96
|
-
const middleware = createApiMiddleware(routes)
|
|
97
|
-
|
|
98
|
-
return {
|
|
99
|
-
async request(
|
|
100
|
-
path: string,
|
|
101
|
-
options: {
|
|
102
|
-
method?: string
|
|
103
|
-
headers?: Record<string, string>
|
|
104
|
-
body?: unknown
|
|
105
|
-
} = {},
|
|
106
|
-
): Promise<Response> {
|
|
107
|
-
const ctx = createTestContext(path, options)
|
|
108
|
-
const result = await middleware(ctx)
|
|
109
|
-
if (!result) {
|
|
110
|
-
return new Response('Not Found', { status: 404 })
|
|
111
|
-
}
|
|
112
|
-
return result
|
|
113
|
-
},
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* Create a mock API handler for testing.
|
|
119
|
-
* Records all calls and returns a configurable response.
|
|
120
|
-
*
|
|
121
|
-
* @example
|
|
122
|
-
* import { createMockHandler } from "@pyreon/zero/testing"
|
|
123
|
-
*
|
|
124
|
-
* const handler = createMockHandler({ status: 200, body: { ok: true } })
|
|
125
|
-
* // ... use handler in your API route module
|
|
126
|
-
* expect(handler.calls).toHaveLength(1)
|
|
127
|
-
* expect(handler.calls[0].params).toEqual({ id: "123" })
|
|
128
|
-
*/
|
|
129
|
-
export function createMockHandler(
|
|
130
|
-
responseConfig: { status?: number; body?: unknown; headers?: Record<string, string> } = {},
|
|
131
|
-
): ApiHandler & {
|
|
132
|
-
calls: Array<{ path: string; params: Record<string, string> }>
|
|
133
|
-
} {
|
|
134
|
-
const { status = 200, body = null, headers = {} } = responseConfig
|
|
135
|
-
const calls: Array<{ path: string; params: Record<string, string> }> = []
|
|
136
|
-
|
|
137
|
-
const handler: ApiHandler = (ctx) => {
|
|
138
|
-
calls.push({ path: ctx.path, params: ctx.params })
|
|
139
|
-
return new Response(JSON.stringify(body), {
|
|
140
|
-
status,
|
|
141
|
-
headers: { 'Content-Type': 'application/json', ...headers },
|
|
142
|
-
})
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
return Object.assign(handler, { calls })
|
|
146
|
-
}
|
package/src/theme.tsx
DELETED
|
@@ -1,257 +0,0 @@
|
|
|
1
|
-
import type { VNodeChild } from '@pyreon/core'
|
|
2
|
-
import { onMount } from '@pyreon/core'
|
|
3
|
-
import { effect, signal } from '@pyreon/reactivity'
|
|
4
|
-
|
|
5
|
-
// Dev-mode counter sink — see packages/internals/perf-harness for contract.
|
|
6
|
-
const __DEV__ = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'
|
|
7
|
-
const _countSink = globalThis as { __pyreon_count__?: (name: string, n?: number) => void }
|
|
8
|
-
|
|
9
|
-
// ─── Theme system ───────────────────────────────────────────────────────────
|
|
10
|
-
//
|
|
11
|
-
// Provides dark/light/system theme support with:
|
|
12
|
-
// - System preference detection via matchMedia
|
|
13
|
-
// - Persistent preference via localStorage
|
|
14
|
-
// - No flash of wrong theme (inline script in HTML)
|
|
15
|
-
// - Reactive theme signal for components
|
|
16
|
-
|
|
17
|
-
export type Theme = 'light' | 'dark' | 'system'
|
|
18
|
-
|
|
19
|
-
const STORAGE_KEY = 'zero-theme'
|
|
20
|
-
|
|
21
|
-
/** Reactive theme signal. */
|
|
22
|
-
export const theme = signal<Theme>('system')
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Reactive signal tracking the OS color-scheme preference. Updated by the
|
|
26
|
-
* `matchMedia('(prefers-color-scheme: dark)').change` event registered in
|
|
27
|
-
* `initTheme`. Components reading `resolvedTheme()` subscribe to BOTH
|
|
28
|
-
* `theme` and this signal, so a user toggling dark mode at the OS level
|
|
29
|
-
* re-renders everything reactively — not just the `<html data-theme>`
|
|
30
|
-
* attribute.
|
|
31
|
-
*
|
|
32
|
-
* SSR default is `_ssrDefault` (mutable via `setSSRThemeDefault`) so the
|
|
33
|
-
* server-rendered theme can differ from the client's OS preference.
|
|
34
|
-
*/
|
|
35
|
-
const _osPrefersDark = signal<boolean>(false)
|
|
36
|
-
|
|
37
|
-
/** SSR fallback when system preference can't be detected. Default: 'light'. */
|
|
38
|
-
let _ssrDefault: 'light' | 'dark' = 'light'
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Set the default theme for SSR (when `matchMedia` is unavailable).
|
|
42
|
-
* Call once at server startup before rendering.
|
|
43
|
-
*/
|
|
44
|
-
export function setSSRThemeDefault(value: 'light' | 'dark'): void {
|
|
45
|
-
_ssrDefault = value
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Reactive read of the resolved theme. Subscribes to `theme` (explicit
|
|
50
|
-
* user choice) and — when `theme === 'system'` — to `_osPrefersDark`
|
|
51
|
-
* (OS color-scheme preference). Components using `resolvedTheme()`
|
|
52
|
-
* inside JSX / effects / computeds re-render when either changes.
|
|
53
|
-
*/
|
|
54
|
-
export function resolvedTheme(): 'light' | 'dark' {
|
|
55
|
-
const t = theme()
|
|
56
|
-
if (t === 'system') {
|
|
57
|
-
if (typeof window === 'undefined') return _ssrDefault
|
|
58
|
-
return _osPrefersDark() ? 'dark' : 'light'
|
|
59
|
-
}
|
|
60
|
-
return t
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/** Toggle between light and dark. */
|
|
64
|
-
export function toggleTheme() {
|
|
65
|
-
const current = resolvedTheme()
|
|
66
|
-
setTheme(current === 'dark' ? 'light' : 'dark')
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
/** Set theme explicitly. */
|
|
70
|
-
export function setTheme(t: Theme) {
|
|
71
|
-
theme.set(t)
|
|
72
|
-
if (typeof document !== 'undefined') {
|
|
73
|
-
document.documentElement.dataset.theme = resolvedTheme()
|
|
74
|
-
try {
|
|
75
|
-
localStorage.setItem(STORAGE_KEY, t)
|
|
76
|
-
} catch {
|
|
77
|
-
// localStorage may not be available (SSR, private browsing)
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// Refcount + shared-teardown for `initTheme()`. The first mount runs the
|
|
83
|
-
// real setup (localStorage read + matchMedia listener + effect); subsequent
|
|
84
|
-
// mounts (other ThemeToggles, or an explicit `initTheme()` call in a
|
|
85
|
-
// layout alongside `<ThemeToggle>`) only bump the refcount. Each unmount
|
|
86
|
-
// decrements; when the count returns to 0 the shared teardown runs.
|
|
87
|
-
//
|
|
88
|
-
// Pre-fix every `initTheme()` call ran its own `onMount(setup)` — N
|
|
89
|
-
// mounted ThemeToggles → N matchMedia listeners + N effects, all writing
|
|
90
|
-
// the SAME values to the SAME `document.documentElement.dataset.theme`.
|
|
91
|
-
// Class D event-listener pile-up, real production case for any app with
|
|
92
|
-
// header + footer ThemeToggle widgets.
|
|
93
|
-
let _initRefCount = 0
|
|
94
|
-
let _disposeShared: (() => void) | null = null
|
|
95
|
-
|
|
96
|
-
function _setupShared(): () => void {
|
|
97
|
-
// Read persisted preference
|
|
98
|
-
try {
|
|
99
|
-
const stored = localStorage.getItem(STORAGE_KEY) as Theme | null
|
|
100
|
-
if (stored === 'light' || stored === 'dark' || stored === 'system') {
|
|
101
|
-
theme.set(stored)
|
|
102
|
-
}
|
|
103
|
-
} catch {
|
|
104
|
-
// localStorage may not be available
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// Apply to document
|
|
108
|
-
document.documentElement.dataset.theme = resolvedTheme()
|
|
109
|
-
|
|
110
|
-
// Watch for system preference changes. Seed the signal from the
|
|
111
|
-
// current media-query state, then update reactively on each OS
|
|
112
|
-
// preference flip. Components reading `resolvedTheme()` pick up the
|
|
113
|
-
// change automatically (they subscribe to `_osPrefersDark` when
|
|
114
|
-
// `theme === 'system'`).
|
|
115
|
-
const mq = window.matchMedia('(prefers-color-scheme: dark)')
|
|
116
|
-
_osPrefersDark.set(mq.matches)
|
|
117
|
-
function onChange(e: MediaQueryListEvent) {
|
|
118
|
-
_osPrefersDark.set(e.matches)
|
|
119
|
-
}
|
|
120
|
-
mq.addEventListener('change', onChange)
|
|
121
|
-
|
|
122
|
-
// Re-apply when theme signal changes — updates data-theme + favicons
|
|
123
|
-
const dispose = effect(() => {
|
|
124
|
-
const mode = resolvedTheme()
|
|
125
|
-
document.documentElement.dataset.theme = mode
|
|
126
|
-
|
|
127
|
-
// Swap favicon variants (if dual-variant favicons are present)
|
|
128
|
-
const faviconLinks = document.querySelectorAll<HTMLLinkElement>('[data-favicon-theme]')
|
|
129
|
-
for (const link of faviconLinks) {
|
|
130
|
-
link.media = link.dataset.faviconTheme === mode ? '' : 'not all'
|
|
131
|
-
}
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
return () => {
|
|
135
|
-
mq.removeEventListener('change', onChange)
|
|
136
|
-
dispose?.dispose()
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Reset the refcount + shared teardown. Useful for tests.
|
|
142
|
-
* @internal
|
|
143
|
-
*/
|
|
144
|
-
export function _resetInitThemeForTests(): void {
|
|
145
|
-
if (_disposeShared) _disposeShared()
|
|
146
|
-
_initRefCount = 0
|
|
147
|
-
_disposeShared = null
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Initialize the theme system. Safe to call multiple times — uses a
|
|
152
|
-
* mount-based refcount so multiple `<ThemeToggle>` instances (or
|
|
153
|
-
* `<ThemeToggle>` plus an explicit `initTheme()` in your layout) share
|
|
154
|
-
* a SINGLE matchMedia listener + effect.
|
|
155
|
-
*
|
|
156
|
-
* Reads from localStorage, listens for system preference changes.
|
|
157
|
-
*/
|
|
158
|
-
export function initTheme() {
|
|
159
|
-
onMount(() => {
|
|
160
|
-
if (_initRefCount === 0) {
|
|
161
|
-
_disposeShared = _setupShared()
|
|
162
|
-
}
|
|
163
|
-
_initRefCount++
|
|
164
|
-
// Leak-class D diagnostic — emit per refcount++. Net (acquire -
|
|
165
|
-
// release) = currently-mounted ThemeToggle count; should be
|
|
166
|
-
// bounded by the user's UI shape. Steady-state monotonic growth
|
|
167
|
-
// signals the refcount guard regressed (every mount registers a
|
|
168
|
-
// fresh listener again).
|
|
169
|
-
if (__DEV__) _countSink.__pyreon_count__?.('theme.initRefAcquire')
|
|
170
|
-
|
|
171
|
-
return () => {
|
|
172
|
-
_initRefCount--
|
|
173
|
-
// Pair with `theme.initRefAcquire`. Equal counts at process exit
|
|
174
|
-
// = healthy lifecycle. Diff = active subscribers / orphan inits.
|
|
175
|
-
if (__DEV__) _countSink.__pyreon_count__?.('theme.initRefRelease')
|
|
176
|
-
if (_initRefCount === 0 && _disposeShared) {
|
|
177
|
-
_disposeShared()
|
|
178
|
-
_disposeShared = null
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
})
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Theme toggle button component.
|
|
186
|
-
*
|
|
187
|
-
* @example
|
|
188
|
-
* import { ThemeToggle } from "@pyreon/zero/theme"
|
|
189
|
-
* <ThemeToggle />
|
|
190
|
-
*/
|
|
191
|
-
export function ThemeToggle(props: { class?: string; style?: string }): VNodeChild {
|
|
192
|
-
initTheme()
|
|
193
|
-
|
|
194
|
-
return (
|
|
195
|
-
<button
|
|
196
|
-
class={props.class}
|
|
197
|
-
style={props.style}
|
|
198
|
-
onClick={toggleTheme}
|
|
199
|
-
aria-label="Toggle theme"
|
|
200
|
-
title="Toggle theme"
|
|
201
|
-
type="button"
|
|
202
|
-
>
|
|
203
|
-
{() =>
|
|
204
|
-
resolvedTheme() === 'dark' ? (
|
|
205
|
-
<svg
|
|
206
|
-
width="18"
|
|
207
|
-
height="18"
|
|
208
|
-
viewBox="0 0 24 24"
|
|
209
|
-
fill="none"
|
|
210
|
-
stroke="currentColor"
|
|
211
|
-
stroke-width="2"
|
|
212
|
-
stroke-linecap="round"
|
|
213
|
-
stroke-linejoin="round"
|
|
214
|
-
aria-hidden="true"
|
|
215
|
-
>
|
|
216
|
-
<circle cx="12" cy="12" r="5" />
|
|
217
|
-
<line x1="12" y1="1" x2="12" y2="3" />
|
|
218
|
-
<line x1="12" y1="21" x2="12" y2="23" />
|
|
219
|
-
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
|
220
|
-
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
|
221
|
-
<line x1="1" y1="12" x2="3" y2="12" />
|
|
222
|
-
<line x1="21" y1="12" x2="23" y2="12" />
|
|
223
|
-
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
|
224
|
-
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
|
225
|
-
</svg>
|
|
226
|
-
) : (
|
|
227
|
-
<svg
|
|
228
|
-
width="18"
|
|
229
|
-
height="18"
|
|
230
|
-
viewBox="0 0 24 24"
|
|
231
|
-
fill="none"
|
|
232
|
-
stroke="currentColor"
|
|
233
|
-
stroke-width="2"
|
|
234
|
-
stroke-linecap="round"
|
|
235
|
-
stroke-linejoin="round"
|
|
236
|
-
aria-hidden="true"
|
|
237
|
-
>
|
|
238
|
-
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z" />
|
|
239
|
-
</svg>
|
|
240
|
-
)
|
|
241
|
-
}
|
|
242
|
-
</button>
|
|
243
|
-
)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
* Inline script to prevent flash of wrong theme.
|
|
248
|
-
* Include this in your index.html <head> BEFORE any stylesheets.
|
|
249
|
-
*
|
|
250
|
-
* @example
|
|
251
|
-
* // index.html
|
|
252
|
-
* <head>
|
|
253
|
-
* <script>{themeScript}</script>
|
|
254
|
-
* ...
|
|
255
|
-
* </head>
|
|
256
|
-
*/
|
|
257
|
-
export const themeScript = `(function(){try{var t=localStorage.getItem("${STORAGE_KEY}");var r=t==="light"?"light":t==="dark"?"dark":window.matchMedia("(prefers-color-scheme:dark)").matches?"dark":"light";document.documentElement.dataset.theme=r;document.querySelectorAll("[data-favicon-theme]").forEach(function(l){l.media=l.dataset.faviconTheme===r?"":"not all"})}catch(e){}})()`
|