@pyreon/zero 0.24.5 → 0.24.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/package.json +10 -39
  2. package/src/actions.ts +0 -196
  3. package/src/adapters/bun.ts +0 -114
  4. package/src/adapters/cloudflare.ts +0 -166
  5. package/src/adapters/index.ts +0 -61
  6. package/src/adapters/netlify.ts +0 -154
  7. package/src/adapters/node.ts +0 -163
  8. package/src/adapters/static.ts +0 -42
  9. package/src/adapters/validate.ts +0 -23
  10. package/src/adapters/vercel.ts +0 -182
  11. package/src/adapters/warn-missing-env.ts +0 -49
  12. package/src/ai.ts +0 -623
  13. package/src/api-routes.ts +0 -219
  14. package/src/app.ts +0 -92
  15. package/src/cache.ts +0 -136
  16. package/src/client.ts +0 -143
  17. package/src/compression.ts +0 -116
  18. package/src/config.ts +0 -35
  19. package/src/cors.ts +0 -94
  20. package/src/csp.ts +0 -226
  21. package/src/entry-server.ts +0 -224
  22. package/src/env.ts +0 -344
  23. package/src/error-overlay.ts +0 -118
  24. package/src/favicon.ts +0 -841
  25. package/src/font.ts +0 -511
  26. package/src/fs-router.ts +0 -1519
  27. package/src/i18n-routing.ts +0 -533
  28. package/src/icon.tsx +0 -182
  29. package/src/icons-plugin.ts +0 -296
  30. package/src/image-plugin.ts +0 -751
  31. package/src/image-types.ts +0 -60
  32. package/src/image.tsx +0 -340
  33. package/src/index.ts +0 -92
  34. package/src/isr.ts +0 -394
  35. package/src/link.tsx +0 -304
  36. package/src/logger.ts +0 -144
  37. package/src/manifest.ts +0 -787
  38. package/src/meta.tsx +0 -354
  39. package/src/middleware.ts +0 -65
  40. package/src/not-found.ts +0 -44
  41. package/src/og-image.ts +0 -378
  42. package/src/rate-limit.ts +0 -140
  43. package/src/script.tsx +0 -260
  44. package/src/seo.ts +0 -617
  45. package/src/server.ts +0 -89
  46. package/src/sharp.d.ts +0 -22
  47. package/src/ssg-plugin.ts +0 -1582
  48. package/src/testing.ts +0 -146
  49. package/src/theme.tsx +0 -257
  50. package/src/types.ts +0 -624
  51. package/src/utils/use-intersection-observer.ts +0 -36
  52. package/src/utils/with-headers.ts +0 -13
  53. package/src/vercel-revalidate-handler.ts +0 -204
  54. package/src/vite-plugin.ts +0 -848
package/src/icon.tsx DELETED
@@ -1,182 +0,0 @@
1
- import type { PyreonHTMLAttributes, SvgAttributes, VNodeChild } from '@pyreon/core'
2
- import { splitProps } from '@pyreon/core'
3
-
4
- // ─── Icon ───────────────────────────────────────────────────────────────────
5
- //
6
- // Renders a FULL, already-complete SVG that you loaded — it does NOT
7
- // synthesize its own <svg> wrapper around hand-authored <path> children.
8
- // You load an svg (it contains the <svg> root itself); Icon renders it and
9
- // makes it container-sizable + theme-aware.
10
- //
11
- // Two ways to hand it the loaded svg (you chose: support both):
12
- // • `as` — an imported SVG *component* (`import X from './x.svg?component'`).
13
- // Rendered directly — NO host wrapper. Recommended form: it's a
14
- // real <svg> element, so container-fill is reliable.
15
- // • `svg` — the raw `<svg>…</svg>` *markup string*
16
- // (`import x from './x.svg?raw'`). Inlined via a single `<span>`
17
- // host (a markup string can't mount without a parent element —
18
- // this one host is unavoidable for the string form).
19
- //
20
- // Either way:
21
- // • Container-filling defaults (`fill="currentColor"`,
22
- // `display:block;width:100%;height:100%`) — every consumer prop spreads
23
- // through and OVERRIDES them (`style`, `class`, `fill`, `aria-*`, …).
24
- // • No fixed size → it fills its container; the consumer's wrapper
25
- // (`<span style="width:2rem"><Icon/></span>`, a flex/grid cell,
26
- // `font-size`) controls the size.
27
- // • `fill="currentColor"` → CSS `color` themes it (dark mode for free).
28
- //
29
- // Two layers (mirrors createLink/Link, createImage/Image):
30
- // 1. createIcon(source) — factory: one component per loaded glyph
31
- // 2. Icon — generic shell for a one-off loaded svg
32
- //
33
- // There is intentionally no `useIcon` — an icon has no composable behaviour
34
- // (no async, no state, no router). A hook layer would be surface for its
35
- // own sake.
36
-
37
- const FILL_STYLE = 'display:block;width:100%;height:100%'
38
-
39
- /** An imported SVG component (`import X from './x.svg?component'`). */
40
- export type SvgComponent = (props: SvgAttributes) => VNodeChild
41
-
42
- /**
43
- * Props for {@link Icon}. The standard `<svg>` attribute surface
44
- * (`fill`, `class`, `style`, `aria-*`, `onClick`, …) — every one passed
45
- * straight through and overriding the container-fill defaults — plus the
46
- * two source props.
47
- */
48
- export interface IconProps extends SvgAttributes {
49
- /**
50
- * An imported SVG component, e.g. `import X from './icon.svg?component'`.
51
- * Rendered directly with no host wrapper. Recommended over `svg`.
52
- */
53
- as?: SvgComponent | undefined
54
- /**
55
- * A full `<svg>…</svg>` markup string, e.g.
56
- * `import x from './icon.svg?raw'`. Inlined inside a single `<span>` host.
57
- */
58
- svg?: string | undefined
59
- }
60
-
61
- /**
62
- * Render a loaded SVG — container-filling, theme-aware, props-transparent.
63
- *
64
- * @example
65
- * import Check from './check.svg?component'
66
- * <span style="width:2rem"><Icon as={Check} /></span>
67
- *
68
- * @example
69
- * import check from './check.svg?raw'
70
- * <span style="width:2rem"><Icon svg={check} /></span>
71
- */
72
- export function Icon(props: IconProps): VNodeChild {
73
- const [own, rest] = splitProps(props, ['as', 'svg'])
74
-
75
- // Component form — render the imported SVG directly, no host wrapper.
76
- // Defaults first so consumer `rest` (spread) overrides them; JSX spread
77
- // is reactivity-safe (compiler wraps it with `_wrapSpread`).
78
- if (own.as) {
79
- const As = own.as
80
- return <As fill="currentColor" style={FILL_STYLE} {...rest} />
81
- }
82
-
83
- // Raw-markup form — the string already contains its own <svg>, so we
84
- // inline it via a single <span> host. `dangerouslySetInnerHTML` last so
85
- // it can't be clobbered by a stray spread key.
86
- if (own.svg) {
87
- // svg-only props (`fill`, `viewBox`, …) are inapplicable to the host
88
- // span AND can't reach the opaque inlined markup — only host-level
89
- // attrs (`class`, `style`, `aria-*`, events) are meaningfully
90
- // forwardable here. Narrow the spread to the host's real surface.
91
- const hostRest = rest as unknown as PyreonHTMLAttributes<HTMLElement>
92
- return (
93
- <span
94
- style={FILL_STYLE}
95
- {...hostRest}
96
- dangerouslySetInnerHTML={{ __html: own.svg }}
97
- />
98
- )
99
- }
100
-
101
- return null
102
- }
103
-
104
- /**
105
- * Build a reusable icon component from a loaded svg — a markup string OR an
106
- * imported SVG component. The result is still just `<Icon>`, so it's
107
- * container-sizable + theme-aware with every prop passed through.
108
- *
109
- * @example
110
- * import check from './check.svg?raw'
111
- * export const Check = createIcon(check)
112
- *
113
- * import StarSvg from './star.svg?component'
114
- * export const Star = createIcon(StarSvg)
115
- *
116
- * // …sized + themed entirely by the consumer:
117
- * <span style="width:48px"><Check class="text-green-600" /></span>
118
- */
119
- export function createIcon(
120
- source: string | SvgComponent,
121
- ): (props: SvgAttributes) => VNodeChild {
122
- return (props: SvgAttributes) =>
123
- typeof source === 'string' ? (
124
- <Icon svg={source} {...props} />
125
- ) : (
126
- <Icon as={source} {...props} />
127
- )
128
- }
129
-
130
- // ─── createNamedIcon — typed icon-set runtime ────────────────────────────────
131
- //
132
- // The runtime half of `iconsPlugin`. The plugin scans a folder and writes a
133
- // generated file that calls this with a registry literal; `keyof typeof
134
- // REGISTRY` makes `name` a strict union (autocompletes, rejects typos) and
135
- // gives real go-to-definition — zero per-app wiring.
136
-
137
- /** How a named icon set renders each entry. */
138
- export type IconMode = 'inline' | 'image'
139
-
140
- /** Props of a component built by {@link createNamedIcon}. */
141
- export type NamedIconProps<R extends Record<string, string>> = {
142
- /** A name from the scanned set — strictly typed to the available files. */
143
- name: keyof R & string
144
- /** `<img>` alt text (image mode). Defaults to `""` (decorative). */
145
- alt?: string
146
- } & Omit<IconProps, 'as' | 'svg'>
147
-
148
- /**
149
- * Build a strictly-typed `<Icon name="…" />` from a name→source registry.
150
- *
151
- * - `mode: 'inline'` (default) — `source` is raw `<svg>` markup; rendered via
152
- * {@link Icon} so it's `currentColor`-themeable (system icons you recolor).
153
- * - `mode: 'image'` — `source` is an asset URL; rendered as `<img>` with NO
154
- * svg mutation, original colors preserved (colorful / brand icons).
155
- *
156
- * Either way it stays container-filling + props-transparent. Not called by
157
- * hand normally — `iconsPlugin` emits the generated file that calls it.
158
- *
159
- * @example
160
- * // icons.gen.tsx (auto-generated):
161
- * export const Icon = createNamedIcon({ 'check-circle': '<svg…' })
162
- * // app:
163
- * <span style="width:2rem"><Icon name="check-circle" /></span>
164
- */
165
- export function createNamedIcon<R extends Record<string, string>>(
166
- registry: R,
167
- options: { mode?: IconMode } = {},
168
- ): (props: NamedIconProps<R>) => VNodeChild {
169
- const mode = options.mode ?? 'inline'
170
- return (props: NamedIconProps<R>) => {
171
- const [own, rest] = splitProps(props, ['name', 'alt'])
172
- const source = registry[own.name]
173
- if (mode === 'image') {
174
- // svg-only props can't apply to an <img>; only host attrs forward.
175
- const hostRest = rest as unknown as PyreonHTMLAttributes<HTMLImageElement>
176
- return (
177
- <img src={source} alt={own.alt ?? ''} style={FILL_STYLE} {...hostRest} />
178
- )
179
- }
180
- return <Icon svg={source} {...rest} />
181
- }
182
- }
@@ -1,296 +0,0 @@
1
- import { existsSync, readdirSync } from 'node:fs'
2
- import { readFile, writeFile } from 'node:fs/promises'
3
- import { basename, dirname, join, relative } from 'node:path'
4
- import type { Plugin } from 'vite'
5
-
6
- import type { IconMode } from './icon'
7
-
8
- // ─── iconsPlugin — folder → strictly-typed icon set ─────────────────────────
9
- //
10
- // Point it at a folder of `*.svg` files; it writes a generated `icons.gen.tsx`
11
- // that exports a strictly-typed `<Icon name="…" />`. Add an svg → the `name`
12
- // union widens; remove one → a now-invalid `name` fails typecheck. The
13
- // generated file calls `createNamedIcon(REGISTRY)` so `keyof typeof REGISTRY`
14
- // IS the type surface (autocomplete + real go-to-definition, zero per-app
15
- // wiring — same one-touch shape as fs-router / islands auto-registry).
16
- //
17
- // Two render modes (per the colorful-vs-system split):
18
- // • mode: 'inline' (default) — system icons. Each svg inlined as raw markup;
19
- // `currentColor`-themeable, recolor via CSS `color`.
20
- // • mode: 'image' — colorful / brand icons. Each svg emitted as a
21
- // static asset, rendered `<img>`. NO mutation, original colors preserved.
22
- //
23
- // import { iconsPlugin } from '@pyreon/zero/server'
24
- // iconsPlugin({ dir: './src/icons' }) // → src/icons.gen.tsx
25
- // // app:
26
- // import { Icon } from './icons.gen'
27
- // <span style="width:2rem"><Icon name="check-circle" /></span>
28
-
29
- /** One named set in the multi-set form. */
30
- export interface IconSetConfig {
31
- /** Folder of `*.svg` files to scan for this set. */
32
- dir: string
33
- /**
34
- * `'inline'` (default — system icons, `currentColor`-themeable) or
35
- * `'image'` (colorful / brand icons, rendered `<img>`, no mutation).
36
- */
37
- mode?: IconMode
38
- }
39
-
40
- export interface IconsPluginConfig {
41
- /**
42
- * Single-set form: a folder of `*.svg` files → one `<Icon name="…" />`
43
- * with a single `IconName` union. Mutually exclusive with `sets`.
44
- */
45
- dir?: string
46
- /**
47
- * Named multi-set form: `{ ui: { dir }, brand: { dir, mode } }` → one
48
- * generated file exporting a strictly-typed component PER set with
49
- * NAMESPACED types so they never clash:
50
- * `ui` → `<UiIcon name="…" />` + `type UiIconName`
51
- * `brand` → `<BrandIcon name="…" />` + `type BrandIconName`
52
- * Mutually exclusive with `dir`.
53
- */
54
- sets?: Record<string, IconSetConfig>
55
- /**
56
- * Where to write the generated `.tsx`. Single-set default: `icons.gen.tsx`
57
- * next to `dir` (e.g. `src/icons` → `src/icons.gen.tsx`). Multi-set
58
- * default: `src/icons.gen.tsx` under the project root. Recommend
59
- * gitignoring it — it's a build artifact.
60
- */
61
- out?: string
62
- /** Single-set form only — render mode (`'inline'` default | `'image'`). */
63
- mode?: IconMode
64
- }
65
-
66
- /** Set key → exported component name. `ui` → `UiIcon`, `brand-marks` → `BrandMarksIcon`. */
67
- export function componentNameFromSetKey(key: string): string {
68
- const pascal = key
69
- .split(/[-_/\s]+/)
70
- .filter(Boolean)
71
- .map((p) => p.charAt(0).toUpperCase() + p.slice(1))
72
- .join('')
73
- const safe = pascal.replace(/[^A-Za-z0-9_$]/g, '')
74
- const base = /^[A-Za-z_$]/.test(safe) ? safe : `Set${safe}`
75
- return `${base}Icon`
76
- }
77
-
78
- /** Filename stem → registry key. `Check-Circle.svg` → `check-circle`. */
79
- export function iconNameFromFile(file: string): string {
80
- return basename(file, '.svg')
81
- .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
82
- .replace(/[\s_]+/g, '-')
83
- .toLowerCase()
84
- }
85
-
86
- /** Registry key → safe JS import binding. `check-circle` → `checkCircle`. */
87
- function bindingFromName(name: string): string {
88
- const camel = name.replace(/[-/](.)/g, (_, c: string) => c.toUpperCase())
89
- const safe = camel.replace(/[^A-Za-z0-9_$]/g, '_')
90
- return /^[A-Za-z_$]/.test(safe) ? safe : `_${safe}`
91
- }
92
-
93
- /** List the `*.svg` filenames in `dir` (sorted, stable). Empty if missing. */
94
- export function scanIconDir(dir: string): string[] {
95
- if (!existsSync(dir)) return []
96
- return readdirSync(dir)
97
- .filter((f) => f.toLowerCase().endsWith('.svg'))
98
- .sort()
99
- }
100
-
101
- /**
102
- * Render the generated `.tsx` source for a set of svg filenames. Pure —
103
- * unit-tested directly; the plugin only adds fs + watch around it.
104
- */
105
- export function generateIconSetSource(
106
- files: string[],
107
- opts: { mode: IconMode; importDir: string },
108
- ): string {
109
- const query = opts.mode === 'image' ? '' : '?raw'
110
- const seen = new Map<string, string>() // binding → name (collision guard)
111
- const entries: { key: string; binding: string; file: string }[] = []
112
- for (const file of files) {
113
- const key = iconNameFromFile(file)
114
- let binding = bindingFromName(key)
115
- while (seen.has(binding)) binding = `${binding}_`
116
- seen.set(binding, key)
117
- entries.push({ key, binding, file })
118
- }
119
-
120
- const header = [
121
- '// AUTO-GENERATED by @pyreon/zero iconsPlugin — do not edit.',
122
- `// Add / remove .svg files in ${opts.importDir} and this regenerates.`,
123
- '/// <reference types="vite/client" />',
124
- "import { createNamedIcon } from '@pyreon/zero'",
125
- ]
126
- const imports = entries.map(
127
- (e) => `import ${e.binding} from '${opts.importDir}/${e.file}${query}'`,
128
- )
129
- const registry = [
130
- 'const REGISTRY = {',
131
- ...entries.map((e) => ` ${JSON.stringify(e.key)}: ${e.binding},`),
132
- '} as const',
133
- ]
134
- const tail = [
135
- 'export type IconName = keyof typeof REGISTRY',
136
- `export const Icon = createNamedIcon(REGISTRY${
137
- opts.mode === 'image' ? ", { mode: 'image' }" : ''
138
- })`,
139
- '',
140
- ]
141
- return [...header, '', ...imports, '', ...registry, '', ...tail].join('\n')
142
- }
143
-
144
- /** One resolved set for the multi-set generator. */
145
- export interface NamedSetInput {
146
- /** Set key (`ui`) — becomes `<UiIcon>` + `type UiIconName`. */
147
- key: string
148
- files: string[]
149
- mode: IconMode
150
- /** Relative import dir from the generated file to this set's folder. */
151
- importDir: string
152
- }
153
-
154
- /**
155
- * Render the generated `.tsx` for the NAMED MULTI-SET form. One file, one
156
- * `createNamedIcon` import, one strictly-typed component PER set with
157
- * namespaced types (`UiIcon`/`UiIconName`, `BrandIcon`/`BrandIconName`) so
158
- * sets never clash. Bindings are per-set-prefixed so two sets sharing a
159
- * glyph filename don't collide.
160
- */
161
- export function generateNamedIconSetsSource(sets: NamedSetInput[]): string {
162
- const header = [
163
- '// AUTO-GENERATED by @pyreon/zero iconsPlugin — do not edit.',
164
- '// Add / remove .svg files in the configured set folders and this regenerates.',
165
- '/// <reference types="vite/client" />',
166
- "import { createNamedIcon } from '@pyreon/zero'",
167
- ]
168
- const blocks: string[] = []
169
- for (const set of sets) {
170
- const component = componentNameFromSetKey(set.key)
171
- const typeName = `${component}Name`
172
- const registry = `${component}_REGISTRY`
173
- const query = set.mode === 'image' ? '' : '?raw'
174
- const seen = new Set<string>()
175
- const entries: { key: string; binding: string; file: string }[] = []
176
- for (const file of set.files) {
177
- const k = iconNameFromFile(file)
178
- // Per-set binding prefix → no cross-set collision even on shared names.
179
- let binding = `${bindingFromName(set.key)}_${bindingFromName(k)}`
180
- while (seen.has(binding)) binding = `${binding}_`
181
- seen.add(binding)
182
- entries.push({ key: k, binding, file })
183
- }
184
- const imports = entries.map(
185
- (e) => `import ${e.binding} from '${set.importDir}/${e.file}${query}'`,
186
- )
187
- blocks.push(
188
- [
189
- `// ── set "${set.key}" → <${component} name="…" /> ──`,
190
- ...imports,
191
- `const ${registry} = {`,
192
- ...entries.map((e) => ` ${JSON.stringify(e.key)}: ${e.binding},`),
193
- '} as const',
194
- `export type ${typeName} = keyof typeof ${registry}`,
195
- `export const ${component} = createNamedIcon(${registry}${
196
- set.mode === 'image' ? ", { mode: 'image' }" : ''
197
- })`,
198
- ].join('\n'),
199
- )
200
- }
201
- return [...header, '', blocks.join('\n\n'), ''].join('\n')
202
- }
203
-
204
- function resolveOut(cfg: IconsPluginConfig, root: string): string {
205
- if (cfg.out) return join(root, cfg.out)
206
- if (cfg.dir) {
207
- const dir = join(root, cfg.dir)
208
- return join(dirname(dir), `${basename(dir)}.gen.tsx`)
209
- }
210
- // Multi-set form with no explicit `out`.
211
- return join(root, 'src', 'icons.gen.tsx')
212
- }
213
-
214
- /**
215
- * Vite plugin: scan `dir` for `*.svg`, write a strictly-typed
216
- * `icons.gen.tsx`, regenerate on add / unlink in dev.
217
- */
218
- export function iconsPlugin(cfg: IconsPluginConfig): Plugin {
219
- const hasDir = typeof cfg.dir === 'string'
220
- const hasSets = !!cfg.sets && Object.keys(cfg.sets).length > 0
221
- if (hasDir === hasSets) {
222
- throw new Error(
223
- '[Pyreon] iconsPlugin: provide EXACTLY ONE of `dir` (single set) or ' +
224
- '`sets` (named multi-set). ' +
225
- (hasDir
226
- ? 'Both were given.'
227
- : 'Neither was given (or `sets` is empty).'),
228
- )
229
- }
230
- let root = process.cwd()
231
- const mode: IconMode = cfg.mode ?? 'inline'
232
-
233
- /** Relative `./…` import dir from the generated file to a scanned folder. */
234
- function rel(out: string, scanned: string): string {
235
- const r = relative(dirname(out), scanned).split('\\').join('/')
236
- return r.startsWith('.') ? r : `./${r}`
237
- }
238
-
239
- async function regenerate(): Promise<void> {
240
- const out = resolveOut(cfg, root)
241
- let source: string
242
- if (hasSets) {
243
- const sets: NamedSetInput[] = Object.entries(cfg.sets ?? {}).map(
244
- ([key, sc]) => {
245
- const scanned = join(root, sc.dir)
246
- return {
247
- key,
248
- files: scanIconDir(scanned),
249
- mode: sc.mode ?? 'inline',
250
- importDir: rel(out, scanned),
251
- }
252
- },
253
- )
254
- source = generateNamedIconSetsSource(sets)
255
- } else {
256
- const scanned = join(root, cfg.dir as string)
257
- source = generateIconSetSource(scanIconDir(scanned), {
258
- mode,
259
- importDir: rel(out, scanned),
260
- })
261
- }
262
- // Idempotent — never rewrite identical content (avoids an HMR loop).
263
- const current = existsSync(out) ? await readFile(out, 'utf8') : null
264
- if (current !== source) await writeFile(out, source, 'utf8')
265
- }
266
-
267
- const watchDirs = (): string[] =>
268
- hasSets
269
- ? Object.values(cfg.sets ?? {}).map((s) => join(root, s.dir))
270
- : [join(root, cfg.dir as string)]
271
-
272
- return {
273
- name: 'pyreon:zero-icons',
274
- async configResolved(resolved) {
275
- root = resolved.root
276
- await regenerate()
277
- },
278
- async buildStart() {
279
- await regenerate()
280
- },
281
- configureServer(server) {
282
- const dirs = watchDirs()
283
- for (const d of dirs) server.watcher.add(d)
284
- const onChange = (file: string): void => {
285
- if (
286
- file.toLowerCase().endsWith('.svg') &&
287
- dirs.some((d) => file.startsWith(d))
288
- ) {
289
- void regenerate()
290
- }
291
- }
292
- server.watcher.on('add', onChange)
293
- server.watcher.on('unlink', onChange)
294
- },
295
- }
296
- }