@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.
Files changed (54) hide show
  1. package/package.json +10 -39
  2. package/src/actions.ts +0 -196
  3. package/src/adapters/bun.ts +0 -114
  4. package/src/adapters/cloudflare.ts +0 -166
  5. package/src/adapters/index.ts +0 -61
  6. package/src/adapters/netlify.ts +0 -154
  7. package/src/adapters/node.ts +0 -163
  8. package/src/adapters/static.ts +0 -42
  9. package/src/adapters/validate.ts +0 -23
  10. package/src/adapters/vercel.ts +0 -182
  11. package/src/adapters/warn-missing-env.ts +0 -49
  12. package/src/ai.ts +0 -623
  13. package/src/api-routes.ts +0 -219
  14. package/src/app.ts +0 -92
  15. package/src/cache.ts +0 -136
  16. package/src/client.ts +0 -143
  17. package/src/compression.ts +0 -116
  18. package/src/config.ts +0 -35
  19. package/src/cors.ts +0 -94
  20. package/src/csp.ts +0 -226
  21. package/src/entry-server.ts +0 -224
  22. package/src/env.ts +0 -344
  23. package/src/error-overlay.ts +0 -118
  24. package/src/favicon.ts +0 -841
  25. package/src/font.ts +0 -511
  26. package/src/fs-router.ts +0 -1519
  27. package/src/i18n-routing.ts +0 -533
  28. package/src/icon.tsx +0 -182
  29. package/src/icons-plugin.ts +0 -296
  30. package/src/image-plugin.ts +0 -751
  31. package/src/image-types.ts +0 -60
  32. package/src/image.tsx +0 -340
  33. package/src/index.ts +0 -92
  34. package/src/isr.ts +0 -394
  35. package/src/link.tsx +0 -304
  36. package/src/logger.ts +0 -144
  37. package/src/manifest.ts +0 -787
  38. package/src/meta.tsx +0 -354
  39. package/src/middleware.ts +0 -65
  40. package/src/not-found.ts +0 -44
  41. package/src/og-image.ts +0 -378
  42. package/src/rate-limit.ts +0 -140
  43. package/src/script.tsx +0 -260
  44. package/src/seo.ts +0 -617
  45. package/src/server.ts +0 -89
  46. package/src/sharp.d.ts +0 -22
  47. package/src/ssg-plugin.ts +0 -1582
  48. package/src/testing.ts +0 -146
  49. package/src/theme.tsx +0 -257
  50. package/src/types.ts +0 -624
  51. package/src/utils/use-intersection-observer.ts +0 -36
  52. package/src/utils/with-headers.ts +0 -13
  53. package/src/vercel-revalidate-handler.ts +0 -204
  54. 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){}})()`