@pyreon/zero 0.24.4 → 0.24.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -39
- package/src/actions.ts +0 -196
- package/src/adapters/bun.ts +0 -114
- package/src/adapters/cloudflare.ts +0 -166
- package/src/adapters/index.ts +0 -61
- package/src/adapters/netlify.ts +0 -154
- package/src/adapters/node.ts +0 -163
- package/src/adapters/static.ts +0 -42
- package/src/adapters/validate.ts +0 -23
- package/src/adapters/vercel.ts +0 -182
- package/src/adapters/warn-missing-env.ts +0 -49
- package/src/ai.ts +0 -623
- package/src/api-routes.ts +0 -219
- package/src/app.ts +0 -92
- package/src/cache.ts +0 -136
- package/src/client.ts +0 -143
- package/src/compression.ts +0 -116
- package/src/config.ts +0 -35
- package/src/cors.ts +0 -94
- package/src/csp.ts +0 -226
- package/src/entry-server.ts +0 -224
- package/src/env.ts +0 -344
- package/src/error-overlay.ts +0 -118
- package/src/favicon.ts +0 -841
- package/src/font.ts +0 -511
- package/src/fs-router.ts +0 -1519
- package/src/i18n-routing.ts +0 -533
- package/src/icon.tsx +0 -182
- package/src/icons-plugin.ts +0 -296
- package/src/image-plugin.ts +0 -751
- package/src/image-types.ts +0 -60
- package/src/image.tsx +0 -340
- package/src/index.ts +0 -92
- package/src/isr.ts +0 -394
- package/src/link.tsx +0 -304
- package/src/logger.ts +0 -144
- package/src/manifest.ts +0 -787
- package/src/meta.tsx +0 -354
- package/src/middleware.ts +0 -65
- package/src/not-found.ts +0 -44
- package/src/og-image.ts +0 -378
- package/src/rate-limit.ts +0 -140
- package/src/script.tsx +0 -260
- package/src/seo.ts +0 -617
- package/src/server.ts +0 -89
- package/src/sharp.d.ts +0 -22
- package/src/ssg-plugin.ts +0 -1582
- package/src/testing.ts +0 -146
- package/src/theme.tsx +0 -257
- package/src/types.ts +0 -624
- package/src/utils/use-intersection-observer.ts +0 -36
- package/src/utils/with-headers.ts +0 -13
- package/src/vercel-revalidate-handler.ts +0 -204
- package/src/vite-plugin.ts +0 -848
package/src/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
|
-
}
|
package/src/icons-plugin.ts
DELETED
|
@@ -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
|
-
}
|