@pyreon/zero 0.14.0 → 0.16.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/api-routes-Ci0kVmM4.js +146 -0
- package/lib/client.js +7 -2
- package/lib/csp.js +19 -9
- package/lib/env.js +6 -6
- package/lib/font.js +3 -3
- package/lib/{fs-router-CQ7Zxeca.js → fs-router-MewHc5SB.js} +56 -24
- package/lib/i18n-routing.js +112 -1
- package/lib/image-plugin.js +4 -0
- package/lib/image.js +141 -108
- package/lib/index.js +253 -132
- package/lib/link.js +1 -49
- package/lib/og-image.js +5 -5
- package/lib/rolldown-runtime-CjeV3_4I.js +18 -0
- package/lib/script.js +115 -74
- package/lib/seo.js +186 -15
- package/lib/server.js +275 -1247
- package/lib/theme.js +1 -50
- package/lib/types/config.d.ts +275 -3
- package/lib/types/env.d.ts +2 -2
- package/lib/types/i18n-routing.d.ts +197 -6
- package/lib/types/image.d.ts +105 -5
- package/lib/types/index.d.ts +640 -178
- package/lib/types/link.d.ts +3 -3
- package/lib/types/script.d.ts +78 -6
- package/lib/types/seo.d.ts +128 -4
- package/lib/types/server.d.ts +603 -77
- package/lib/types/theme.d.ts +2 -2
- package/lib/vite-plugin-xjWZwudX.js +2454 -0
- package/package.json +16 -13
- package/src/adapters/bun.ts +20 -1
- package/src/adapters/cloudflare.ts +78 -1
- package/src/adapters/index.ts +25 -3
- package/src/adapters/netlify.ts +63 -1
- package/src/adapters/node.ts +25 -1
- package/src/adapters/static.ts +26 -1
- package/src/adapters/validate.ts +8 -1
- package/src/adapters/vercel.ts +76 -1
- package/src/adapters/warn-missing-env.ts +49 -0
- package/src/app.ts +35 -1
- package/src/client.ts +18 -0
- package/src/csp.ts +28 -12
- package/src/entry-server.ts +55 -5
- package/src/env.ts +7 -7
- package/src/font.ts +3 -3
- package/src/fs-router.ts +123 -4
- package/src/i18n-routing.ts +246 -12
- package/src/image.tsx +242 -91
- package/src/index.ts +4 -4
- package/src/isr.ts +24 -6
- package/src/manifest.ts +675 -0
- package/src/og-image.ts +5 -5
- package/src/script.tsx +159 -36
- package/src/seo.ts +346 -15
- package/src/server.ts +10 -2
- package/src/ssg-plugin.ts +1523 -0
- package/src/types.ts +329 -19
- package/src/vercel-revalidate-handler.ts +204 -0
- package/src/vite-plugin.ts +326 -68
- package/lib/actions.js.map +0 -1
- package/lib/ai.js.map +0 -1
- package/lib/api-routes.js.map +0 -1
- package/lib/cache.js.map +0 -1
- package/lib/client.js.map +0 -1
- package/lib/compression.js.map +0 -1
- package/lib/config.js.map +0 -1
- package/lib/cors.js.map +0 -1
- package/lib/csp.js.map +0 -1
- package/lib/env.js.map +0 -1
- package/lib/favicon.js.map +0 -1
- package/lib/font.js.map +0 -1
- package/lib/fs-router-3xzp-4Wj.js.map +0 -1
- package/lib/fs-router-CQ7Zxeca.js.map +0 -1
- package/lib/i18n-routing.js.map +0 -1
- package/lib/image-plugin.js.map +0 -1
- package/lib/image.js.map +0 -1
- package/lib/index.js.map +0 -1
- package/lib/link.js.map +0 -1
- package/lib/logger.js.map +0 -1
- package/lib/meta.js.map +0 -1
- package/lib/middleware.js.map +0 -1
- package/lib/og-image.js.map +0 -1
- package/lib/rate-limit.js.map +0 -1
- package/lib/script.js.map +0 -1
- package/lib/seo.js.map +0 -1
- package/lib/server.js.map +0 -1
- package/lib/testing.js.map +0 -1
- package/lib/theme.js.map +0 -1
- package/lib/types/actions.d.ts.map +0 -1
- package/lib/types/ai.d.ts.map +0 -1
- package/lib/types/api-routes.d.ts.map +0 -1
- package/lib/types/cache.d.ts.map +0 -1
- package/lib/types/client.d.ts.map +0 -1
- package/lib/types/compression.d.ts.map +0 -1
- package/lib/types/config.d.ts.map +0 -1
- package/lib/types/cors.d.ts.map +0 -1
- package/lib/types/csp.d.ts.map +0 -1
- package/lib/types/env.d.ts.map +0 -1
- package/lib/types/favicon.d.ts.map +0 -1
- package/lib/types/font.d.ts.map +0 -1
- package/lib/types/i18n-routing.d.ts.map +0 -1
- package/lib/types/image-plugin.d.ts.map +0 -1
- package/lib/types/image.d.ts.map +0 -1
- package/lib/types/index.d.ts.map +0 -1
- package/lib/types/link.d.ts.map +0 -1
- package/lib/types/logger.d.ts.map +0 -1
- package/lib/types/meta.d.ts.map +0 -1
- package/lib/types/middleware.d.ts.map +0 -1
- package/lib/types/og-image.d.ts.map +0 -1
- package/lib/types/rate-limit.d.ts.map +0 -1
- package/lib/types/script.d.ts.map +0 -1
- package/lib/types/seo.d.ts.map +0 -1
- package/lib/types/server.d.ts.map +0 -1
- package/lib/types/testing.d.ts.map +0 -1
- package/lib/types/theme.d.ts.map +0 -1
package/lib/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../src/utils/use-intersection-observer.ts","../../../core/core/lib/jsx-runtime.js","../src/image.tsx","../src/link.tsx","../src/script.tsx","../src/i18n-routing.ts","../src/meta.tsx","../src/theme.tsx","../src/index.ts"],"sourcesContent":["import { onMount, onUnmount } from '@pyreon/core'\n\n/**\n * Observes an element and calls `onIntersect` once it enters the viewport.\n * Automatically disconnects after the first intersection.\n *\n * @param getElement - Getter for the target element (may be undefined before mount).\n * @param onIntersect - Callback fired when the element becomes visible.\n * @param rootMargin - IntersectionObserver rootMargin. Default: \"200px\".\n */\nexport function useIntersectionObserver(\n getElement: () => HTMLElement | undefined,\n onIntersect: () => void,\n rootMargin = '200px',\n) {\n onMount(() => {\n const el = getElement()\n if (!el) return undefined\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n onIntersect()\n observer.disconnect()\n }\n }\n },\n { rootMargin },\n )\n\n observer.observe(el)\n onUnmount(() => observer.disconnect())\n return undefined\n })\n}\n","//#region src/h.ts\n/** Marker for fragment nodes — renders children without a wrapper element */\nconst Fragment = Symbol(\"Pyreon.Fragment\");\n/**\n* Hyperscript function — the compiled output of JSX.\n* `<div class=\"x\">hello</div>` → `h(\"div\", { class: \"x\" }, \"hello\")`\n*\n* Generic on P so TypeScript validates props match the component's signature\n* at the call site, then stores the result in the loosely-typed VNode.\n*/\n/** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */\nconst EMPTY_PROPS = {};\nfunction h(type, props, ...children) {\n\treturn {\n\t\ttype,\n\t\tprops: props ?? EMPTY_PROPS,\n\t\tchildren: normalizeChildren(children),\n\t\tkey: props?.key ?? null\n\t};\n}\nfunction normalizeChildren(children) {\n\tfor (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);\n\treturn children;\n}\nfunction flattenChildren(children) {\n\tconst result = [];\n\tfor (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));\n\telse result.push(child);\n\treturn result;\n}\n\n//#endregion\n//#region src/jsx-runtime.ts\n/**\n* JSX automatic runtime.\n*\n* When tsconfig has `\"jsxImportSource\": \"@pyreon/core\"`, the TS/bundler compiler\n* rewrites JSX to imports from this file automatically:\n* <div class=\"x\" /> → jsx(\"div\", { class: \"x\" })\n*/\nfunction jsx(type, props, key) {\n\tconst { children, ...rest } = props;\n\tconst propsWithKey = key != null ? {\n\t\t...rest,\n\t\tkey\n\t} : rest;\n\tif (typeof type === \"function\") return h(type, children !== void 0 ? {\n\t\t...propsWithKey,\n\t\tchildren\n\t} : propsWithKey);\n\treturn h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);\n}\nconst jsxs = jsx;\n\n//#endregion\nexport { Fragment, jsx, jsxs };\n//# sourceMappingURL=jsx-runtime.js.map","import type { VNodeChild } from '@pyreon/core'\nimport { createRef } from '@pyreon/core'\nimport { signal } from '@pyreon/reactivity'\nimport type { FormatSource } from './image-plugin'\nimport { useIntersectionObserver } from './utils/use-intersection-observer'\n\n// ─── Image optimization component ───────────────────────────────────────────\n//\n// <Image> provides:\n// - Lazy loading via IntersectionObserver (loads when near viewport)\n// - Automatic width/height to prevent CLS (Cumulative Layout Shift)\n// - Responsive srcset generation from width descriptors\n// - Multi-format support via <picture> (WebP/AVIF with fallback)\n// - Blur-up placeholder while loading\n// - Priority loading for above-the-fold images\n\nexport interface ImageProps {\n /** Image source URL. */\n src: string\n /** Alt text (required for accessibility). */\n alt: string\n /** Intrinsic width of the image. */\n width: number\n /** Intrinsic height of the image. */\n height: number\n /** Responsive sizes attribute. Default: \"100vw\" */\n sizes?: string\n /** Responsive srcset string or source array. */\n srcset?: string | ImageSource[]\n /** Per-format source sets for <picture>. Provided automatically by imagePlugin. */\n formats?: FormatSource[]\n /** Loading strategy. \"lazy\" uses IntersectionObserver, \"eager\" loads immediately. Default: \"lazy\" */\n loading?: 'lazy' | 'eager'\n /** Mark as priority (LCP image). Disables lazy loading, adds fetchPriority=\"high\". */\n priority?: boolean\n /** Low-quality placeholder image URL or base64 data URI for blur-up effect. */\n placeholder?: string\n /** CSS class name. */\n class?: string\n /** Inline styles. */\n style?: string\n /** CSS object-fit. Default: \"cover\" */\n fit?: 'cover' | 'contain' | 'fill' | 'none' | 'scale-down'\n /** Decode async. Default: true */\n decoding?: 'sync' | 'async' | 'auto'\n /**\n * Raw mode — renders a plain `<img>` without the container div,\n * aspect-ratio, max-width, or lazy loading wrapper.\n * Use when the Image is inside a custom layout (absolute positioning, etc.).\n */\n raw?: boolean\n}\n\nexport interface ImageSource {\n src: string\n width: number\n}\n\n/**\n * Optimized image component with lazy loading, responsive images,\n * multi-format <picture> support, and blur-up placeholders.\n *\n * @example\n * // With imagePlugin — spread the import directly\n * import hero from \"./hero.jpg?optimize\"\n * <Image {...hero} alt=\"Hero\" priority />\n *\n * @example\n * // Manual usage\n * <Image src=\"/hero.jpg\" alt=\"Hero\" width={1200} height={630} />\n */\nexport function Image(props: ImageProps): VNodeChild {\n // Raw mode: plain <img> without container, lazy loading, or layout constraints\n if (props.raw) {\n return (\n <img\n src={props.src}\n alt={props.alt}\n width={props.width}\n height={props.height}\n class={props.class}\n style={props.style}\n decoding={props.decoding ?? 'async'}\n loading={props.loading ?? 'lazy'}\n fetchPriority={props.priority ? 'high' : undefined}\n />\n ) as any\n }\n\n const isEager = props.priority || props.loading === 'eager'\n const loaded = signal(isEager)\n const inView = signal(isEager)\n const containerRef = createRef<HTMLElement>()\n\n // Resolve srcset from string or array\n const resolvedSrcset =\n typeof props.srcset === 'string'\n ? props.srcset\n : props.srcset?.map((s) => `${s.src} ${s.width}w`).join(', ')\n\n const sizes = props.sizes ?? '100vw'\n const fit = props.fit ?? 'cover'\n const hasFormats = props.formats && props.formats.length > 0\n const aspectRatio = `${props.width} / ${props.height}`\n\n if (!isEager) {\n useIntersectionObserver(\n () => containerRef.current ?? undefined,\n () => inView.set(true),\n )\n }\n\n // Static styles (don't depend on signals)\n const containerStyle = [\n 'position: relative',\n 'overflow: hidden',\n `aspect-ratio: ${aspectRatio}`,\n `max-width: ${props.width}px`,\n 'width: 100%',\n props.style,\n ]\n .filter(Boolean)\n .join('; ')\n\n const imgEl = (\n <img\n src={() => (inView() ? props.src : '')}\n srcSet={() => (!hasFormats && inView() && resolvedSrcset ? resolvedSrcset : '')}\n sizes={resolvedSrcset ? sizes : undefined}\n alt={props.alt}\n width={props.width}\n height={props.height}\n loading={isEager ? 'eager' : 'lazy'}\n decoding={props.decoding ?? 'async'}\n fetchPriority={props.priority ? 'high' : undefined}\n onLoad={() => loaded.set(true)}\n style={() =>\n [\n 'display: block',\n 'width: 100%',\n 'height: 100%',\n `object-fit: ${fit}`,\n 'transition: opacity 0.3s ease',\n props.placeholder && !loaded() ? 'opacity: 0' : 'opacity: 1',\n ].join('; ')\n }\n />\n )\n\n return (\n <div ref={containerRef} class={props.class} style={containerStyle}>\n {props.placeholder && (\n <img\n src={props.placeholder}\n alt=\"\"\n aria-hidden=\"true\"\n loading=\"eager\"\n style={() =>\n [\n 'position: absolute',\n 'inset: 0',\n 'width: 100%',\n 'height: 100%',\n 'object-fit: cover',\n 'filter: blur(20px)',\n 'transform: scale(1.1)',\n 'transition: opacity 0.4s ease',\n loaded() ? 'opacity: 0; pointer-events: none' : 'opacity: 1',\n ].join('; ')\n }\n />\n )}\n {hasFormats ? (\n <picture>\n {props.formats?.map((fmt) => (\n <source\n type={fmt.type}\n srcSet={() => (inView() ? (fmt.srcset ?? '') : '')}\n sizes={sizes}\n />\n ))}\n {imgEl}\n </picture>\n ) : (\n imgEl\n )}\n </div>\n )\n}\n","import { createRef } from '@pyreon/core'\nimport { useRouter } from '@pyreon/router'\nimport { useIntersectionObserver } from './utils/use-intersection-observer'\n\n// ─── Link component with prefetching ────────────────────────────────────────\n//\n// Provides client-side navigation, prefetching, and active state tracking.\n// Three levels of API:\n//\n// 1. useLink(props) — composable returning handlers, state, and ref callback\n// 2. createLink(Comp) — HOC wrapping any component with link behavior\n// 3. Link — default <a>-based link (built on createLink)\n\nexport interface LinkProps {\n /** Target URL path. */\n href: string\n /** Link content. */\n children?: any\n /** CSS class name. */\n class?: string\n /** Class applied when this link matches the current route. */\n activeClass?: string\n /** Class applied when this link exactly matches the current route. */\n exactActiveClass?: string\n /** Prefetch strategy. Default: \"hover\" */\n prefetch?: 'hover' | 'viewport' | 'none'\n /** Open in new tab. */\n external?: boolean\n /** Inline styles. */\n style?: string\n /** ARIA label. */\n 'aria-label'?: string\n /** Additional click handler — called before navigation. Call e.preventDefault() to cancel. */\n onClick?: ((e: MouseEvent) => void) | undefined\n}\n\n/** Props passed to a custom component via createLink. */\nexport interface LinkRenderProps {\n href: string\n ref: import('@pyreon/core').Ref<HTMLAnchorElement>\n onClick: (e: MouseEvent) => void\n onMouseEnter: () => void\n onTouchStart: () => void\n isActive: () => boolean\n isExactActive: () => boolean\n /** Reactive class string — pass directly to element for auto-updates on route change. */\n class: (() => string) | string | undefined\n style?: string\n target?: string\n rel?: string\n 'aria-label'?: string\n children?: any\n}\n\n/** Return type of useLink. */\nexport interface UseLinkReturn {\n /** Ref object — attach to the root element for viewport-based prefetch. */\n ref: import('@pyreon/core').Ref<HTMLAnchorElement>\n /** Click handler — performs client-side navigation. */\n handleClick: (e: MouseEvent) => void\n /** Mouse enter handler — triggers hover prefetch. */\n handleMouseEnter: () => void\n /** Touch start handler — triggers prefetch on mobile. */\n handleTouchStart: () => void\n /** Whether the link partially matches the current route. */\n isActive: () => boolean\n /** Whether the link exactly matches the current route. */\n isExactActive: () => boolean\n /** Resolved class string including active classes. */\n classes: () => string\n}\n\nconst MAX_PREFETCH_CACHE = 200\n// Maps href → list of <link> elements injected into <head>. When the\n// cache evicts an href (FIFO at MAX_PREFETCH_CACHE), the matching <link>\n// elements must be removed too — otherwise head bloats unboundedly\n// across long SPA sessions (every Link interaction added 2 <link> nodes\n// with no cleanup).\nconst prefetched = new Map<string, Element[]>()\n\nfunction doPrefetch(href: string) {\n // Prefetch only fires from browser-mounted Link interactions (hover /\n // click intent). Explicit guard documents the SSR-safety contract.\n if (typeof document === 'undefined') return\n if (prefetched.has(href)) return\n // Evict oldest entries when cache is full — AND remove their DOM nodes.\n if (prefetched.size >= MAX_PREFETCH_CACHE) {\n const firstEntry = prefetched.entries().next().value\n if (firstEntry) {\n const [oldestHref, oldestLinks] = firstEntry\n for (const link of oldestLinks) link.remove()\n prefetched.delete(oldestHref)\n }\n }\n\n const injected: Element[] = []\n const docLink = document.createElement('link')\n docLink.rel = 'prefetch'\n docLink.href = href\n docLink.as = 'document'\n document.head.appendChild(docLink)\n injected.push(docLink)\n\n try {\n const chunkHint = document.createElement('link')\n chunkHint.rel = 'modulepreload'\n chunkHint.href = href\n document.head.appendChild(chunkHint)\n injected.push(chunkHint)\n } catch {\n // modulepreload is a hint, not critical\n }\n\n prefetched.set(href, injected)\n}\n\n/**\n * Prefetch a route's JS chunk by injecting `<link rel=\"prefetch\">` into the\n * document head. Deduplicates — calling with the same href twice is a no-op.\n *\n * @example\n * prefetchRoute('/about')\n * prefetchRoute('/dashboard')\n */\nexport function prefetchRoute(href: string): void {\n doPrefetch(href)\n}\n\n/**\n * Composable that provides all link behavior — navigation, prefetching,\n * active state, and viewport observation.\n *\n * Use this for full control when `createLink` is too opinionated.\n *\n * @example\n * function MyLink(props: LinkProps) {\n * const link = useLink(props)\n * return (\n * <button ref={link.ref} class={link.classes()} onClick={link.handleClick}>\n * {props.children}\n * </button>\n * )\n * }\n */\nexport function useLink(props: LinkProps): UseLinkReturn {\n const router = useRouter()\n const elementRef = createRef<HTMLAnchorElement>()\n const strategy = props.prefetch ?? 'hover'\n\n function handleClick(e: MouseEvent) {\n // Call user's onClick first — they may call e.preventDefault()\n if (props.onClick) {\n ;(props.onClick as (e: MouseEvent) => void)(e)\n }\n\n if (\n e.defaultPrevented ||\n e.button !== 0 ||\n e.metaKey ||\n e.ctrlKey ||\n e.shiftKey ||\n e.altKey ||\n props.external ||\n !props.href\n ) {\n return\n }\n e.preventDefault()\n router.push(props.href)\n }\n\n function handleMouseEnter() {\n if (strategy === 'hover') {\n doPrefetch(props.href)\n }\n }\n\n function handleTouchStart() {\n if (strategy === 'hover' || strategy === 'viewport') {\n doPrefetch(props.href)\n }\n }\n\n if (strategy === 'viewport') {\n useIntersectionObserver(\n () => elementRef.current ?? undefined,\n () => doPrefetch(props.href),\n )\n }\n\n const isActive = () => {\n const currentPath = router.currentRoute()?.path\n if (!currentPath || !props.href) return false\n if (props.href === '/') return currentPath === '/'\n return currentPath.startsWith(props.href)\n }\n\n const isExactActive = () => {\n const currentPath = router.currentRoute()?.path\n if (!currentPath) return false\n return currentPath === props.href\n }\n\n const classes = () => {\n const cls: string[] = []\n if (props.class) cls.push(props.class)\n if (props.activeClass && isActive()) cls.push(props.activeClass)\n if (props.exactActiveClass && isExactActive()) cls.push(props.exactActiveClass)\n return cls.join(' ')\n }\n\n return {\n ref: elementRef,\n handleClick,\n handleMouseEnter,\n handleTouchStart,\n isActive,\n isExactActive,\n classes,\n }\n}\n\n/**\n * Higher-order component that wraps any component with link behavior.\n *\n * The wrapped component receives {@link LinkRenderProps} with all handlers,\n * active state, and accessibility attributes pre-wired.\n *\n * @example\n * // Custom button link\n * const ButtonLink = createLink((props) => (\n * <button\n * ref={props.ref}\n * class={props.class}\n * onClick={props.onClick}\n * onMouseEnter={props.onMouseEnter}\n * >\n * {props.children}\n * </button>\n * ))\n *\n * // Custom styled component\n * const CardLink = createLink((props) => (\n * <div\n * ref={props.ref}\n * class={`card ${props.isActive() ? \"card--active\" : \"\"}`}\n * onClick={props.onClick}\n * onMouseEnter={props.onMouseEnter}\n * >\n * {props.children}\n * </div>\n * ))\n *\n * // Usage\n * <ButtonLink href=\"/about\">About</ButtonLink>\n * <CardLink href=\"/posts\" prefetch=\"viewport\">Posts</CardLink>\n */\nexport function createLink(Component: (props: LinkRenderProps) => any): (props: LinkProps) => any {\n return function WrappedLink(props: LinkProps) {\n const link = useLink(props)\n\n return (\n <Component\n href={props.href}\n ref={link.ref}\n onClick={link.handleClick}\n onMouseEnter={link.handleMouseEnter}\n onTouchStart={link.handleTouchStart}\n isActive={link.isActive}\n isExactActive={link.isExactActive}\n class={link.classes}\n {...(props.style ? { style: props.style } : {})}\n {...(props.external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}\n {...(props['aria-label'] ? { 'aria-label': props['aria-label'] } : {})}\n children={props.children}\n />\n )\n }\n}\n\n/**\n * Default navigation link built on an `<a>` tag.\n *\n * @example\n * <Link href=\"/about\" prefetch=\"viewport\">About</Link>\n * <Link href=\"/posts\" activeClass=\"nav-active\">Posts</Link>\n */\nexport const Link = createLink((props: LinkRenderProps) => (\n <a\n ref={props.ref}\n href={props.href}\n {...(props.class ? { class: props.class } : {})}\n {...(props.style ? { style: props.style } : {})}\n {...(props.target ? { target: props.target } : {})}\n {...(props.rel ? { rel: props.rel } : {})}\n {...(props['aria-label'] ? { 'aria-label': props['aria-label'] } : {})}\n {...(props.isExactActive() ? { 'aria-current': 'page' as const } : {})}\n onClick={props.onClick}\n onMouseEnter={props.onMouseEnter}\n onTouchStart={props.onTouchStart}\n >\n {props.children}\n </a>\n))\n","import type { VNodeChild } from '@pyreon/core'\nimport { createRef, onMount, onUnmount } from '@pyreon/core'\nimport { useIntersectionObserver } from './utils/use-intersection-observer'\n\n// ─── Script optimization component ─────────────────────────────────────────\n//\n// <Script> provides optimized third-party script loading:\n// - Defer loading until after hydration\n// - Load on idle (requestIdleCallback)\n// - Load on interaction (click, scroll, etc.)\n// - Load on viewport entry\n// - Worker offloading for analytics scripts\n\nexport interface ScriptProps {\n /** Script source URL. */\n src: string\n /** Loading strategy. Default: \"afterHydration\" */\n strategy?: ScriptStrategy\n /** Inline script content (alternative to src). */\n children?: string\n /** Script id for deduplication. */\n id?: string\n /** Async attribute. Default: true */\n async?: boolean\n /** onLoad callback. */\n onLoad?: () => void\n /** onError callback. */\n onError?: (error: Error) => void\n}\n\nexport type ScriptStrategy =\n | 'beforeHydration'\n | 'afterHydration'\n | 'onIdle'\n | 'onInteraction'\n | 'onViewport'\n\n/**\n * Optimized script loading component.\n *\n * @example\n * // Load analytics after page is interactive\n * <Script src=\"https://analytics.example.com/script.js\" strategy=\"onIdle\" />\n *\n * // Load chat widget when user scrolls\n * <Script src=\"/chat-widget.js\" strategy=\"onViewport\" />\n *\n * // Inline script with deferred execution\n * <Script strategy=\"afterHydration\">\n * {`console.log(\"App hydrated!\")`}\n * </Script>\n */\nexport function Script(props: ScriptProps): VNodeChild {\n function loadScript() {\n // Only invoked from `onMount` — explicit guard documents the\n // SSR-safety contract at the callsite (the rule can't AST-trace the\n // indirect call).\n if (typeof document === 'undefined') return\n // Deduplication\n if (props.id && document.getElementById(props.id)) return\n\n const script = document.createElement('script')\n if (props.src) script.src = props.src\n if (props.id) script.id = props.id\n script.async = props.async !== false\n\n if (props.onLoad) script.onload = props.onLoad\n if (props.onError) {\n script.onerror = () => props.onError?.(new Error(`Failed to load: ${props.src}`))\n }\n\n if (props.children && !props.src) {\n script.textContent = props.children\n }\n\n document.head.appendChild(script)\n }\n\n onMount(() => {\n const strategy = props.strategy ?? 'afterHydration'\n\n switch (strategy) {\n case 'beforeHydration':\n // Already in HTML — do nothing\n break\n\n case 'afterHydration':\n // Load immediately after mount (hydration is complete)\n loadScript()\n break\n\n case 'onIdle':\n if ('requestIdleCallback' in window) {\n requestIdleCallback(() => loadScript(), { timeout: 5000 })\n } else {\n setTimeout(loadScript, 200)\n }\n break\n\n case 'onInteraction': {\n const events = ['click', 'scroll', 'keydown', 'touchstart']\n function handler() {\n for (const e of events) document.removeEventListener(e, handler)\n loadScript()\n }\n for (const e of events) {\n document.addEventListener(e, handler, { once: true, passive: true })\n }\n onUnmount(() => {\n for (const e of events) document.removeEventListener(e, handler)\n })\n break\n }\n\n case 'onViewport':\n // Handled below via useIntersectionObserver on the sentinel element\n break\n }\n return undefined\n })\n\n const sentinelRef = createRef<HTMLElement>()\n const strategy = props.strategy ?? 'afterHydration'\n\n if (strategy === 'onViewport') {\n useIntersectionObserver(\n () => sentinelRef.current ?? undefined,\n () => loadScript(),\n )\n }\n\n if (strategy === 'onViewport') {\n return <div ref={sentinelRef} style=\"width:0;height:0;overflow:hidden\" />\n }\n\n return null\n}\n","import { createContext } from '@pyreon/core'\nimport { signal } from '@pyreon/reactivity'\nimport type { Plugin } from 'vite'\n\n// ─── Localized routing ─────────────────────────────────────────────────────\n//\n// Adds locale-prefixed routes to Zero's file-system router:\n// - /about → /en/about, /de/about, /cs/about\n// - / → /en, /de, /cs (or default locale without prefix)\n// - Automatic locale detection from Accept-Language header\n// - Redirect to preferred locale\n// - hreflang link generation\n//\n// Usage:\n// import { i18nRouting } from \"@pyreon/zero\"\n// export default { plugins: [Pyreon], defaultLocale: \"en\" })] }\n\nexport interface I18nRoutingConfig {\n /** Supported locales. e.g. [\"en\", \"de\", \"cs\"] */\n locales: string[]\n /** Default locale — served without prefix (/ instead of /en/). */\n defaultLocale: string\n /** Redirect root to detected locale. Default: true */\n detectLocale?: boolean\n /** Cookie name to persist locale preference. Default: \"locale\" */\n cookieName?: string\n /** URL strategy. Default: \"prefix-except-default\" */\n strategy?: 'prefix' | 'prefix-except-default'\n}\n\nexport interface LocaleContext {\n /** Current locale code. e.g. \"en\", \"de\" */\n locale: string\n /** All supported locales. */\n locales: string[]\n /** Default locale. */\n defaultLocale: string\n /** Build a localized path. e.g. localePath(\"/about\", \"de\") → \"/de/about\" */\n localePath: (path: string, locale?: string) => string\n /** Get hreflang alternates for the current path. */\n alternates: () => Array<{ locale: string; url: string }>\n}\n\n/**\n * Detect preferred locale from Accept-Language header.\n */\nexport function detectLocaleFromHeader(\n acceptLanguage: string | null | undefined,\n locales: string[],\n defaultLocale: string,\n): string {\n if (!acceptLanguage) return defaultLocale\n\n // Parse Accept-Language: en-US,en;q=0.9,de;q=0.8\n const preferred = acceptLanguage\n .split(',')\n .map((part) => {\n const [lang, q] = part.trim().split(';q=')\n return {\n lang: lang?.split('-')[0]?.toLowerCase() ?? '',\n quality: q ? Number.parseFloat(q) : 1,\n }\n })\n .sort((a, b) => b.quality - a.quality)\n\n for (const { lang } of preferred) {\n if (locales.includes(lang)) return lang\n }\n\n return defaultLocale\n}\n\n/**\n * Extract locale from a URL path.\n * Returns { locale, pathWithoutLocale }.\n */\nexport function extractLocaleFromPath(\n path: string,\n locales: string[],\n defaultLocale: string,\n): { locale: string; pathWithoutLocale: string } {\n const segments = path.split('/').filter(Boolean)\n const firstSegment = segments[0]?.toLowerCase()\n\n if (firstSegment && locales.includes(firstSegment)) {\n return {\n locale: firstSegment,\n pathWithoutLocale: '/' + segments.slice(1).join('/') || '/',\n }\n }\n\n return { locale: defaultLocale, pathWithoutLocale: path }\n}\n\n/**\n * Build a localized path.\n */\nexport function buildLocalePath(\n path: string,\n locale: string,\n defaultLocale: string,\n strategy: 'prefix' | 'prefix-except-default',\n): string {\n const clean = path === '/' ? '' : path\n if (strategy === 'prefix-except-default' && locale === defaultLocale) {\n return path\n }\n return `/${locale}${clean}`\n}\n\n/**\n * Create a LocaleContext for use in components and loaders.\n */\nexport function createLocaleContext(\n locale: string,\n path: string,\n config: I18nRoutingConfig,\n): LocaleContext {\n const strategy = config.strategy ?? 'prefix-except-default'\n\n return {\n locale,\n locales: config.locales,\n defaultLocale: config.defaultLocale,\n\n localePath(targetPath: string, targetLocale?: string) {\n return buildLocalePath(\n targetPath,\n targetLocale ?? locale,\n config.defaultLocale,\n strategy,\n )\n },\n\n alternates() {\n const { pathWithoutLocale } = extractLocaleFromPath(\n path,\n config.locales,\n config.defaultLocale,\n )\n return config.locales.map((loc) => ({\n locale: loc,\n url: buildLocalePath(pathWithoutLocale, loc, config.defaultLocale, strategy),\n }))\n },\n }\n}\n\n/**\n * I18n routing middleware for Zero's server.\n *\n * - Detects locale from URL prefix or Accept-Language header\n * - Redirects root to preferred locale (when detectLocale is true)\n * - Sets locale context for loaders and components\n *\n * @example\n * ```ts\n * // zero.config.ts\n * import { i18nRouting } from \"@pyreon/zero\"\n *\n * export default defineConfig({\n * plugins: [\n * i18nRouting({\n * locales: [\"en\", \"de\", \"cs\"],\n * defaultLocale: \"en\",\n * }),\n * ],\n * })\n * ```\n */\nexport function i18nRouting(config: I18nRoutingConfig): Plugin {\n const strategy = config.strategy ?? 'prefix-except-default'\n const detectEnabled = config.detectLocale !== false\n const cookieName = config.cookieName ?? 'locale'\n\n return {\n name: 'pyreon-zero-i18n-routing',\n\n // Route duplication is NOT handled here. The fs-router's `scanRouteFiles`\n // consumes the i18n config to duplicate routes per locale at build time.\n // This plugin only provides: (1) the server middleware for locale detection\n // and (2) the runtime hooks (useLocale, setLocale) for client-side use.\n configResolved() {},\n\n configureServer(server) {\n server.middlewares.use((req, res, next) => {\n const url = req.url ?? '/'\n\n // Skip static assets\n if (url.startsWith('/@') || url.startsWith('/__') || url.includes('.')) {\n return next()\n }\n\n const { locale } = extractLocaleFromPath(\n url,\n config.locales,\n config.defaultLocale,\n )\n\n // Redirect root to detected locale\n if (detectEnabled && url === '/') {\n const cookies = parseCookies(req.headers.cookie)\n const preferredFromCookie = cookies[cookieName]\n const preferredFromHeader = detectLocaleFromHeader(\n req.headers['accept-language'],\n config.locales,\n config.defaultLocale,\n )\n const preferred = preferredFromCookie && config.locales.includes(preferredFromCookie)\n ? preferredFromCookie\n : preferredFromHeader\n\n if (strategy === 'prefix' || preferred !== config.defaultLocale) {\n res.writeHead(302, { Location: `/${preferred}/` })\n res.end()\n return\n }\n }\n\n // Attach locale context to request for loaders\n ;(req as any).__locale = locale\n ;(req as any).__localeContext = createLocaleContext(locale, url, config)\n\n // Update the module-level signal so useLocale() returns the correct value\n localeSignal.set(locale)\n\n next()\n })\n },\n }\n}\n\nfunction parseCookies(header: string | undefined): Record<string, string> {\n if (!header) return {}\n const result: Record<string, string> = {}\n for (const pair of header.split(';')) {\n const [key, value] = pair.trim().split('=')\n if (key && value) result[key] = decodeURIComponent(value)\n }\n return result\n}\n\n// ─── Reactive locale hook ───────────────────────────────────────────────────\n\n/** @internal Context for the current locale. */\nexport const LocaleCtx = createContext<string>('en')\n\n/** Current locale signal — set by the server middleware or client-side detection. */\nexport const localeSignal = signal('en')\n\n/**\n * Read the current locale reactively.\n *\n * Returns the locale signal value directly — reactive in both SSR and CSR.\n * The server middleware sets `localeSignal` per-request, and client-side\n * `setLocale()` updates it as well.\n *\n * @example\n * ```tsx\n * const locale = useLocale() // \"en\", \"de\", etc.\n * ```\n */\nexport function useLocale(): string {\n return localeSignal()\n}\n\n/**\n * Set the locale client-side and update the URL.\n *\n * @example\n * ```tsx\n * <button onClick={() => setLocale('de')}>Deutsch</button>\n * ```\n */\nexport function setLocale(\n locale: string,\n config: I18nRoutingConfig,\n): void {\n localeSignal.set(locale)\n\n // Persist to cookie\n if (typeof document !== 'undefined') {\n document.cookie = `${config.cookieName ?? 'locale'}=${locale}; path=/; max-age=31536000`\n }\n\n // Navigate to localized URL — use pushState to avoid full page reload\n if (typeof window !== 'undefined') {\n const strategy = config.strategy ?? 'prefix-except-default'\n const { pathWithoutLocale } = extractLocaleFromPath(\n window.location.pathname,\n config.locales,\n config.defaultLocale,\n )\n const newPath = buildLocalePath(pathWithoutLocale, locale, config.defaultLocale, strategy)\n window.history.pushState(null, '', newPath)\n // Dispatch popstate so @pyreon/router picks up the URL change\n window.dispatchEvent(new PopStateEvent('popstate'))\n }\n}\n","import type { VNodeChild } from '@pyreon/core'\nimport type { UseHeadInput } from '@pyreon/head'\nimport { useHead } from '@pyreon/head'\nimport type { I18nRoutingConfig } from './i18n-routing'\nimport { extractLocaleFromPath } from './i18n-routing'\n\n// ─── Inline helpers (no node:fs dependency) ─────────────────────────────────\n// These are inlined to avoid importing from favicon.ts/og-image.ts which\n// pull in node:fs at the top level — making Meta unsafe for client bundles.\n\n/** Favicon plugin config shape (type-only). */\ninterface FaviconPluginConfig {\n source: string\n themeColor?: string\n manifest?: boolean\n locales?: Record<string, { source: string; darkSource?: string }>\n [key: string]: unknown\n}\n\nfunction faviconLinks(\n locale: string | undefined,\n config: FaviconPluginConfig,\n): Array<{ rel: string; type?: string; sizes?: string; href: string }> {\n const hasLocaleOverride = locale && config.locales?.[locale]\n const prefix = hasLocaleOverride ? `/${locale}` : ''\n const isSvg = (hasLocaleOverride ? config.locales![locale]!.source : config.source).endsWith('.svg')\n const links: Array<{ rel: string; type?: string; sizes?: string; href: string }> = []\n if (isSvg) links.push({ rel: 'icon', type: 'image/svg+xml', href: `${prefix}/favicon.svg` })\n links.push(\n { rel: 'icon', type: 'image/png', sizes: '32x32', href: `${prefix}/favicon-32x32.png` },\n { rel: 'icon', type: 'image/png', sizes: '16x16', href: `${prefix}/favicon-16x16.png` },\n { rel: 'apple-touch-icon', sizes: '180x180', href: `${prefix}/apple-touch-icon.png` },\n )\n if (config.manifest !== false) links.push({ rel: 'manifest', href: `${prefix}/site.webmanifest` })\n return links\n}\n\nfunction ogImagePath(templateName: string, locale?: string, outDir = 'og', format: 'png' | 'jpeg' = 'png'): string {\n const ext = format === 'jpeg' ? 'jpg' : 'png'\n const suffix = locale ? `-${locale}` : ''\n return `/${outDir}/${templateName}${suffix}.${ext}`\n}\n\n// ─── Meta component ────────────────────────────────────────────────────────\n\nexport interface MetaProps {\n /** Page title. Accepts reactive accessor `() => string`. */\n title?: string | (() => string)\n /** Page description. Accepts reactive accessor. */\n description?: string | (() => string)\n /** Canonical URL. Also sets og:url. */\n canonical?: string\n /** Open Graph image URL. Also sets twitter:image. */\n image?: string\n /** Image alt text for accessibility. */\n imageAlt?: string\n /** Image width in pixels (og:image:width). Helps crawlers layout before loading. */\n imageWidth?: number\n /** Image height in pixels (og:image:height). */\n imageHeight?: number\n /** Open Graph type. Default: \"website\" */\n type?: 'website' | 'article' | 'product' | 'profile'\n /** Site name for og:site_name. */\n siteName?: string\n /** Twitter card type. Default: \"summary_large_image\" */\n twitterCard?: 'summary' | 'summary_large_image' | 'app' | 'player'\n /** Twitter @handle. */\n twitterSite?: string\n /** Twitter creator @handle. */\n twitterCreator?: string\n /** Locale. Default: \"en_US\" */\n locale?: string\n /** Alternate locales for hreflang. */\n alternateLocales?: Array<{ locale: string; url: string }>\n /** Robots directives. Default: \"index, follow\" */\n robots?: string\n /** Convenience: set `true` to emit `noindex, nofollow`. Overrides `robots`. */\n noIndex?: boolean\n /** Published time (ISO 8601) for article type. */\n publishedTime?: string\n /** Modified time (ISO 8601) for article type. */\n modifiedTime?: string\n /** Article author. */\n author?: string\n /** Article tags. */\n tags?: string[]\n /** JSON-LD structured data object. */\n jsonLd?: Record<string, unknown>\n /** Additional custom meta tags. */\n extra?: Array<{ name?: string; property?: string; content: string }>\n /**\n * Open Graph video URL. Also sets og:video:type if the URL ends with\n * a known extension (.mp4, .webm).\n */\n video?: string\n /** Video width in pixels. */\n videoWidth?: number\n /** Video height in pixels. */\n videoHeight?: number\n /**\n * Open Graph audio URL.\n */\n audio?: string\n /**\n * I18n routing config — when provided, auto-generates hreflang alternate\n * links for all locales based on the current path.\n * Also sets og:locale and og:locale:alternate.\n */\n i18n?: I18nRoutingConfig\n /** Base URL for building absolute hreflang URLs. e.g. \"https://example.com\" */\n origin?: string\n /**\n * Favicon plugin config — when provided, injects locale-aware favicon\n * `<link>` tags into `<head>`. Uses the current locale to select\n * the correct favicon set.\n */\n favicon?: FaviconPluginConfig\n /**\n * OG image template name — auto-resolves to the correct locale-specific\n * OG image path generated by `ogImagePlugin`.\n * Sets both `og:image` and `twitter:image` unless `image` is also provided.\n */\n ogTemplate?: string\n /** Output directory for OG images. Default: \"og\" */\n ogImageDir?: string\n /** OG image format. Default: \"png\" */\n ogImageFormat?: 'png' | 'jpeg'\n children?: VNodeChild\n}\n\nconst resolveStr = (v: string | (() => string) | undefined): string | undefined =>\n typeof v === 'function' ? v() : v\n\n/**\n * Declarative meta component for SSR-compatible page metadata.\n *\n * Supports reactive title/description — when passed as `() => string` accessors,\n * they are forwarded to `useHead()` as a reactive getter so updates propagate\n * automatically via signal tracking.\n *\n * @example\n * ```tsx\n * <Meta title=\"My Page\" description=\"...\" image=\"/og.jpg\" canonical=\"https://...\" />\n * ```\n *\n * @example Reactive title\n * ```tsx\n * const count = signal(0)\n * <Meta title={() => `${count()} items`} />\n * ```\n */\nexport function Meta(props: MetaProps): VNodeChild {\n const hasReactiveTitle = typeof props.title === 'function'\n const hasReactiveDescription = typeof props.description === 'function'\n\n // If title or description are reactive accessors, pass a getter to useHead\n // so it re-evaluates when the signals change.\n if (hasReactiveTitle || hasReactiveDescription) {\n useHead((): UseHeadInput => {\n const title = resolveStr(props.title)\n const description = resolveStr(props.description)\n const resolved = { ...props, title, description } as Parameters<typeof buildMetaTags>[0]\n const tags = buildMetaTags(resolved)\n const input: UseHeadInput = { meta: tags.meta, link: tags.link, script: tags.script }\n if (title) input.title = title\n return input\n })\n } else {\n const title = resolveStr(props.title)\n const description = resolveStr(props.description)\n const resolved = { ...props, title, description } as Parameters<typeof buildMetaTags>[0]\n const tags = buildMetaTags(resolved)\n const input: UseHeadInput = { meta: tags.meta, link: tags.link, script: tags.script }\n if (title) input.title = title\n useHead(input)\n }\n\n return props.children ?? null\n}\n\ninterface MetaTagEntry {\n name?: string\n property?: string\n content: string\n [key: string]: string | undefined\n}\n\ninterface LinkTagEntry {\n rel: string\n href?: string\n hreflang?: string\n type?: string\n sizes?: string\n [key: string]: string | undefined\n}\n\ninterface ScriptTagEntry {\n type: string\n children: string\n}\n\ninterface MetaTags {\n meta: MetaTagEntry[]\n link: LinkTagEntry[]\n script: ScriptTagEntry[]\n}\n\nexport function buildMetaTags(\n props: Omit<MetaProps, 'title' | 'description' | 'children'> & {\n title?: string\n description?: string\n },\n): MetaTags {\n const meta: MetaTagEntry[] = []\n const link: LinkTagEntry[] = []\n const script: ScriptTagEntry[] = []\n\n const {\n title, description, canonical, imageAlt, imageWidth, imageHeight,\n type = 'website', siteName,\n twitterCard = 'summary_large_image', twitterSite, twitterCreator,\n locale = 'en_US', alternateLocales,\n publishedTime, modifiedTime, author, tags, jsonLd, extra,\n video, videoWidth, videoHeight, audio,\n favicon, ogTemplate, ogImageDir, ogImageFormat,\n } = props\n\n // noIndex convenience overrides robots\n const robots = props.noIndex ? 'noindex, nofollow' : (props.robots ?? 'index, follow')\n\n // Resolve image: explicit `image` prop takes precedence over `ogTemplate`\n const image = props.image ?? (\n ogTemplate\n ? ogImagePath(ogTemplate, locale !== 'en_US' ? locale : undefined, ogImageDir, ogImageFormat)\n : undefined\n )\n\n // Auto-resolve image dimensions for OG template images\n const resolvedImageWidth = imageWidth ?? (ogTemplate && !props.image ? 1200 : undefined)\n const resolvedImageHeight = imageHeight ?? (ogTemplate && !props.image ? 630 : undefined)\n\n if (description) meta.push({ name: 'description', content: description })\n if (robots) meta.push({ name: 'robots', content: robots })\n if (author) meta.push({ name: 'author', content: author })\n\n if (title) meta.push({ property: 'og:title', content: title })\n if (description) meta.push({ property: 'og:description', content: description })\n if (canonical) meta.push({ property: 'og:url', content: canonical })\n if (image) meta.push({ property: 'og:image', content: image })\n if (imageAlt) meta.push({ property: 'og:image:alt', content: imageAlt })\n if (resolvedImageWidth) meta.push({ property: 'og:image:width', content: String(resolvedImageWidth) })\n if (resolvedImageHeight) meta.push({ property: 'og:image:height', content: String(resolvedImageHeight) })\n meta.push({ property: 'og:type', content: type })\n if (siteName) meta.push({ property: 'og:site_name', content: siteName })\n meta.push({ property: 'og:locale', content: locale })\n\n // Video\n if (video) {\n meta.push({ property: 'og:video', content: video })\n if (videoWidth) meta.push({ property: 'og:video:width', content: String(videoWidth) })\n if (videoHeight) meta.push({ property: 'og:video:height', content: String(videoHeight) })\n // Auto-detect video type from extension\n if (video.endsWith('.mp4')) meta.push({ property: 'og:video:type', content: 'video/mp4' })\n else if (video.endsWith('.webm')) meta.push({ property: 'og:video:type', content: 'video/webm' })\n }\n\n // Audio\n if (audio) {\n meta.push({ property: 'og:audio', content: audio })\n }\n\n if (type === 'article') {\n if (publishedTime) meta.push({ property: 'article:published_time', content: publishedTime })\n if (modifiedTime) meta.push({ property: 'article:modified_time', content: modifiedTime })\n if (author) meta.push({ property: 'article:author', content: author })\n if (tags) for (const tag of tags) meta.push({ property: 'article:tag', content: tag })\n }\n\n meta.push({ name: 'twitter:card', content: twitterCard })\n if (title) meta.push({ name: 'twitter:title', content: title })\n if (description) meta.push({ name: 'twitter:description', content: description })\n if (image) meta.push({ name: 'twitter:image', content: image })\n if (imageAlt) meta.push({ name: 'twitter:image:alt', content: imageAlt })\n if (twitterSite) meta.push({ name: 'twitter:site', content: twitterSite })\n if (twitterCreator) meta.push({ name: 'twitter:creator', content: twitterCreator })\n\n if (canonical) link.push({ rel: 'canonical', href: canonical })\n if (alternateLocales) {\n for (const alt of alternateLocales) {\n link.push({ rel: 'alternate', hreflang: alt.locale, href: alt.url })\n }\n }\n\n if (jsonLd) {\n script.push({\n type: 'application/ld+json',\n children: JSON.stringify({ '@context': 'https://schema.org', ...jsonLd }),\n })\n }\n\n if (extra) for (const tag of extra) meta.push(tag)\n\n // I18n: auto-generate hreflang alternates from i18nRouting config\n if (props.i18n) {\n const i18nConfig = props.i18n\n const origin = props.origin ?? ''\n const currentPath = canonical?.replace(origin, '') ?? '/'\n const { pathWithoutLocale } = extractLocaleFromPath(\n currentPath,\n i18nConfig.locales,\n i18nConfig.defaultLocale,\n )\n const strategy = i18nConfig.strategy ?? 'prefix-except-default'\n\n for (const loc of i18nConfig.locales) {\n const localizedPath =\n strategy === 'prefix-except-default' && loc === i18nConfig.defaultLocale\n ? pathWithoutLocale\n : `/${loc}${pathWithoutLocale === '/' ? '' : pathWithoutLocale}`\n\n link.push({\n rel: 'alternate',\n hreflang: loc,\n href: `${origin}${localizedPath}`,\n })\n\n // og:locale:alternate for non-current locales\n if (loc !== locale) {\n meta.push({ property: 'og:locale:alternate', content: loc })\n }\n }\n\n // x-default hreflang pointing to default locale\n link.push({\n rel: 'alternate',\n hreflang: 'x-default',\n href: `${origin}${pathWithoutLocale}`,\n })\n }\n\n // Favicon: inject locale-aware favicon links\n if (favicon) {\n const faviconLocale = locale !== 'en_US' ? locale : undefined\n for (const fl of faviconLinks(faviconLocale, favicon)) {\n link.push(fl as LinkTagEntry)\n }\n // Theme color meta from favicon config\n if (favicon.themeColor) {\n meta.push({ name: 'theme-color', content: favicon.themeColor })\n }\n }\n\n return { meta, link, script }\n}\n","import type { VNodeChild } from '@pyreon/core'\nimport { onMount } from '@pyreon/core'\nimport { effect, signal } from '@pyreon/reactivity'\n\n// ─── Theme system ───────────────────────────────────────────────────────────\n//\n// Provides dark/light/system theme support with:\n// - System preference detection via matchMedia\n// - Persistent preference via localStorage\n// - No flash of wrong theme (inline script in HTML)\n// - Reactive theme signal for components\n\nexport type Theme = 'light' | 'dark' | 'system'\n\nconst STORAGE_KEY = 'zero-theme'\n\n/** Reactive theme signal. */\nexport const theme = signal<Theme>('system')\n\n/**\n * Reactive signal tracking the OS color-scheme preference. Updated by the\n * `matchMedia('(prefers-color-scheme: dark)').change` event registered in\n * `initTheme`. Components reading `resolvedTheme()` subscribe to BOTH\n * `theme` and this signal, so a user toggling dark mode at the OS level\n * re-renders everything reactively — not just the `<html data-theme>`\n * attribute.\n *\n * SSR default is `_ssrDefault` (mutable via `setSSRThemeDefault`) so the\n * server-rendered theme can differ from the client's OS preference.\n */\nconst _osPrefersDark = signal<boolean>(false)\n\n/** SSR fallback when system preference can't be detected. Default: 'light'. */\nlet _ssrDefault: 'light' | 'dark' = 'light'\n\n/**\n * Set the default theme for SSR (when `matchMedia` is unavailable).\n * Call once at server startup before rendering.\n */\nexport function setSSRThemeDefault(value: 'light' | 'dark'): void {\n _ssrDefault = value\n}\n\n/**\n * Reactive read of the resolved theme. Subscribes to `theme` (explicit\n * user choice) and — when `theme === 'system'` — to `_osPrefersDark`\n * (OS color-scheme preference). Components using `resolvedTheme()`\n * inside JSX / effects / computeds re-render when either changes.\n */\nexport function resolvedTheme(): 'light' | 'dark' {\n const t = theme()\n if (t === 'system') {\n if (typeof window === 'undefined') return _ssrDefault\n return _osPrefersDark() ? 'dark' : 'light'\n }\n return t\n}\n\n/** Toggle between light and dark. */\nexport function toggleTheme() {\n const current = resolvedTheme()\n setTheme(current === 'dark' ? 'light' : 'dark')\n}\n\n/** Set theme explicitly. */\nexport function setTheme(t: Theme) {\n theme.set(t)\n if (typeof document !== 'undefined') {\n document.documentElement.dataset.theme = resolvedTheme()\n try {\n localStorage.setItem(STORAGE_KEY, t)\n } catch {\n // localStorage may not be available (SSR, private browsing)\n }\n }\n}\n\n/**\n * Initialize the theme system. Call once in your app entry or layout.\n * Reads from localStorage, listens for system preference changes.\n */\nexport function initTheme() {\n onMount(() => {\n // Read persisted preference\n try {\n const stored = localStorage.getItem(STORAGE_KEY) as Theme | null\n if (stored === 'light' || stored === 'dark' || stored === 'system') {\n theme.set(stored)\n }\n } catch {\n // localStorage may not be available\n }\n\n // Apply to document\n document.documentElement.dataset.theme = resolvedTheme()\n\n // Watch for system preference changes. Seed the signal from the\n // current media-query state, then update reactively on each OS\n // preference flip. Components reading `resolvedTheme()` pick up the\n // change automatically (they subscribe to `_osPrefersDark` when\n // `theme === 'system'`).\n const mq = window.matchMedia('(prefers-color-scheme: dark)')\n _osPrefersDark.set(mq.matches)\n function onChange(e: MediaQueryListEvent) {\n _osPrefersDark.set(e.matches)\n }\n mq.addEventListener('change', onChange)\n\n // Re-apply when theme signal changes — updates data-theme + favicons\n const dispose = effect(() => {\n const mode = resolvedTheme()\n document.documentElement.dataset.theme = mode\n\n // Swap favicon variants (if dual-variant favicons are present)\n const faviconLinks = document.querySelectorAll<HTMLLinkElement>('[data-favicon-theme]')\n for (const link of faviconLinks) {\n link.media = link.dataset.faviconTheme === mode ? '' : 'not all'\n }\n })\n\n return () => {\n mq.removeEventListener('change', onChange)\n dispose?.dispose()\n }\n })\n}\n\n/**\n * Theme toggle button component.\n *\n * @example\n * import { ThemeToggle } from \"@pyreon/zero/theme\"\n * <ThemeToggle />\n */\nexport function ThemeToggle(props: { class?: string; style?: string }): VNodeChild {\n initTheme()\n\n return (\n <button\n class={props.class}\n style={props.style}\n onClick={toggleTheme}\n aria-label=\"Toggle theme\"\n title=\"Toggle theme\"\n type=\"button\"\n >\n {() =>\n resolvedTheme() === 'dark' ? (\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <circle cx=\"12\" cy=\"12\" r=\"5\" />\n <line x1=\"12\" y1=\"1\" x2=\"12\" y2=\"3\" />\n <line x1=\"12\" y1=\"21\" x2=\"12\" y2=\"23\" />\n <line x1=\"4.22\" y1=\"4.22\" x2=\"5.64\" y2=\"5.64\" />\n <line x1=\"18.36\" y1=\"18.36\" x2=\"19.78\" y2=\"19.78\" />\n <line x1=\"1\" y1=\"12\" x2=\"3\" y2=\"12\" />\n <line x1=\"21\" y1=\"12\" x2=\"23\" y2=\"12\" />\n <line x1=\"4.22\" y1=\"19.78\" x2=\"5.64\" y2=\"18.36\" />\n <line x1=\"18.36\" y1=\"5.64\" x2=\"19.78\" y2=\"4.22\" />\n </svg>\n ) : (\n <svg\n width=\"18\"\n height=\"18\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n stroke-width=\"2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n aria-hidden=\"true\"\n >\n <path d=\"M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z\" />\n </svg>\n )\n }\n </button>\n )\n}\n\n/**\n * Inline script to prevent flash of wrong theme.\n * Include this in your index.html <head> BEFORE any stylesheets.\n *\n * @example\n * // index.html\n * <head>\n * <script>{themeScript}</script>\n * ...\n * </head>\n */\nexport 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){}})()`\n","/**\n * @pyreon/zero — client-safe exports.\n *\n * This entry contains only browser-safe components and hooks.\n * No node:fs, node:path, or other server-only imports.\n *\n * For server/build-time features, use subpath imports:\n * import { faviconPlugin } from \"@pyreon/zero/favicon\"\n * import { createServer } from \"@pyreon/zero/server\"\n * import { defineConfig } from \"@pyreon/zero/config\"\n * import { validateEnv } from \"@pyreon/zero/env\"\n */\n\n// ─── Components (browser-safe) ──────────────────────────────────────────────\n\nexport type { ImageProps, ImageSource } from \"./image\";\nexport { Image } from \"./image\";\nexport type { LinkProps, LinkRenderProps, UseLinkReturn } from \"./link\";\nexport { createLink, Link, prefetchRoute, useLink } from \"./link\";\nexport type { ScriptProps, ScriptStrategy } from \"./script\";\nexport { Script } from \"./script\";\nexport type { MetaProps } from \"./meta\";\nexport { buildMetaTags, Meta } from \"./meta\";\n\n// ─── Theme (browser-safe) ───────────────────────────────────────────────────\n\nexport type { Theme } from \"./theme\";\nexport {\n\tinitTheme,\n\tresolvedTheme,\n\tsetSSRThemeDefault,\n\tsetTheme,\n\tThemeToggle,\n\ttheme,\n\tthemeScript,\n\ttoggleTheme,\n} from \"./theme\";\n\n// ─── I18n hooks (browser-safe) ──────────────────────────────────────────────\n\nexport type { I18nRoutingConfig, LocaleContext } from \"./i18n-routing\";\nexport {\n\tbuildLocalePath,\n\textractLocaleFromPath,\n\tsetLocale,\n\tuseLocale,\n} from \"./i18n-routing\";\n\n// ─── Server-only stubs ──────────────────────────────────────────────────────\n// Throw clear error messages when developers accidentally import server-only\n// APIs from the main entry. These are tree-shaken if not imported.\n\nfunction serverOnly(name: string, subpath: string): never {\n throw new Error(\n `[Pyreon] \"${name}\" is server-only and cannot be imported from \"@pyreon/zero\".\\n` +\n `Import from the subpath instead:\\n\\n` +\n ` import { ${name} } from \"@pyreon/zero/${subpath}\"\\n`,\n )\n}\n\n/* eslint-disable @typescript-eslint/no-unused-vars */\n/** @deprecated Import from `@pyreon/zero/favicon` instead */\nexport function faviconPlugin(..._: unknown[]): never { return serverOnly('faviconPlugin', 'favicon') }\n/** @deprecated Import from `@pyreon/zero/seo` instead */\nexport function seoPlugin(..._: unknown[]): never { return serverOnly('seoPlugin', 'seo') }\n/** @deprecated Import from `@pyreon/zero/server` instead */\nexport function createServer(..._: unknown[]): never { return serverOnly('createServer', 'server') }\n/** @deprecated Import from `@pyreon/zero/config` instead */\nexport function defineConfig(..._: unknown[]): never { return serverOnly('defineConfig', 'config') }\n/** @deprecated Import from `@pyreon/zero/env` instead */\nexport function validateEnv(..._: unknown[]): never { return serverOnly('validateEnv', 'env') }\n/** @deprecated Import from `@pyreon/zero/og-image` instead */\nexport function ogImagePlugin(..._: unknown[]): never { return serverOnly('ogImagePlugin', 'og-image') }\n/** @deprecated Import from `@pyreon/zero/ai` instead */\nexport function aiPlugin(..._: unknown[]): never { return serverOnly('aiPlugin', 'ai') }\n\n// ─── Types (no runtime, safe everywhere) ────────────────────────────────────\n\nexport type {\n\tAdapter,\n\tAdapterBuildOptions,\n\tFileRoute,\n\tISRConfig,\n\tLoaderContext,\n\tRenderMode,\n\tRouteMeta,\n\tRouteMiddlewareEntry,\n\tRouteModule,\n\tZeroConfig,\n} from \"./types\";\n"],"mappings":"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"}
|
package/lib/link.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"link.js","names":[],"sources":["../src/utils/use-intersection-observer.ts","../../../core/core/lib/jsx-runtime.js","../src/link.tsx"],"sourcesContent":["import { onMount, onUnmount } from '@pyreon/core'\n\n/**\n * Observes an element and calls `onIntersect` once it enters the viewport.\n * Automatically disconnects after the first intersection.\n *\n * @param getElement - Getter for the target element (may be undefined before mount).\n * @param onIntersect - Callback fired when the element becomes visible.\n * @param rootMargin - IntersectionObserver rootMargin. Default: \"200px\".\n */\nexport function useIntersectionObserver(\n getElement: () => HTMLElement | undefined,\n onIntersect: () => void,\n rootMargin = '200px',\n) {\n onMount(() => {\n const el = getElement()\n if (!el) return undefined\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n onIntersect()\n observer.disconnect()\n }\n }\n },\n { rootMargin },\n )\n\n observer.observe(el)\n onUnmount(() => observer.disconnect())\n return undefined\n })\n}\n","//#region src/h.ts\n/** Marker for fragment nodes — renders children without a wrapper element */\nconst Fragment = Symbol(\"Pyreon.Fragment\");\n/**\n* Hyperscript function — the compiled output of JSX.\n* `<div class=\"x\">hello</div>` → `h(\"div\", { class: \"x\" }, \"hello\")`\n*\n* Generic on P so TypeScript validates props match the component's signature\n* at the call site, then stores the result in the loosely-typed VNode.\n*/\n/** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */\nconst EMPTY_PROPS = {};\nfunction h(type, props, ...children) {\n\treturn {\n\t\ttype,\n\t\tprops: props ?? EMPTY_PROPS,\n\t\tchildren: normalizeChildren(children),\n\t\tkey: props?.key ?? null\n\t};\n}\nfunction normalizeChildren(children) {\n\tfor (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);\n\treturn children;\n}\nfunction flattenChildren(children) {\n\tconst result = [];\n\tfor (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));\n\telse result.push(child);\n\treturn result;\n}\n\n//#endregion\n//#region src/jsx-runtime.ts\n/**\n* JSX automatic runtime.\n*\n* When tsconfig has `\"jsxImportSource\": \"@pyreon/core\"`, the TS/bundler compiler\n* rewrites JSX to imports from this file automatically:\n* <div class=\"x\" /> → jsx(\"div\", { class: \"x\" })\n*/\nfunction jsx(type, props, key) {\n\tconst { children, ...rest } = props;\n\tconst propsWithKey = key != null ? {\n\t\t...rest,\n\t\tkey\n\t} : rest;\n\tif (typeof type === \"function\") return h(type, children !== void 0 ? {\n\t\t...propsWithKey,\n\t\tchildren\n\t} : propsWithKey);\n\treturn h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);\n}\nconst jsxs = jsx;\n\n//#endregion\nexport { Fragment, jsx, jsxs };\n//# sourceMappingURL=jsx-runtime.js.map","import { createRef } from '@pyreon/core'\nimport { useRouter } from '@pyreon/router'\nimport { useIntersectionObserver } from './utils/use-intersection-observer'\n\n// ─── Link component with prefetching ────────────────────────────────────────\n//\n// Provides client-side navigation, prefetching, and active state tracking.\n// Three levels of API:\n//\n// 1. useLink(props) — composable returning handlers, state, and ref callback\n// 2. createLink(Comp) — HOC wrapping any component with link behavior\n// 3. Link — default <a>-based link (built on createLink)\n\nexport interface LinkProps {\n /** Target URL path. */\n href: string\n /** Link content. */\n children?: any\n /** CSS class name. */\n class?: string\n /** Class applied when this link matches the current route. */\n activeClass?: string\n /** Class applied when this link exactly matches the current route. */\n exactActiveClass?: string\n /** Prefetch strategy. Default: \"hover\" */\n prefetch?: 'hover' | 'viewport' | 'none'\n /** Open in new tab. */\n external?: boolean\n /** Inline styles. */\n style?: string\n /** ARIA label. */\n 'aria-label'?: string\n /** Additional click handler — called before navigation. Call e.preventDefault() to cancel. */\n onClick?: ((e: MouseEvent) => void) | undefined\n}\n\n/** Props passed to a custom component via createLink. */\nexport interface LinkRenderProps {\n href: string\n ref: import('@pyreon/core').Ref<HTMLAnchorElement>\n onClick: (e: MouseEvent) => void\n onMouseEnter: () => void\n onTouchStart: () => void\n isActive: () => boolean\n isExactActive: () => boolean\n /** Reactive class string — pass directly to element for auto-updates on route change. */\n class: (() => string) | string | undefined\n style?: string\n target?: string\n rel?: string\n 'aria-label'?: string\n children?: any\n}\n\n/** Return type of useLink. */\nexport interface UseLinkReturn {\n /** Ref object — attach to the root element for viewport-based prefetch. */\n ref: import('@pyreon/core').Ref<HTMLAnchorElement>\n /** Click handler — performs client-side navigation. */\n handleClick: (e: MouseEvent) => void\n /** Mouse enter handler — triggers hover prefetch. */\n handleMouseEnter: () => void\n /** Touch start handler — triggers prefetch on mobile. */\n handleTouchStart: () => void\n /** Whether the link partially matches the current route. */\n isActive: () => boolean\n /** Whether the link exactly matches the current route. */\n isExactActive: () => boolean\n /** Resolved class string including active classes. */\n classes: () => string\n}\n\nconst MAX_PREFETCH_CACHE = 200\n// Maps href → list of <link> elements injected into <head>. When the\n// cache evicts an href (FIFO at MAX_PREFETCH_CACHE), the matching <link>\n// elements must be removed too — otherwise head bloats unboundedly\n// across long SPA sessions (every Link interaction added 2 <link> nodes\n// with no cleanup).\nconst prefetched = new Map<string, Element[]>()\n\nfunction doPrefetch(href: string) {\n // Prefetch only fires from browser-mounted Link interactions (hover /\n // click intent). Explicit guard documents the SSR-safety contract.\n if (typeof document === 'undefined') return\n if (prefetched.has(href)) return\n // Evict oldest entries when cache is full — AND remove their DOM nodes.\n if (prefetched.size >= MAX_PREFETCH_CACHE) {\n const firstEntry = prefetched.entries().next().value\n if (firstEntry) {\n const [oldestHref, oldestLinks] = firstEntry\n for (const link of oldestLinks) link.remove()\n prefetched.delete(oldestHref)\n }\n }\n\n const injected: Element[] = []\n const docLink = document.createElement('link')\n docLink.rel = 'prefetch'\n docLink.href = href\n docLink.as = 'document'\n document.head.appendChild(docLink)\n injected.push(docLink)\n\n try {\n const chunkHint = document.createElement('link')\n chunkHint.rel = 'modulepreload'\n chunkHint.href = href\n document.head.appendChild(chunkHint)\n injected.push(chunkHint)\n } catch {\n // modulepreload is a hint, not critical\n }\n\n prefetched.set(href, injected)\n}\n\n/**\n * Prefetch a route's JS chunk by injecting `<link rel=\"prefetch\">` into the\n * document head. Deduplicates — calling with the same href twice is a no-op.\n *\n * @example\n * prefetchRoute('/about')\n * prefetchRoute('/dashboard')\n */\nexport function prefetchRoute(href: string): void {\n doPrefetch(href)\n}\n\n/**\n * Composable that provides all link behavior — navigation, prefetching,\n * active state, and viewport observation.\n *\n * Use this for full control when `createLink` is too opinionated.\n *\n * @example\n * function MyLink(props: LinkProps) {\n * const link = useLink(props)\n * return (\n * <button ref={link.ref} class={link.classes()} onClick={link.handleClick}>\n * {props.children}\n * </button>\n * )\n * }\n */\nexport function useLink(props: LinkProps): UseLinkReturn {\n const router = useRouter()\n const elementRef = createRef<HTMLAnchorElement>()\n const strategy = props.prefetch ?? 'hover'\n\n function handleClick(e: MouseEvent) {\n // Call user's onClick first — they may call e.preventDefault()\n if (props.onClick) {\n ;(props.onClick as (e: MouseEvent) => void)(e)\n }\n\n if (\n e.defaultPrevented ||\n e.button !== 0 ||\n e.metaKey ||\n e.ctrlKey ||\n e.shiftKey ||\n e.altKey ||\n props.external ||\n !props.href\n ) {\n return\n }\n e.preventDefault()\n router.push(props.href)\n }\n\n function handleMouseEnter() {\n if (strategy === 'hover') {\n doPrefetch(props.href)\n }\n }\n\n function handleTouchStart() {\n if (strategy === 'hover' || strategy === 'viewport') {\n doPrefetch(props.href)\n }\n }\n\n if (strategy === 'viewport') {\n useIntersectionObserver(\n () => elementRef.current ?? undefined,\n () => doPrefetch(props.href),\n )\n }\n\n const isActive = () => {\n const currentPath = router.currentRoute()?.path\n if (!currentPath || !props.href) return false\n if (props.href === '/') return currentPath === '/'\n return currentPath.startsWith(props.href)\n }\n\n const isExactActive = () => {\n const currentPath = router.currentRoute()?.path\n if (!currentPath) return false\n return currentPath === props.href\n }\n\n const classes = () => {\n const cls: string[] = []\n if (props.class) cls.push(props.class)\n if (props.activeClass && isActive()) cls.push(props.activeClass)\n if (props.exactActiveClass && isExactActive()) cls.push(props.exactActiveClass)\n return cls.join(' ')\n }\n\n return {\n ref: elementRef,\n handleClick,\n handleMouseEnter,\n handleTouchStart,\n isActive,\n isExactActive,\n classes,\n }\n}\n\n/**\n * Higher-order component that wraps any component with link behavior.\n *\n * The wrapped component receives {@link LinkRenderProps} with all handlers,\n * active state, and accessibility attributes pre-wired.\n *\n * @example\n * // Custom button link\n * const ButtonLink = createLink((props) => (\n * <button\n * ref={props.ref}\n * class={props.class}\n * onClick={props.onClick}\n * onMouseEnter={props.onMouseEnter}\n * >\n * {props.children}\n * </button>\n * ))\n *\n * // Custom styled component\n * const CardLink = createLink((props) => (\n * <div\n * ref={props.ref}\n * class={`card ${props.isActive() ? \"card--active\" : \"\"}`}\n * onClick={props.onClick}\n * onMouseEnter={props.onMouseEnter}\n * >\n * {props.children}\n * </div>\n * ))\n *\n * // Usage\n * <ButtonLink href=\"/about\">About</ButtonLink>\n * <CardLink href=\"/posts\" prefetch=\"viewport\">Posts</CardLink>\n */\nexport function createLink(Component: (props: LinkRenderProps) => any): (props: LinkProps) => any {\n return function WrappedLink(props: LinkProps) {\n const link = useLink(props)\n\n return (\n <Component\n href={props.href}\n ref={link.ref}\n onClick={link.handleClick}\n onMouseEnter={link.handleMouseEnter}\n onTouchStart={link.handleTouchStart}\n isActive={link.isActive}\n isExactActive={link.isExactActive}\n class={link.classes}\n {...(props.style ? { style: props.style } : {})}\n {...(props.external ? { target: '_blank', rel: 'noopener noreferrer' } : {})}\n {...(props['aria-label'] ? { 'aria-label': props['aria-label'] } : {})}\n children={props.children}\n />\n )\n }\n}\n\n/**\n * Default navigation link built on an `<a>` tag.\n *\n * @example\n * <Link href=\"/about\" prefetch=\"viewport\">About</Link>\n * <Link href=\"/posts\" activeClass=\"nav-active\">Posts</Link>\n */\nexport const Link = createLink((props: LinkRenderProps) => (\n <a\n ref={props.ref}\n href={props.href}\n {...(props.class ? { class: props.class } : {})}\n {...(props.style ? { style: props.style } : {})}\n {...(props.target ? { target: props.target } : {})}\n {...(props.rel ? { rel: props.rel } : {})}\n {...(props['aria-label'] ? { 'aria-label': props['aria-label'] } : {})}\n {...(props.isExactActive() ? { 'aria-current': 'page' as const } : {})}\n onClick={props.onClick}\n onMouseEnter={props.onMouseEnter}\n onTouchStart={props.onTouchStart}\n >\n {props.children}\n </a>\n))\n"],"mappings":"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"}
|
package/lib/logger.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"logger.js","names":[],"sources":["../src/logger.ts"],"sourcesContent":["/**\n * Request logging middleware.\n *\n * Logs HTTP requests with method, path, status, and duration.\n * Supports custom formatters and log levels.\n *\n * @example\n * ```ts\n * import { loggerMiddleware } from \"@pyreon/zero\"\n *\n * export default defineConfig({\n * middleware: [loggerMiddleware()],\n * })\n * ```\n */\nimport type { Middleware, MiddlewareContext } from '@pyreon/server'\n\nexport interface LoggerConfig {\n /**\n * Log level — controls which requests are logged.\n * - \"all\": log every request\n * - \"none\": disable logging\n * Default: \"all\"\n */\n level?: 'all' | 'none'\n /**\n * Custom log formatter. Receives request details and returns\n * the string to log (or null to skip).\n */\n format?: (entry: LogEntry) => string | null\n /**\n * Skip logging for these path prefixes.\n * Default: [\"/__\", \"/@\", \"/node_modules\"]\n */\n skip?: string[]\n /**\n * Enable colorized output (ANSI codes).\n * Default: true in development, false in production.\n */\n colors?: boolean\n}\n\nexport interface LogEntry {\n method: string\n path: string\n duration: number\n timestamp: Date\n userAgent?: string | undefined\n ip?: string | undefined\n}\n\nconst COLORS = {\n reset: '\\x1b[0m',\n dim: '\\x1b[2m',\n green: '\\x1b[32m',\n yellow: '\\x1b[33m',\n red: '\\x1b[31m',\n cyan: '\\x1b[36m',\n magenta: '\\x1b[35m',\n}\n\nfunction methodColor(method: string, colors: boolean): string {\n if (!colors) return method.padEnd(7)\n const padded = method.padEnd(7)\n switch (method) {\n case 'GET': return `${COLORS.green}${padded}${COLORS.reset}`\n case 'POST': return `${COLORS.cyan}${padded}${COLORS.reset}`\n case 'PUT': return `${COLORS.yellow}${padded}${COLORS.reset}`\n case 'PATCH': return `${COLORS.yellow}${padded}${COLORS.reset}`\n case 'DELETE': return `${COLORS.red}${padded}${COLORS.reset}`\n default: return `${COLORS.magenta}${padded}${COLORS.reset}`\n }\n}\n\nfunction defaultFormat(entry: LogEntry, colors: boolean): string {\n const dur = entry.duration < 1\n ? '<1ms'\n : entry.duration < 1000\n ? `${Math.round(entry.duration)}ms`\n : `${(entry.duration / 1000).toFixed(2)}s`\n\n const dim = colors ? COLORS.dim : ''\n const reset = colors ? COLORS.reset : ''\n\n return ` ${methodColor(entry.method, colors)} ${entry.path} ${dim}${dur}${reset}`\n}\n\n/**\n * Request logging middleware.\n *\n * Logs incoming requests with method, path, and duration.\n * Runs in middleware phase — logs timing from middleware start to\n * microtask completion (approximate request duration).\n *\n * @example\n * ```ts\n * // Basic usage\n * loggerMiddleware()\n *\n * // Custom format\n * loggerMiddleware({\n * format: (e) => `${e.method} ${e.path} (${e.duration}ms)`,\n * })\n * ```\n */\nexport function loggerMiddleware(config?: LoggerConfig): Middleware {\n const level = config?.level ?? 'all'\n if (level === 'none') return () => {}\n\n const skip = config?.skip ?? ['/__', '/@', '/node_modules']\n const isDev = typeof process !== 'undefined' && process.env.NODE_ENV !== 'production'\n const colors = config?.colors ?? isDev\n\n return (ctx: MiddlewareContext) => {\n // Skip internal paths\n if (skip.some((p) => ctx.path.startsWith(p))) return\n\n const start = performance.now()\n\n const entry: LogEntry = {\n method: ctx.req.method ?? 'GET',\n path: ctx.path,\n duration: 0,\n timestamp: new Date(),\n userAgent: ctx.req.headers.get('user-agent') ?? undefined,\n }\n\n // Use queueMicrotask to log after the middleware chain completes\n queueMicrotask(() => {\n entry.duration = performance.now() - start\n\n if (config?.format) {\n const line = config.format(entry)\n if (line) {\n // oxlint-disable-next-line no-console\n console.log(line)\n }\n } else {\n // oxlint-disable-next-line no-console\n console.log(defaultFormat(entry, colors))\n }\n })\n }\n}\n"],"mappings":";AAmDA,MAAM,SAAS;CACb,OAAO;CACP,KAAK;CACL,OAAO;CACP,QAAQ;CACR,KAAK;CACL,MAAM;CACN,SAAS;CACV;AAED,SAAS,YAAY,QAAgB,QAAyB;AAC5D,KAAI,CAAC,OAAQ,QAAO,OAAO,OAAO,EAAE;CACpC,MAAM,SAAS,OAAO,OAAO,EAAE;AAC/B,SAAQ,QAAR;EACE,KAAK,MAAO,QAAO,GAAG,OAAO,QAAQ,SAAS,OAAO;EACrD,KAAK,OAAQ,QAAO,GAAG,OAAO,OAAO,SAAS,OAAO;EACrD,KAAK,MAAO,QAAO,GAAG,OAAO,SAAS,SAAS,OAAO;EACtD,KAAK,QAAS,QAAO,GAAG,OAAO,SAAS,SAAS,OAAO;EACxD,KAAK,SAAU,QAAO,GAAG,OAAO,MAAM,SAAS,OAAO;EACtD,QAAS,QAAO,GAAG,OAAO,UAAU,SAAS,OAAO;;;AAIxD,SAAS,cAAc,OAAiB,QAAyB;CAC/D,MAAM,MAAM,MAAM,WAAW,IACzB,SACA,MAAM,WAAW,MACf,GAAG,KAAK,MAAM,MAAM,SAAS,CAAC,MAC9B,IAAI,MAAM,WAAW,KAAM,QAAQ,EAAE,CAAC;CAE5C,MAAM,MAAM,SAAS,OAAO,MAAM;CAClC,MAAM,QAAQ,SAAS,OAAO,QAAQ;AAEtC,QAAO,KAAK,YAAY,MAAM,QAAQ,OAAO,CAAC,GAAG,MAAM,KAAK,GAAG,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;AAqB7E,SAAgB,iBAAiB,QAAmC;AAElE,MADc,QAAQ,SAAS,WACjB,OAAQ,cAAa;CAEnC,MAAM,OAAO,QAAQ,QAAQ;EAAC;EAAO;EAAM;EAAgB;CAC3D,MAAM,QAAQ,OAAO,YAAY,eAAe,QAAQ,IAAI,aAAa;CACzE,MAAM,SAAS,QAAQ,UAAU;AAEjC,SAAQ,QAA2B;AAEjC,MAAI,KAAK,MAAM,MAAM,IAAI,KAAK,WAAW,EAAE,CAAC,CAAE;EAE9C,MAAM,QAAQ,YAAY,KAAK;EAE/B,MAAM,QAAkB;GACtB,QAAQ,IAAI,IAAI,UAAU;GAC1B,MAAM,IAAI;GACV,UAAU;GACV,2BAAW,IAAI,MAAM;GACrB,WAAW,IAAI,IAAI,QAAQ,IAAI,aAAa,IAAI;GACjD;AAGD,uBAAqB;AACnB,SAAM,WAAW,YAAY,KAAK,GAAG;AAErC,OAAI,QAAQ,QAAQ;IAClB,MAAM,OAAO,OAAO,OAAO,MAAM;AACjC,QAAI,KAEF,SAAQ,IAAI,KAAK;SAInB,SAAQ,IAAI,cAAc,OAAO,OAAO,CAAC;IAE3C"}
|
package/lib/meta.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"meta.js","names":[],"sources":["../src/i18n-routing.ts","../src/meta.tsx"],"sourcesContent":["import { createContext } from '@pyreon/core'\nimport { signal } from '@pyreon/reactivity'\nimport type { Plugin } from 'vite'\n\n// ─── Localized routing ─────────────────────────────────────────────────────\n//\n// Adds locale-prefixed routes to Zero's file-system router:\n// - /about → /en/about, /de/about, /cs/about\n// - / → /en, /de, /cs (or default locale without prefix)\n// - Automatic locale detection from Accept-Language header\n// - Redirect to preferred locale\n// - hreflang link generation\n//\n// Usage:\n// import { i18nRouting } from \"@pyreon/zero\"\n// export default { plugins: [Pyreon], defaultLocale: \"en\" })] }\n\nexport interface I18nRoutingConfig {\n /** Supported locales. e.g. [\"en\", \"de\", \"cs\"] */\n locales: string[]\n /** Default locale — served without prefix (/ instead of /en/). */\n defaultLocale: string\n /** Redirect root to detected locale. Default: true */\n detectLocale?: boolean\n /** Cookie name to persist locale preference. Default: \"locale\" */\n cookieName?: string\n /** URL strategy. Default: \"prefix-except-default\" */\n strategy?: 'prefix' | 'prefix-except-default'\n}\n\nexport interface LocaleContext {\n /** Current locale code. e.g. \"en\", \"de\" */\n locale: string\n /** All supported locales. */\n locales: string[]\n /** Default locale. */\n defaultLocale: string\n /** Build a localized path. e.g. localePath(\"/about\", \"de\") → \"/de/about\" */\n localePath: (path: string, locale?: string) => string\n /** Get hreflang alternates for the current path. */\n alternates: () => Array<{ locale: string; url: string }>\n}\n\n/**\n * Detect preferred locale from Accept-Language header.\n */\nexport function detectLocaleFromHeader(\n acceptLanguage: string | null | undefined,\n locales: string[],\n defaultLocale: string,\n): string {\n if (!acceptLanguage) return defaultLocale\n\n // Parse Accept-Language: en-US,en;q=0.9,de;q=0.8\n const preferred = acceptLanguage\n .split(',')\n .map((part) => {\n const [lang, q] = part.trim().split(';q=')\n return {\n lang: lang?.split('-')[0]?.toLowerCase() ?? '',\n quality: q ? Number.parseFloat(q) : 1,\n }\n })\n .sort((a, b) => b.quality - a.quality)\n\n for (const { lang } of preferred) {\n if (locales.includes(lang)) return lang\n }\n\n return defaultLocale\n}\n\n/**\n * Extract locale from a URL path.\n * Returns { locale, pathWithoutLocale }.\n */\nexport function extractLocaleFromPath(\n path: string,\n locales: string[],\n defaultLocale: string,\n): { locale: string; pathWithoutLocale: string } {\n const segments = path.split('/').filter(Boolean)\n const firstSegment = segments[0]?.toLowerCase()\n\n if (firstSegment && locales.includes(firstSegment)) {\n return {\n locale: firstSegment,\n pathWithoutLocale: '/' + segments.slice(1).join('/') || '/',\n }\n }\n\n return { locale: defaultLocale, pathWithoutLocale: path }\n}\n\n/**\n * Build a localized path.\n */\nexport function buildLocalePath(\n path: string,\n locale: string,\n defaultLocale: string,\n strategy: 'prefix' | 'prefix-except-default',\n): string {\n const clean = path === '/' ? '' : path\n if (strategy === 'prefix-except-default' && locale === defaultLocale) {\n return path\n }\n return `/${locale}${clean}`\n}\n\n/**\n * Create a LocaleContext for use in components and loaders.\n */\nexport function createLocaleContext(\n locale: string,\n path: string,\n config: I18nRoutingConfig,\n): LocaleContext {\n const strategy = config.strategy ?? 'prefix-except-default'\n\n return {\n locale,\n locales: config.locales,\n defaultLocale: config.defaultLocale,\n\n localePath(targetPath: string, targetLocale?: string) {\n return buildLocalePath(\n targetPath,\n targetLocale ?? locale,\n config.defaultLocale,\n strategy,\n )\n },\n\n alternates() {\n const { pathWithoutLocale } = extractLocaleFromPath(\n path,\n config.locales,\n config.defaultLocale,\n )\n return config.locales.map((loc) => ({\n locale: loc,\n url: buildLocalePath(pathWithoutLocale, loc, config.defaultLocale, strategy),\n }))\n },\n }\n}\n\n/**\n * I18n routing middleware for Zero's server.\n *\n * - Detects locale from URL prefix or Accept-Language header\n * - Redirects root to preferred locale (when detectLocale is true)\n * - Sets locale context for loaders and components\n *\n * @example\n * ```ts\n * // zero.config.ts\n * import { i18nRouting } from \"@pyreon/zero\"\n *\n * export default defineConfig({\n * plugins: [\n * i18nRouting({\n * locales: [\"en\", \"de\", \"cs\"],\n * defaultLocale: \"en\",\n * }),\n * ],\n * })\n * ```\n */\nexport function i18nRouting(config: I18nRoutingConfig): Plugin {\n const strategy = config.strategy ?? 'prefix-except-default'\n const detectEnabled = config.detectLocale !== false\n const cookieName = config.cookieName ?? 'locale'\n\n return {\n name: 'pyreon-zero-i18n-routing',\n\n // Route duplication is NOT handled here. The fs-router's `scanRouteFiles`\n // consumes the i18n config to duplicate routes per locale at build time.\n // This plugin only provides: (1) the server middleware for locale detection\n // and (2) the runtime hooks (useLocale, setLocale) for client-side use.\n configResolved() {},\n\n configureServer(server) {\n server.middlewares.use((req, res, next) => {\n const url = req.url ?? '/'\n\n // Skip static assets\n if (url.startsWith('/@') || url.startsWith('/__') || url.includes('.')) {\n return next()\n }\n\n const { locale } = extractLocaleFromPath(\n url,\n config.locales,\n config.defaultLocale,\n )\n\n // Redirect root to detected locale\n if (detectEnabled && url === '/') {\n const cookies = parseCookies(req.headers.cookie)\n const preferredFromCookie = cookies[cookieName]\n const preferredFromHeader = detectLocaleFromHeader(\n req.headers['accept-language'],\n config.locales,\n config.defaultLocale,\n )\n const preferred = preferredFromCookie && config.locales.includes(preferredFromCookie)\n ? preferredFromCookie\n : preferredFromHeader\n\n if (strategy === 'prefix' || preferred !== config.defaultLocale) {\n res.writeHead(302, { Location: `/${preferred}/` })\n res.end()\n return\n }\n }\n\n // Attach locale context to request for loaders\n ;(req as any).__locale = locale\n ;(req as any).__localeContext = createLocaleContext(locale, url, config)\n\n // Update the module-level signal so useLocale() returns the correct value\n localeSignal.set(locale)\n\n next()\n })\n },\n }\n}\n\nfunction parseCookies(header: string | undefined): Record<string, string> {\n if (!header) return {}\n const result: Record<string, string> = {}\n for (const pair of header.split(';')) {\n const [key, value] = pair.trim().split('=')\n if (key && value) result[key] = decodeURIComponent(value)\n }\n return result\n}\n\n// ─── Reactive locale hook ───────────────────────────────────────────────────\n\n/** @internal Context for the current locale. */\nexport const LocaleCtx = createContext<string>('en')\n\n/** Current locale signal — set by the server middleware or client-side detection. */\nexport const localeSignal = signal('en')\n\n/**\n * Read the current locale reactively.\n *\n * Returns the locale signal value directly — reactive in both SSR and CSR.\n * The server middleware sets `localeSignal` per-request, and client-side\n * `setLocale()` updates it as well.\n *\n * @example\n * ```tsx\n * const locale = useLocale() // \"en\", \"de\", etc.\n * ```\n */\nexport function useLocale(): string {\n return localeSignal()\n}\n\n/**\n * Set the locale client-side and update the URL.\n *\n * @example\n * ```tsx\n * <button onClick={() => setLocale('de')}>Deutsch</button>\n * ```\n */\nexport function setLocale(\n locale: string,\n config: I18nRoutingConfig,\n): void {\n localeSignal.set(locale)\n\n // Persist to cookie\n if (typeof document !== 'undefined') {\n document.cookie = `${config.cookieName ?? 'locale'}=${locale}; path=/; max-age=31536000`\n }\n\n // Navigate to localized URL — use pushState to avoid full page reload\n if (typeof window !== 'undefined') {\n const strategy = config.strategy ?? 'prefix-except-default'\n const { pathWithoutLocale } = extractLocaleFromPath(\n window.location.pathname,\n config.locales,\n config.defaultLocale,\n )\n const newPath = buildLocalePath(pathWithoutLocale, locale, config.defaultLocale, strategy)\n window.history.pushState(null, '', newPath)\n // Dispatch popstate so @pyreon/router picks up the URL change\n window.dispatchEvent(new PopStateEvent('popstate'))\n }\n}\n","import type { VNodeChild } from '@pyreon/core'\nimport type { UseHeadInput } from '@pyreon/head'\nimport { useHead } from '@pyreon/head'\nimport type { I18nRoutingConfig } from './i18n-routing'\nimport { extractLocaleFromPath } from './i18n-routing'\n\n// ─── Inline helpers (no node:fs dependency) ─────────────────────────────────\n// These are inlined to avoid importing from favicon.ts/og-image.ts which\n// pull in node:fs at the top level — making Meta unsafe for client bundles.\n\n/** Favicon plugin config shape (type-only). */\ninterface FaviconPluginConfig {\n source: string\n themeColor?: string\n manifest?: boolean\n locales?: Record<string, { source: string; darkSource?: string }>\n [key: string]: unknown\n}\n\nfunction faviconLinks(\n locale: string | undefined,\n config: FaviconPluginConfig,\n): Array<{ rel: string; type?: string; sizes?: string; href: string }> {\n const hasLocaleOverride = locale && config.locales?.[locale]\n const prefix = hasLocaleOverride ? `/${locale}` : ''\n const isSvg = (hasLocaleOverride ? config.locales![locale]!.source : config.source).endsWith('.svg')\n const links: Array<{ rel: string; type?: string; sizes?: string; href: string }> = []\n if (isSvg) links.push({ rel: 'icon', type: 'image/svg+xml', href: `${prefix}/favicon.svg` })\n links.push(\n { rel: 'icon', type: 'image/png', sizes: '32x32', href: `${prefix}/favicon-32x32.png` },\n { rel: 'icon', type: 'image/png', sizes: '16x16', href: `${prefix}/favicon-16x16.png` },\n { rel: 'apple-touch-icon', sizes: '180x180', href: `${prefix}/apple-touch-icon.png` },\n )\n if (config.manifest !== false) links.push({ rel: 'manifest', href: `${prefix}/site.webmanifest` })\n return links\n}\n\nfunction ogImagePath(templateName: string, locale?: string, outDir = 'og', format: 'png' | 'jpeg' = 'png'): string {\n const ext = format === 'jpeg' ? 'jpg' : 'png'\n const suffix = locale ? `-${locale}` : ''\n return `/${outDir}/${templateName}${suffix}.${ext}`\n}\n\n// ─── Meta component ────────────────────────────────────────────────────────\n\nexport interface MetaProps {\n /** Page title. Accepts reactive accessor `() => string`. */\n title?: string | (() => string)\n /** Page description. Accepts reactive accessor. */\n description?: string | (() => string)\n /** Canonical URL. Also sets og:url. */\n canonical?: string\n /** Open Graph image URL. Also sets twitter:image. */\n image?: string\n /** Image alt text for accessibility. */\n imageAlt?: string\n /** Image width in pixels (og:image:width). Helps crawlers layout before loading. */\n imageWidth?: number\n /** Image height in pixels (og:image:height). */\n imageHeight?: number\n /** Open Graph type. Default: \"website\" */\n type?: 'website' | 'article' | 'product' | 'profile'\n /** Site name for og:site_name. */\n siteName?: string\n /** Twitter card type. Default: \"summary_large_image\" */\n twitterCard?: 'summary' | 'summary_large_image' | 'app' | 'player'\n /** Twitter @handle. */\n twitterSite?: string\n /** Twitter creator @handle. */\n twitterCreator?: string\n /** Locale. Default: \"en_US\" */\n locale?: string\n /** Alternate locales for hreflang. */\n alternateLocales?: Array<{ locale: string; url: string }>\n /** Robots directives. Default: \"index, follow\" */\n robots?: string\n /** Convenience: set `true` to emit `noindex, nofollow`. Overrides `robots`. */\n noIndex?: boolean\n /** Published time (ISO 8601) for article type. */\n publishedTime?: string\n /** Modified time (ISO 8601) for article type. */\n modifiedTime?: string\n /** Article author. */\n author?: string\n /** Article tags. */\n tags?: string[]\n /** JSON-LD structured data object. */\n jsonLd?: Record<string, unknown>\n /** Additional custom meta tags. */\n extra?: Array<{ name?: string; property?: string; content: string }>\n /**\n * Open Graph video URL. Also sets og:video:type if the URL ends with\n * a known extension (.mp4, .webm).\n */\n video?: string\n /** Video width in pixels. */\n videoWidth?: number\n /** Video height in pixels. */\n videoHeight?: number\n /**\n * Open Graph audio URL.\n */\n audio?: string\n /**\n * I18n routing config — when provided, auto-generates hreflang alternate\n * links for all locales based on the current path.\n * Also sets og:locale and og:locale:alternate.\n */\n i18n?: I18nRoutingConfig\n /** Base URL for building absolute hreflang URLs. e.g. \"https://example.com\" */\n origin?: string\n /**\n * Favicon plugin config — when provided, injects locale-aware favicon\n * `<link>` tags into `<head>`. Uses the current locale to select\n * the correct favicon set.\n */\n favicon?: FaviconPluginConfig\n /**\n * OG image template name — auto-resolves to the correct locale-specific\n * OG image path generated by `ogImagePlugin`.\n * Sets both `og:image` and `twitter:image` unless `image` is also provided.\n */\n ogTemplate?: string\n /** Output directory for OG images. Default: \"og\" */\n ogImageDir?: string\n /** OG image format. Default: \"png\" */\n ogImageFormat?: 'png' | 'jpeg'\n children?: VNodeChild\n}\n\nconst resolveStr = (v: string | (() => string) | undefined): string | undefined =>\n typeof v === 'function' ? v() : v\n\n/**\n * Declarative meta component for SSR-compatible page metadata.\n *\n * Supports reactive title/description — when passed as `() => string` accessors,\n * they are forwarded to `useHead()` as a reactive getter so updates propagate\n * automatically via signal tracking.\n *\n * @example\n * ```tsx\n * <Meta title=\"My Page\" description=\"...\" image=\"/og.jpg\" canonical=\"https://...\" />\n * ```\n *\n * @example Reactive title\n * ```tsx\n * const count = signal(0)\n * <Meta title={() => `${count()} items`} />\n * ```\n */\nexport function Meta(props: MetaProps): VNodeChild {\n const hasReactiveTitle = typeof props.title === 'function'\n const hasReactiveDescription = typeof props.description === 'function'\n\n // If title or description are reactive accessors, pass a getter to useHead\n // so it re-evaluates when the signals change.\n if (hasReactiveTitle || hasReactiveDescription) {\n useHead((): UseHeadInput => {\n const title = resolveStr(props.title)\n const description = resolveStr(props.description)\n const resolved = { ...props, title, description } as Parameters<typeof buildMetaTags>[0]\n const tags = buildMetaTags(resolved)\n const input: UseHeadInput = { meta: tags.meta, link: tags.link, script: tags.script }\n if (title) input.title = title\n return input\n })\n } else {\n const title = resolveStr(props.title)\n const description = resolveStr(props.description)\n const resolved = { ...props, title, description } as Parameters<typeof buildMetaTags>[0]\n const tags = buildMetaTags(resolved)\n const input: UseHeadInput = { meta: tags.meta, link: tags.link, script: tags.script }\n if (title) input.title = title\n useHead(input)\n }\n\n return props.children ?? null\n}\n\ninterface MetaTagEntry {\n name?: string\n property?: string\n content: string\n [key: string]: string | undefined\n}\n\ninterface LinkTagEntry {\n rel: string\n href?: string\n hreflang?: string\n type?: string\n sizes?: string\n [key: string]: string | undefined\n}\n\ninterface ScriptTagEntry {\n type: string\n children: string\n}\n\ninterface MetaTags {\n meta: MetaTagEntry[]\n link: LinkTagEntry[]\n script: ScriptTagEntry[]\n}\n\nexport function buildMetaTags(\n props: Omit<MetaProps, 'title' | 'description' | 'children'> & {\n title?: string\n description?: string\n },\n): MetaTags {\n const meta: MetaTagEntry[] = []\n const link: LinkTagEntry[] = []\n const script: ScriptTagEntry[] = []\n\n const {\n title, description, canonical, imageAlt, imageWidth, imageHeight,\n type = 'website', siteName,\n twitterCard = 'summary_large_image', twitterSite, twitterCreator,\n locale = 'en_US', alternateLocales,\n publishedTime, modifiedTime, author, tags, jsonLd, extra,\n video, videoWidth, videoHeight, audio,\n favicon, ogTemplate, ogImageDir, ogImageFormat,\n } = props\n\n // noIndex convenience overrides robots\n const robots = props.noIndex ? 'noindex, nofollow' : (props.robots ?? 'index, follow')\n\n // Resolve image: explicit `image` prop takes precedence over `ogTemplate`\n const image = props.image ?? (\n ogTemplate\n ? ogImagePath(ogTemplate, locale !== 'en_US' ? locale : undefined, ogImageDir, ogImageFormat)\n : undefined\n )\n\n // Auto-resolve image dimensions for OG template images\n const resolvedImageWidth = imageWidth ?? (ogTemplate && !props.image ? 1200 : undefined)\n const resolvedImageHeight = imageHeight ?? (ogTemplate && !props.image ? 630 : undefined)\n\n if (description) meta.push({ name: 'description', content: description })\n if (robots) meta.push({ name: 'robots', content: robots })\n if (author) meta.push({ name: 'author', content: author })\n\n if (title) meta.push({ property: 'og:title', content: title })\n if (description) meta.push({ property: 'og:description', content: description })\n if (canonical) meta.push({ property: 'og:url', content: canonical })\n if (image) meta.push({ property: 'og:image', content: image })\n if (imageAlt) meta.push({ property: 'og:image:alt', content: imageAlt })\n if (resolvedImageWidth) meta.push({ property: 'og:image:width', content: String(resolvedImageWidth) })\n if (resolvedImageHeight) meta.push({ property: 'og:image:height', content: String(resolvedImageHeight) })\n meta.push({ property: 'og:type', content: type })\n if (siteName) meta.push({ property: 'og:site_name', content: siteName })\n meta.push({ property: 'og:locale', content: locale })\n\n // Video\n if (video) {\n meta.push({ property: 'og:video', content: video })\n if (videoWidth) meta.push({ property: 'og:video:width', content: String(videoWidth) })\n if (videoHeight) meta.push({ property: 'og:video:height', content: String(videoHeight) })\n // Auto-detect video type from extension\n if (video.endsWith('.mp4')) meta.push({ property: 'og:video:type', content: 'video/mp4' })\n else if (video.endsWith('.webm')) meta.push({ property: 'og:video:type', content: 'video/webm' })\n }\n\n // Audio\n if (audio) {\n meta.push({ property: 'og:audio', content: audio })\n }\n\n if (type === 'article') {\n if (publishedTime) meta.push({ property: 'article:published_time', content: publishedTime })\n if (modifiedTime) meta.push({ property: 'article:modified_time', content: modifiedTime })\n if (author) meta.push({ property: 'article:author', content: author })\n if (tags) for (const tag of tags) meta.push({ property: 'article:tag', content: tag })\n }\n\n meta.push({ name: 'twitter:card', content: twitterCard })\n if (title) meta.push({ name: 'twitter:title', content: title })\n if (description) meta.push({ name: 'twitter:description', content: description })\n if (image) meta.push({ name: 'twitter:image', content: image })\n if (imageAlt) meta.push({ name: 'twitter:image:alt', content: imageAlt })\n if (twitterSite) meta.push({ name: 'twitter:site', content: twitterSite })\n if (twitterCreator) meta.push({ name: 'twitter:creator', content: twitterCreator })\n\n if (canonical) link.push({ rel: 'canonical', href: canonical })\n if (alternateLocales) {\n for (const alt of alternateLocales) {\n link.push({ rel: 'alternate', hreflang: alt.locale, href: alt.url })\n }\n }\n\n if (jsonLd) {\n script.push({\n type: 'application/ld+json',\n children: JSON.stringify({ '@context': 'https://schema.org', ...jsonLd }),\n })\n }\n\n if (extra) for (const tag of extra) meta.push(tag)\n\n // I18n: auto-generate hreflang alternates from i18nRouting config\n if (props.i18n) {\n const i18nConfig = props.i18n\n const origin = props.origin ?? ''\n const currentPath = canonical?.replace(origin, '') ?? '/'\n const { pathWithoutLocale } = extractLocaleFromPath(\n currentPath,\n i18nConfig.locales,\n i18nConfig.defaultLocale,\n )\n const strategy = i18nConfig.strategy ?? 'prefix-except-default'\n\n for (const loc of i18nConfig.locales) {\n const localizedPath =\n strategy === 'prefix-except-default' && loc === i18nConfig.defaultLocale\n ? pathWithoutLocale\n : `/${loc}${pathWithoutLocale === '/' ? '' : pathWithoutLocale}`\n\n link.push({\n rel: 'alternate',\n hreflang: loc,\n href: `${origin}${localizedPath}`,\n })\n\n // og:locale:alternate for non-current locales\n if (loc !== locale) {\n meta.push({ property: 'og:locale:alternate', content: loc })\n }\n }\n\n // x-default hreflang pointing to default locale\n link.push({\n rel: 'alternate',\n hreflang: 'x-default',\n href: `${origin}${pathWithoutLocale}`,\n })\n }\n\n // Favicon: inject locale-aware favicon links\n if (favicon) {\n const faviconLocale = locale !== 'en_US' ? locale : undefined\n for (const fl of faviconLinks(faviconLocale, favicon)) {\n link.push(fl as LinkTagEntry)\n }\n // Theme color meta from favicon config\n if (favicon.themeColor) {\n meta.push({ name: 'theme-color', content: favicon.themeColor })\n }\n }\n\n return { meta, link, script }\n}\n"],"mappings":"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"}
|
package/lib/middleware.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.js","names":[],"sources":["../src/middleware.ts"],"sourcesContent":["import type { Middleware, MiddlewareContext } from '@pyreon/server'\n\n// ─── Middleware composition ─────────────────────────────────────────────────\n//\n// Chains multiple middleware functions into a single middleware.\n// Each middleware runs in order. If any returns a Response, the chain\n// short-circuits and that Response is returned. If all return void,\n// the composed middleware returns void (continues to rendering).\n\n/**\n * Compose multiple middleware into a single middleware function.\n * Middleware runs sequentially — if any returns a Response, the chain stops.\n *\n * @example\n * import { compose } from \"@pyreon/zero/middleware\"\n * import { corsMiddleware } from \"@pyreon/zero/cors\"\n * import { rateLimitMiddleware } from \"@pyreon/zero/rate-limit\"\n *\n * const combined = compose(\n * corsMiddleware({ origin: \"*\" }),\n * rateLimitMiddleware({ max: 100 }),\n * cacheMiddleware(),\n * )\n */\nexport function compose(...middlewares: Middleware[]): Middleware {\n return async (ctx: MiddlewareContext) => {\n for (const mw of middlewares) {\n const result = await mw(ctx)\n if (result instanceof Response) return result\n }\n }\n}\n\n// ─── Shared request context ─────────────────────────────────────────────────\n//\n// Lightweight context bag attached to MiddlewareContext.locals so middleware\n// can communicate without coupling. Uses a namespaced key to avoid collisions\n// with user-defined locals.\n\nconst ZERO_CTX_KEY = '__zeroCtx'\n\n/**\n * Get the shared Zero context from a middleware context.\n * Creates one if it doesn't exist. Middleware can use this to\n * pass data to downstream middleware without polluting `ctx.locals`.\n *\n * @example\n * const authMiddleware: Middleware = (ctx) => {\n * const zctx = getContext(ctx)\n * zctx.userId = \"user_123\"\n * }\n *\n * const loggingMiddleware: Middleware = (ctx) => {\n * const zctx = getContext(ctx)\n * console.log(\"User:\", zctx.userId)\n * }\n */\nexport function getContext(ctx: MiddlewareContext): Record<string, unknown> {\n let zctx = ctx.locals[ZERO_CTX_KEY] as Record<string, unknown> | undefined\n if (!zctx) {\n zctx = {}\n ctx.locals[ZERO_CTX_KEY] = zctx\n }\n return zctx\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAwBA,SAAgB,QAAQ,GAAG,aAAuC;AAChE,QAAO,OAAO,QAA2B;AACvC,OAAK,MAAM,MAAM,aAAa;GAC5B,MAAM,SAAS,MAAM,GAAG,IAAI;AAC5B,OAAI,kBAAkB,SAAU,QAAO;;;;AAW7C,MAAM,eAAe;;;;;;;;;;;;;;;;;AAkBrB,SAAgB,WAAW,KAAiD;CAC1E,IAAI,OAAO,IAAI,OAAO;AACtB,KAAI,CAAC,MAAM;AACT,SAAO,EAAE;AACT,MAAI,OAAO,gBAAgB;;AAE7B,QAAO"}
|
package/lib/og-image.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"og-image.js","names":[],"sources":["../src/og-image.ts"],"sourcesContent":["/**\n * OG Image generation plugin.\n *\n * Generates Open Graph images at build time from templates with\n * text overlays. Supports locale-specific text for i18n apps.\n * Uses sharp for image processing (same optional dep as favicon/image plugins).\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { ogImagePlugin } from \"@pyreon/zero/og-image\"\n *\n * export default {\n * plugins: [\n * ogImagePlugin({\n * locales: [\"en\", \"de\", \"cs\"],\n * templates: [{\n * name: \"default\",\n * background: \"./src/assets/og-bg.jpg\",\n * layers: [{\n * text: { en: \"Build faster\", de: \"Schneller bauen\", cs: \"Stavte rychleji\" },\n * y: \"40%\",\n * fontSize: 72,\n * }],\n * }],\n * }),\n * ],\n * }\n * ```\n */\nimport { existsSync } from 'node:fs'\nimport { join } from 'node:path'\nimport type { Plugin } from 'vite'\n\nlet sharpWarned = false\nfunction warnSharpMissing() {\n if (sharpWarned) return\n sharpWarned = true\n // oxlint-disable-next-line no-console\n console.warn(\n '\\n[Pyreon] sharp not installed — OG images will not be generated. Install for full support: bun add -D sharp\\n',\n )\n}\n\n// ─── Types ──────────────────────────────────────────────────────────────────\n\nexport interface OgImageLayer {\n /**\n * Text content. Can be:\n * - A string (same for all locales)\n * - A record mapping locale → text\n * - A function receiving locale and returning text\n */\n text: string | Record<string, string> | ((locale: string) => string)\n /** X position — number (px) or string with % (e.g. \"50%\"). Default: \"50%\" */\n x?: number | string\n /** Y position — number (px) or string with % (e.g. \"40%\"). Default: \"50%\" */\n y?: number | string\n /** Font size in px. Default: 64 */\n fontSize?: number\n /** Font family. Default: \"sans-serif\" */\n fontFamily?: string\n /** Font weight. Default: \"bold\" */\n fontWeight?: string\n /** Text color. Default: \"#ffffff\" */\n color?: string\n /** Text anchor (alignment). Default: \"middle\" */\n textAnchor?: 'start' | 'middle' | 'end'\n /** Max width in px before wrapping. Default: 80% of image width. */\n maxWidth?: number\n}\n\nexport interface OgImageTemplate {\n /** Template name — used for output file naming. */\n name: string\n /**\n * Background: path to an image file, or a solid color config.\n *\n * @example \"./src/assets/og-bg.jpg\"\n * @example { color: \"#0066ff\", width: 1200, height: 630 }\n */\n background: string | { color: string; width?: number; height?: number }\n /** Output width. Default: 1200 */\n width?: number\n /** Output height. Default: 630 */\n height?: number\n /** Output format. Default: \"png\" */\n format?: 'png' | 'jpeg'\n /** JPEG quality (1-100). Default: 90 */\n quality?: number\n /** Text layers to overlay on the background. */\n layers?: OgImageLayer[]\n}\n\nexport interface OgImagePluginConfig {\n /** Templates to generate. */\n templates: OgImageTemplate[]\n /** Locales to generate for. When omitted, generates a single image per template. */\n locales?: string[]\n /** Output directory prefix. Default: \"og\" */\n outDir?: string\n}\n\n// ─── Helpers ────────────────────────────────────────────────────────────────\n\nfunction resolvePosition(value: number | string | undefined, dimension: number, fallback = '50%'): number {\n if (value === undefined) value = fallback\n if (typeof value === 'number') return value\n if (value.endsWith('%')) return Math.round((Number.parseFloat(value) / 100) * dimension)\n return Number.parseInt(value, 10) || 0\n}\n\nfunction resolveLayerText(layer: OgImageLayer, locale: string): string {\n if (typeof layer.text === 'string') return layer.text\n if (typeof layer.text === 'function') return layer.text(locale)\n return layer.text[locale] ?? layer.text[Object.keys(layer.text)[0] ?? ''] ?? ''\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n}\n\n/**\n * Build an SVG overlay with text layers.\n * @internal Exported for testing.\n */\nexport function buildTextOverlaySvg(\n layers: OgImageLayer[],\n width: number,\n height: number,\n locale: string,\n): string {\n const textElements = layers.map((layer) => {\n const text = resolveLayerText(layer, locale)\n const x = resolvePosition(layer.x, width, '50%')\n const y = resolvePosition(layer.y, height, '50%')\n const fontSize = layer.fontSize ?? 64\n const fontFamily = layer.fontFamily ?? 'sans-serif'\n const fontWeight = layer.fontWeight ?? 'bold'\n const color = layer.color ?? '#ffffff'\n const anchor = layer.textAnchor ?? 'middle'\n const maxWidth = layer.maxWidth ?? Math.round(width * 0.8)\n\n // Word wrapping via tspan elements.\n // Width estimation: Latin chars ~0.55em, CJK chars ~1.0em, narrow chars ~0.35em.\n const words = text.split(' ')\n const lines: string[] = []\n let currentLine = ''\n\n const estimateWidth = (s: string): number => {\n let width = 0\n for (let i = 0; i < s.length; i++) {\n const code = s.charCodeAt(i)\n if (code >= 0x3000 && code <= 0x9FFF) {\n // CJK characters — full width\n width += fontSize * 1.0\n } else if (code <= 0x7E && 'iljft!|:;.,\\''.includes(s[i]!)) {\n // Narrow Latin characters\n width += fontSize * 0.35\n } else {\n // Regular Latin characters\n width += fontSize * 0.55\n }\n }\n return width\n }\n\n for (const word of words) {\n const testLine = currentLine ? `${currentLine} ${word}` : word\n if (estimateWidth(testLine) > maxWidth && currentLine) {\n lines.push(currentLine)\n currentLine = word\n } else {\n currentLine = testLine\n }\n }\n if (currentLine) lines.push(currentLine)\n\n const tspans = lines\n .map((line, i) => {\n const dy = i === 0 ? '0' : `${fontSize * 1.2}`\n return `<tspan x=\"${x}\" dy=\"${dy}\">${escapeXml(line)}</tspan>`\n })\n .join('')\n\n return `<text x=\"${x}\" y=\"${y}\" font-size=\"${fontSize}\" font-family=\"${escapeXml(fontFamily)}\" font-weight=\"${fontWeight}\" fill=\"${color}\" text-anchor=\"${anchor}\" dominant-baseline=\"middle\">${tspans}</text>`\n })\n\n return `<svg width=\"${width}\" height=\"${height}\" xmlns=\"http://www.w3.org/2000/svg\">${textElements.join('')}</svg>`\n}\n\n/**\n * Render an OG image from a template for a specific locale.\n * @internal Exported for testing.\n */\nexport async function renderOgImage(\n template: OgImageTemplate,\n locale: string,\n rootDir: string,\n): Promise<Uint8Array | null> {\n try {\n const sharp = await import('sharp').then((m) => m.default ?? m)\n const width = template.width ?? 1200\n const height = template.height ?? 630\n\n let pipeline: any\n if (typeof template.background === 'string') {\n const bgPath = join(rootDir, template.background)\n pipeline = sharp(bgPath).resize(width, height, { fit: 'cover' })\n } else {\n pipeline = (sharp as any)({\n create: {\n width,\n height,\n channels: 4,\n background: template.background.color,\n },\n })\n }\n\n // Overlay text layers if any\n if (template.layers && template.layers.length > 0) {\n const svgOverlay = buildTextOverlaySvg(template.layers, width, height, locale)\n pipeline = pipeline.composite([{\n input: Buffer.from(svgOverlay),\n top: 0,\n left: 0,\n }])\n }\n\n if (template.format === 'jpeg') {\n return await pipeline.jpeg({ quality: template.quality ?? 90 }).toBuffer()\n }\n return await pipeline.png().toBuffer()\n } catch {\n warnSharpMissing()\n return null\n }\n}\n\n// ─── Path utility ───────────────────────────────────────────────────────────\n\n/**\n * Compute the OG image path for a template and locale.\n *\n * @example\n * ```ts\n * ogImagePath(\"default\", \"de\") // → \"/og/default-de.png\"\n * ogImagePath(\"default\") // → \"/og/default.png\"\n * ogImagePath(\"hero\", \"en\", \"images\") // → \"/images/hero-en.png\"\n * ```\n */\nexport function ogImagePath(\n templateName: string,\n locale?: string,\n outDir = 'og',\n format: 'png' | 'jpeg' = 'png',\n): string {\n const ext = format === 'jpeg' ? 'jpg' : 'png'\n const suffix = locale ? `-${locale}` : ''\n return `/${outDir}/${templateName}${suffix}.${ext}`\n}\n\n// ─── Vite plugin ────────────────────────────────────────────────────────────\n\n/**\n * OG image generation Vite plugin.\n *\n * Generates Open Graph images at build time. In dev, generates on-demand.\n * Requires `sharp` as an optional dependency.\n *\n * @example\n * ```ts\n * // vite.config.ts\n * import { ogImagePlugin } from \"@pyreon/zero/og-image\"\n *\n * export default {\n * plugins: [\n * ogImagePlugin({\n * locales: [\"en\", \"de\"],\n * templates: [{\n * name: \"default\",\n * background: { color: \"#0066ff\" },\n * layers: [{ text: { en: \"Hello\", de: \"Hallo\" }, fontSize: 72 }],\n * }],\n * }),\n * ],\n * }\n * ```\n */\nexport function ogImagePlugin(config: OgImagePluginConfig): Plugin {\n const outDir = config.outDir ?? 'og'\n let root = ''\n let isBuild = false\n\n return {\n name: 'pyreon-zero-og-image',\n enforce: 'pre',\n\n configResolved(resolvedConfig) {\n root = resolvedConfig.root\n isBuild = resolvedConfig.command === 'build'\n },\n\n // Dev: generate on-demand\n configureServer(server) {\n const devCache = new Map<string, Uint8Array>()\n\n server.middlewares.use(async (req, res, next) => {\n const url = req.url ?? ''\n if (!url.startsWith(`/${outDir}/`)) return next()\n\n // Parse: /og/default-en.png → template=default, locale=en\n const fileName = url.slice(outDir.length + 2) // strip /{outDir}/\n const match = fileName.match(/^(.+?)(?:-([a-z]{2,5}))?\\.(png|jpe?g)$/)\n if (!match) return next()\n\n const [, templateName, locale, ext] = match\n const template = config.templates.find((t) => t.name === templateName)\n if (!template) return next()\n\n const resolvedLocale = locale ?? config.locales?.[0] ?? 'en'\n const cacheKey = `${templateName}:${resolvedLocale}`\n\n let buffer = devCache.get(cacheKey)\n if (!buffer) {\n const result = await renderOgImage(template, resolvedLocale, root)\n if (!result) return next()\n buffer = result\n devCache.set(cacheKey, result)\n }\n\n const contentType = ext === 'jpg' || ext === 'jpeg' ? 'image/jpeg' : 'image/png'\n res.setHeader('Content-Type', contentType)\n res.setHeader('Cache-Control', 'no-cache')\n res.end(Buffer.from(buffer))\n })\n },\n\n // Build: generate all variants\n async generateBundle() {\n if (!isBuild) return\n\n for (const template of config.templates) {\n const locales = config.locales ?? [undefined]\n const format = template.format ?? 'png'\n const ext = format === 'jpeg' ? 'jpg' : 'png'\n\n for (const locale of locales) {\n // Validate background exists if it's a file path\n if (typeof template.background === 'string') {\n const bgPath = join(root, template.background)\n if (!existsSync(bgPath)) {\n // oxlint-disable-next-line no-console\n console.warn(`[Pyreon] Background not found: ${bgPath}`)\n continue\n }\n }\n\n const buffer = await renderOgImage(template, locale ?? 'en', root)\n if (!buffer) continue\n\n const suffix = locale ? `-${locale}` : ''\n this.emitFile({\n type: 'asset',\n fileName: `${outDir}/${template.name}${suffix}.${ext}`,\n source: buffer,\n })\n }\n }\n },\n }\n}\n"],"mappings":"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"}
|
package/lib/rate-limit.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limit.js","names":[],"sources":["../src/rate-limit.ts"],"sourcesContent":["import type { Middleware, MiddlewareContext } from '@pyreon/server'\n\n// ─── Rate limiting middleware ───────────────────────────────────────────────\n\nexport interface RateLimitConfig {\n /** Maximum requests per window. Default: `100` */\n max?: number\n /** Time window in seconds. Default: `60` */\n window?: number\n /** Function to extract the client identifier. Default: IP from headers. */\n keyFn?: (ctx: MiddlewareContext) => string\n /** Custom response when rate limited. */\n onLimit?: (ctx: MiddlewareContext) => Response\n /** URL patterns to rate limit (glob-style). Default: all paths. */\n include?: string[]\n /** URL patterns to exclude from rate limiting. */\n exclude?: string[]\n}\n\ninterface RateLimitEntry {\n count: number\n resetAt: number\n}\n\n/**\n * Rate limiting middleware — limits requests per client within a time window.\n * Uses an in-memory store (suitable for single-instance deployments).\n *\n * @example\n * import { rateLimitMiddleware } from \"@pyreon/zero/rate-limit\"\n *\n * // 100 requests per minute (default)\n * rateLimitMiddleware()\n *\n * // Strict API rate limiting\n * rateLimitMiddleware({\n * max: 20,\n * window: 60,\n * include: [\"/api/*\"],\n * })\n */\nexport function rateLimitMiddleware(config: RateLimitConfig = {}): Middleware {\n const {\n max = 100,\n window: windowSec = 60,\n keyFn = defaultKeyFn,\n onLimit,\n include,\n exclude,\n } = config\n\n const windowMs = windowSec * 1000\n const store = new Map<string, RateLimitEntry>()\n const MAX_STORE_SIZE = 10000\n let lastCleanup = Date.now()\n\n // Inline cleanup — runs during request processing, no setInterval needed.\n // Evicts expired entries when store exceeds half capacity or on window boundary.\n function cleanupIfNeeded(now: number) {\n if (store.size < MAX_STORE_SIZE / 2 && now - lastCleanup < windowMs) return\n lastCleanup = now\n for (const [key, entry] of store) {\n if (entry.resetAt <= now) store.delete(key)\n }\n }\n\n return (ctx: MiddlewareContext) => {\n // Check include/exclude patterns\n if (include && !include.some((p) => matchSimpleGlob(p, ctx.path))) return\n if (exclude?.some((p) => matchSimpleGlob(p, ctx.path))) return\n\n const key = keyFn(ctx)\n const now = Date.now()\n\n cleanupIfNeeded(now)\n\n let entry = store.get(key)\n\n if (!entry || entry.resetAt <= now) {\n entry = { count: 0, resetAt: now + windowMs }\n store.set(key, entry)\n }\n\n entry.count++\n const remaining = Math.max(0, max - entry.count)\n const resetSeconds = Math.ceil((entry.resetAt - now) / 1000)\n\n // Set rate limit headers on all responses\n ctx.headers.set('X-RateLimit-Limit', String(max))\n ctx.headers.set('X-RateLimit-Remaining', String(remaining))\n ctx.headers.set('X-RateLimit-Reset', String(resetSeconds))\n\n if (entry.count > max) {\n if (onLimit) return onLimit(ctx)\n\n return new Response(JSON.stringify({ error: 'Too many requests' }), {\n status: 429,\n headers: {\n 'Content-Type': 'application/json',\n 'Retry-After': String(resetSeconds),\n 'X-RateLimit-Limit': String(max),\n 'X-RateLimit-Remaining': '0',\n 'X-RateLimit-Reset': String(resetSeconds),\n },\n })\n }\n }\n}\n\nfunction defaultKeyFn(ctx: MiddlewareContext): string {\n return (\n ctx.req.headers.get('x-forwarded-for')?.split(',')[0]?.trim() ??\n ctx.req.headers.get('x-real-ip') ??\n 'unknown'\n )\n}\n\n/** Simple glob matching for path patterns. Supports trailing `*`. */\nfunction matchSimpleGlob(pattern: string, path: string): boolean {\n if (pattern.endsWith('/*')) {\n return path.startsWith(pattern.slice(0, -1))\n }\n return pattern === path\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAyCA,SAAgB,oBAAoB,SAA0B,EAAE,EAAc;CAC5E,MAAM,EACJ,MAAM,KACN,QAAQ,YAAY,IACpB,QAAQ,cACR,SACA,SACA,YACE;CAEJ,MAAM,WAAW,YAAY;CAC7B,MAAM,wBAAQ,IAAI,KAA6B;CAC/C,MAAM,iBAAiB;CACvB,IAAI,cAAc,KAAK,KAAK;CAI5B,SAAS,gBAAgB,KAAa;AACpC,MAAI,MAAM,OAAO,iBAAiB,KAAK,MAAM,cAAc,SAAU;AACrE,gBAAc;AACd,OAAK,MAAM,CAAC,KAAK,UAAU,MACzB,KAAI,MAAM,WAAW,IAAK,OAAM,OAAO,IAAI;;AAI/C,SAAQ,QAA2B;AAEjC,MAAI,WAAW,CAAC,QAAQ,MAAM,MAAM,gBAAgB,GAAG,IAAI,KAAK,CAAC,CAAE;AACnE,MAAI,SAAS,MAAM,MAAM,gBAAgB,GAAG,IAAI,KAAK,CAAC,CAAE;EAExD,MAAM,MAAM,MAAM,IAAI;EACtB,MAAM,MAAM,KAAK,KAAK;AAEtB,kBAAgB,IAAI;EAEpB,IAAI,QAAQ,MAAM,IAAI,IAAI;AAE1B,MAAI,CAAC,SAAS,MAAM,WAAW,KAAK;AAClC,WAAQ;IAAE,OAAO;IAAG,SAAS,MAAM;IAAU;AAC7C,SAAM,IAAI,KAAK,MAAM;;AAGvB,QAAM;EACN,MAAM,YAAY,KAAK,IAAI,GAAG,MAAM,MAAM,MAAM;EAChD,MAAM,eAAe,KAAK,MAAM,MAAM,UAAU,OAAO,IAAK;AAG5D,MAAI,QAAQ,IAAI,qBAAqB,OAAO,IAAI,CAAC;AACjD,MAAI,QAAQ,IAAI,yBAAyB,OAAO,UAAU,CAAC;AAC3D,MAAI,QAAQ,IAAI,qBAAqB,OAAO,aAAa,CAAC;AAE1D,MAAI,MAAM,QAAQ,KAAK;AACrB,OAAI,QAAS,QAAO,QAAQ,IAAI;AAEhC,UAAO,IAAI,SAAS,KAAK,UAAU,EAAE,OAAO,qBAAqB,CAAC,EAAE;IAClE,QAAQ;IACR,SAAS;KACP,gBAAgB;KAChB,eAAe,OAAO,aAAa;KACnC,qBAAqB,OAAO,IAAI;KAChC,yBAAyB;KACzB,qBAAqB,OAAO,aAAa;KAC1C;IACF,CAAC;;;;AAKR,SAAS,aAAa,KAAgC;AACpD,QACE,IAAI,IAAI,QAAQ,IAAI,kBAAkB,EAAE,MAAM,IAAI,CAAC,IAAI,MAAM,IAC7D,IAAI,IAAI,QAAQ,IAAI,YAAY,IAChC;;;AAKJ,SAAS,gBAAgB,SAAiB,MAAuB;AAC/D,KAAI,QAAQ,SAAS,KAAK,CACxB,QAAO,KAAK,WAAW,QAAQ,MAAM,GAAG,GAAG,CAAC;AAE9C,QAAO,YAAY"}
|
package/lib/script.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"script.js","names":[],"sources":["../src/utils/use-intersection-observer.ts","../../../core/core/lib/jsx-runtime.js","../src/script.tsx"],"sourcesContent":["import { onMount, onUnmount } from '@pyreon/core'\n\n/**\n * Observes an element and calls `onIntersect` once it enters the viewport.\n * Automatically disconnects after the first intersection.\n *\n * @param getElement - Getter for the target element (may be undefined before mount).\n * @param onIntersect - Callback fired when the element becomes visible.\n * @param rootMargin - IntersectionObserver rootMargin. Default: \"200px\".\n */\nexport function useIntersectionObserver(\n getElement: () => HTMLElement | undefined,\n onIntersect: () => void,\n rootMargin = '200px',\n) {\n onMount(() => {\n const el = getElement()\n if (!el) return undefined\n\n const observer = new IntersectionObserver(\n (entries) => {\n for (const entry of entries) {\n if (entry.isIntersecting) {\n onIntersect()\n observer.disconnect()\n }\n }\n },\n { rootMargin },\n )\n\n observer.observe(el)\n onUnmount(() => observer.disconnect())\n return undefined\n })\n}\n","//#region src/h.ts\n/** Marker for fragment nodes — renders children without a wrapper element */\nconst Fragment = Symbol(\"Pyreon.Fragment\");\n/**\n* Hyperscript function — the compiled output of JSX.\n* `<div class=\"x\">hello</div>` → `h(\"div\", { class: \"x\" }, \"hello\")`\n*\n* Generic on P so TypeScript validates props match the component's signature\n* at the call site, then stores the result in the loosely-typed VNode.\n*/\n/** Shared empty props sentinel — identity-checked in mountElement to skip applyProps. */\nconst EMPTY_PROPS = {};\nfunction h(type, props, ...children) {\n\treturn {\n\t\ttype,\n\t\tprops: props ?? EMPTY_PROPS,\n\t\tchildren: normalizeChildren(children),\n\t\tkey: props?.key ?? null\n\t};\n}\nfunction normalizeChildren(children) {\n\tfor (let i = 0; i < children.length; i++) if (Array.isArray(children[i])) return flattenChildren(children);\n\treturn children;\n}\nfunction flattenChildren(children) {\n\tconst result = [];\n\tfor (const child of children) if (Array.isArray(child)) result.push(...flattenChildren(child));\n\telse result.push(child);\n\treturn result;\n}\n\n//#endregion\n//#region src/jsx-runtime.ts\n/**\n* JSX automatic runtime.\n*\n* When tsconfig has `\"jsxImportSource\": \"@pyreon/core\"`, the TS/bundler compiler\n* rewrites JSX to imports from this file automatically:\n* <div class=\"x\" /> → jsx(\"div\", { class: \"x\" })\n*/\nfunction jsx(type, props, key) {\n\tconst { children, ...rest } = props;\n\tconst propsWithKey = key != null ? {\n\t\t...rest,\n\t\tkey\n\t} : rest;\n\tif (typeof type === \"function\") return h(type, children !== void 0 ? {\n\t\t...propsWithKey,\n\t\tchildren\n\t} : propsWithKey);\n\treturn h(type, propsWithKey, ...children === void 0 ? [] : Array.isArray(children) ? children : [children]);\n}\nconst jsxs = jsx;\n\n//#endregion\nexport { Fragment, jsx, jsxs };\n//# sourceMappingURL=jsx-runtime.js.map","import type { VNodeChild } from '@pyreon/core'\nimport { createRef, onMount, onUnmount } from '@pyreon/core'\nimport { useIntersectionObserver } from './utils/use-intersection-observer'\n\n// ─── Script optimization component ─────────────────────────────────────────\n//\n// <Script> provides optimized third-party script loading:\n// - Defer loading until after hydration\n// - Load on idle (requestIdleCallback)\n// - Load on interaction (click, scroll, etc.)\n// - Load on viewport entry\n// - Worker offloading for analytics scripts\n\nexport interface ScriptProps {\n /** Script source URL. */\n src: string\n /** Loading strategy. Default: \"afterHydration\" */\n strategy?: ScriptStrategy\n /** Inline script content (alternative to src). */\n children?: string\n /** Script id for deduplication. */\n id?: string\n /** Async attribute. Default: true */\n async?: boolean\n /** onLoad callback. */\n onLoad?: () => void\n /** onError callback. */\n onError?: (error: Error) => void\n}\n\nexport type ScriptStrategy =\n | 'beforeHydration'\n | 'afterHydration'\n | 'onIdle'\n | 'onInteraction'\n | 'onViewport'\n\n/**\n * Optimized script loading component.\n *\n * @example\n * // Load analytics after page is interactive\n * <Script src=\"https://analytics.example.com/script.js\" strategy=\"onIdle\" />\n *\n * // Load chat widget when user scrolls\n * <Script src=\"/chat-widget.js\" strategy=\"onViewport\" />\n *\n * // Inline script with deferred execution\n * <Script strategy=\"afterHydration\">\n * {`console.log(\"App hydrated!\")`}\n * </Script>\n */\nexport function Script(props: ScriptProps): VNodeChild {\n function loadScript() {\n // Only invoked from `onMount` — explicit guard documents the\n // SSR-safety contract at the callsite (the rule can't AST-trace the\n // indirect call).\n if (typeof document === 'undefined') return\n // Deduplication\n if (props.id && document.getElementById(props.id)) return\n\n const script = document.createElement('script')\n if (props.src) script.src = props.src\n if (props.id) script.id = props.id\n script.async = props.async !== false\n\n if (props.onLoad) script.onload = props.onLoad\n if (props.onError) {\n script.onerror = () => props.onError?.(new Error(`Failed to load: ${props.src}`))\n }\n\n if (props.children && !props.src) {\n script.textContent = props.children\n }\n\n document.head.appendChild(script)\n }\n\n onMount(() => {\n const strategy = props.strategy ?? 'afterHydration'\n\n switch (strategy) {\n case 'beforeHydration':\n // Already in HTML — do nothing\n break\n\n case 'afterHydration':\n // Load immediately after mount (hydration is complete)\n loadScript()\n break\n\n case 'onIdle':\n if ('requestIdleCallback' in window) {\n requestIdleCallback(() => loadScript(), { timeout: 5000 })\n } else {\n setTimeout(loadScript, 200)\n }\n break\n\n case 'onInteraction': {\n const events = ['click', 'scroll', 'keydown', 'touchstart']\n function handler() {\n for (const e of events) document.removeEventListener(e, handler)\n loadScript()\n }\n for (const e of events) {\n document.addEventListener(e, handler, { once: true, passive: true })\n }\n onUnmount(() => {\n for (const e of events) document.removeEventListener(e, handler)\n })\n break\n }\n\n case 'onViewport':\n // Handled below via useIntersectionObserver on the sentinel element\n break\n }\n return undefined\n })\n\n const sentinelRef = createRef<HTMLElement>()\n const strategy = props.strategy ?? 'afterHydration'\n\n if (strategy === 'onViewport') {\n useIntersectionObserver(\n () => sentinelRef.current ?? undefined,\n () => loadScript(),\n )\n }\n\n if (strategy === 'onViewport') {\n return <div ref={sentinelRef} style=\"width:0;height:0;overflow:hidden\" />\n }\n\n return null\n}\n"],"mappings":"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"}
|
package/lib/seo.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"seo.js","names":[],"sources":["../src/seo.ts"],"sourcesContent":["import type { Middleware } from '@pyreon/server'\nimport type { Plugin } from 'vite'\n\n// ─── SEO utilities ──────────────────────────────────────────────────────────\n//\n// Zero provides built-in SEO tooling:\n// - Automatic sitemap.xml generation from file-based routes\n// - Configurable robots.txt\n// - Structured data (JSON-LD) helpers\n// - Open Graph / Twitter Card meta helpers\n\nexport interface SitemapConfig {\n /** Base URL of the site (required). e.g. \"https://example.com\" */\n origin: string\n /** Paths to exclude from the sitemap. */\n exclude?: string[]\n /** Default change frequency. Default: \"weekly\" */\n changefreq?: ChangeFreq\n /** Default priority. Default: 0.7 */\n priority?: number\n /** Additional URLs to include (for dynamic routes). */\n additionalPaths?: SitemapEntry[]\n}\n\nexport interface SitemapEntry {\n path: string\n changefreq?: ChangeFreq\n priority?: number\n lastmod?: string\n}\n\nexport type ChangeFreq = 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never'\n\n/**\n * Generate a sitemap.xml string from route file paths.\n */\nexport function generateSitemap(routeFiles: string[], config: SitemapConfig): string {\n const { origin, exclude = [], changefreq = 'weekly', priority = 0.7 } = config\n\n const paths = routeFiles\n .filter((f) => {\n // Exclude layout, error, loading files\n const name = f\n .split('/')\n .pop()\n ?.replace(/\\.\\w+$/, '')\n return name !== '_layout' && name !== '_error' && name !== '_loading'\n })\n .map((f) => {\n // Convert file path to URL\n let path = f\n .replace(/\\.\\w+$/, '')\n .replace(/\\/index$/, '/')\n .replace(/^index$/, '/')\n\n // Skip dynamic routes — they need additionalPaths\n if (path.includes('[')) return null\n\n // Strip route groups\n path = path.replace(/\\([\\w-]+\\)\\//g, '')\n\n if (!path.startsWith('/')) path = `/${path}`\n return path\n })\n .filter((p): p is string => p !== null)\n .filter((p) => !exclude.some((e) => p.startsWith(e)))\n\n const allPaths: SitemapEntry[] = [\n ...paths.map((p) => ({ path: p, changefreq, priority })),\n ...(config.additionalPaths ?? []),\n ]\n\n const entries = allPaths\n .map((entry) => {\n const loc = `${origin}${entry.path === '/' ? '' : entry.path}`\n return ` <url>\n <loc>${escapeXml(loc)}</loc>\n <changefreq>${entry.changefreq ?? changefreq}</changefreq>\n <priority>${entry.priority ?? priority}</priority>${entry.lastmod ? `\\n <lastmod>${entry.lastmod}</lastmod>` : ''}\n </url>`\n })\n .join('\\n')\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n${entries}\n</urlset>`\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/\"/g, '"')\n .replace(/'/g, ''')\n}\n\n// ─── Robots.txt ─────────────────────────────────────────────────────────────\n\nexport interface RobotsConfig {\n /** Rules per user-agent. */\n rules?: RobotsRule[]\n /** Sitemap URL. */\n sitemap?: string\n /** Host directive. */\n host?: string\n}\n\nexport interface RobotsRule {\n userAgent: string\n allow?: string[]\n disallow?: string[]\n crawlDelay?: number\n}\n\n/**\n * Generate a robots.txt string.\n */\nexport function generateRobots(config: RobotsConfig = {}): string {\n const { rules = [{ userAgent: '*', allow: ['/'] }], sitemap, host } = config\n const lines: string[] = []\n\n for (const rule of rules) {\n lines.push(`User-agent: ${rule.userAgent}`)\n if (rule.allow) {\n for (const path of rule.allow) lines.push(`Allow: ${path}`)\n }\n if (rule.disallow) {\n for (const path of rule.disallow) lines.push(`Disallow: ${path}`)\n }\n if (rule.crawlDelay) lines.push(`Crawl-delay: ${rule.crawlDelay}`)\n lines.push('')\n }\n\n if (sitemap) lines.push(`Sitemap: ${sitemap}`)\n if (host) lines.push(`Host: ${host}`)\n\n return lines.join('\\n')\n}\n\n// ─── Structured data (JSON-LD) ──────────────────────────────────────────────\n\nexport type JsonLdType =\n | 'WebSite'\n | 'WebPage'\n | 'Article'\n | 'BlogPosting'\n | 'Product'\n | 'Organization'\n | 'Person'\n | 'BreadcrumbList'\n | 'FAQPage'\n | (string & {})\n\n/**\n * Generate a JSON-LD script tag string for structured data.\n *\n * @example\n * useHead({\n * script: [jsonLd({\n * \"@type\": \"WebSite\",\n * name: \"My Site\",\n * url: \"https://example.com\",\n * })],\n * })\n */\nexport function jsonLd(data: Record<string, unknown>): string {\n const ld = {\n '@context': 'https://schema.org',\n ...data,\n }\n return `<script type=\"application/ld+json\">${JSON.stringify(ld)}</script>`\n}\n\n// ─── SEO Vite plugin ────────────────────────────────────────────────────────\n\nexport interface SeoPluginConfig {\n /** Sitemap configuration. */\n sitemap?: SitemapConfig\n /** Robots.txt configuration. */\n robots?: RobotsConfig\n}\n\n/**\n * Zero SEO Vite plugin.\n * Generates sitemap.xml and robots.txt at build time.\n *\n * @example\n * import { seoPlugin } from \"@pyreon/zero/seo\"\n *\n * export default {\n * plugins: [\n * pyreon(),\n * zero(),\n * seoPlugin({\n * sitemap: { origin: \"https://example.com\" },\n * robots: { sitemap: \"https://example.com/sitemap.xml\" },\n * }),\n * ],\n * }\n */\nexport function seoPlugin(config: SeoPluginConfig = {}): Plugin {\n return {\n name: 'pyreon-zero-seo',\n apply: 'build',\n\n async generateBundle(_, _bundle) {\n // Generate sitemap.xml\n if (config.sitemap) {\n const { scanRouteFiles } = await import('./fs-router')\n const routesDir = `${process.cwd()}/src/routes`\n\n try {\n const files = await scanRouteFiles(routesDir)\n const sitemap = generateSitemap(files, config.sitemap)\n\n this.emitFile({\n type: 'asset',\n fileName: 'sitemap.xml',\n source: sitemap,\n })\n } catch {\n // Sitemap generation failed — skip silently\n }\n }\n\n // Generate robots.txt\n if (config.robots) {\n const robots = generateRobots(config.robots)\n\n this.emitFile({\n type: 'asset',\n fileName: 'robots.txt',\n source: robots,\n })\n }\n },\n }\n}\n\n// ─── SEO middleware (serve sitemap/robots in dev) ────────────────────────────\n\n/**\n * SEO middleware for dev server.\n * Serves sitemap.xml and robots.txt dynamically during development.\n */\nexport function seoMiddleware(config: SeoPluginConfig = {}): Middleware {\n return async (ctx) => {\n if (ctx.url.pathname === '/robots.txt' && config.robots) {\n return new Response(generateRobots(config.robots), {\n headers: { 'Content-Type': 'text/plain' },\n })\n }\n\n if (ctx.url.pathname === '/sitemap.xml' && config.sitemap) {\n try {\n const { scanRouteFiles } = await import('./fs-router')\n const routesDir = `${process.cwd()}/src/routes`\n const files = await scanRouteFiles(routesDir)\n const sitemap = generateSitemap(files, config.sitemap)\n\n return new Response(sitemap, {\n headers: { 'Content-Type': 'application/xml' },\n })\n } catch {\n // Sitemap generation failed — continue to rendering\n }\n }\n }\n}\n"],"mappings":"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"}
|