@pyreon/styler 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 +5 -7
- package/src/ThemeProvider.ts +0 -65
- package/src/__tests__/ThemeProvider.test.ts +0 -67
- package/src/__tests__/benchmark.bench.ts +0 -200
- package/src/__tests__/composition-chain.test.ts +0 -537
- package/src/__tests__/css.test.ts +0 -70
- package/src/__tests__/dev-gate-treeshake.test.ts +0 -85
- package/src/__tests__/forward.test.ts +0 -282
- package/src/__tests__/globalStyle.test.ts +0 -72
- package/src/__tests__/hash.test.ts +0 -70
- package/src/__tests__/hybrid-injection.test.ts +0 -225
- package/src/__tests__/index.ts +0 -14
- package/src/__tests__/inject-rules.browser.test.ts +0 -40
- package/src/__tests__/insertion-effect.test.ts +0 -119
- package/src/__tests__/integration-dom.test.ts +0 -58
- package/src/__tests__/integration.test.ts +0 -179
- package/src/__tests__/keyframes.test.ts +0 -68
- package/src/__tests__/memory-growth.test.ts +0 -220
- package/src/__tests__/native-marker.test.ts +0 -9
- package/src/__tests__/p3-features.test.ts +0 -316
- package/src/__tests__/resolve-cache.test.ts +0 -94
- package/src/__tests__/resolve.test.ts +0 -308
- package/src/__tests__/shared.test.ts +0 -133
- package/src/__tests__/sheet-advanced.test.ts +0 -659
- package/src/__tests__/sheet-split-atrules.test.ts +0 -410
- package/src/__tests__/sheet.test.ts +0 -250
- package/src/__tests__/static-styler-resolve-cost.test.ts +0 -160
- package/src/__tests__/styled-reactive.test.ts +0 -74
- package/src/__tests__/styled-ssr.test.ts +0 -75
- package/src/__tests__/styled.test.ts +0 -511
- package/src/__tests__/styler.browser.test.tsx +0 -194
- package/src/__tests__/theme.test.ts +0 -33
- package/src/__tests__/useCSS.test.ts +0 -172
- package/src/css.ts +0 -13
- package/src/env.d.ts +0 -6
- package/src/forward.ts +0 -308
- package/src/globalStyle.ts +0 -53
- package/src/hash.ts +0 -28
- package/src/index.ts +0 -15
- package/src/keyframes.ts +0 -36
- package/src/manifest.ts +0 -332
- package/src/resolve.ts +0 -225
- package/src/shared.ts +0 -22
- package/src/sheet.ts +0 -635
- package/src/styled.tsx +0 -503
- package/src/tests/manifest-snapshot.test.ts +0 -51
- package/src/useCSS.ts +0 -20
package/src/manifest.ts
DELETED
|
@@ -1,332 +0,0 @@
|
|
|
1
|
-
import { defineManifest } from '@pyreon/manifest'
|
|
2
|
-
|
|
3
|
-
export default defineManifest({
|
|
4
|
-
name: '@pyreon/styler',
|
|
5
|
-
title: 'CSS-in-JS',
|
|
6
|
-
tagline:
|
|
7
|
-
'CSS-in-JS — styled() / css / keyframes / createGlobalStyle, reactive theming, FNV-1a-deduped StyleSheet (SSR-safe)',
|
|
8
|
-
description:
|
|
9
|
-
"Pyreon's CSS-in-JS engine. `styled('div')` is a tagged template that returns a `ComponentFn` injecting a generated class; `css` is a tagged template returning a LAZY `CSSResult` resolved on use (not a string); `keyframes` returns the generated animation-name string. Tagged-template interpolations receive the component's `props` (and the theme) so styles can be signal-driven — function interpolations flip the component onto the dynamic resolve path (`isDynamic`). A singleton `StyleSheet` with FNV-1a hashing dedupes and supports SSR; `createSheet()` makes an isolated instance. Theme is delivered through a REACTIVE context — `useTheme()` snapshots at call time, `useThemeAccessor()` returns the raw `() => Theme` accessor for tracking inside effects so whole-theme swaps re-resolve without remounting.",
|
|
10
|
-
category: 'browser',
|
|
11
|
-
features: [
|
|
12
|
-
"styled('div')`...` / styled(Component)`...` / styled.div`...` (Proxy) — component factory with `as` polymorphism + $-transient props",
|
|
13
|
-
'css`...` — lazy CSSResult, resolved on use (NOT a string)',
|
|
14
|
-
'keyframes`...` — returns the generated @keyframes animation-name string',
|
|
15
|
-
'createGlobalStyle`...` — returns a ComponentFn that injects global CSS when mounted',
|
|
16
|
-
'useCSS(template, props?, boost?) — resolve a CSSResult to a class name inside a component',
|
|
17
|
-
'Reactive theming — useTheme() snapshot vs useThemeAccessor() accessor; ThemeProvider merges nested',
|
|
18
|
-
'Singleton StyleSheet (FNV-1a dedup, SSR) + createSheet() for isolated instances',
|
|
19
|
-
'buildProps / filterProps — $-transient + shouldForwardProp DOM prop forwarding (descriptor-preserving)',
|
|
20
|
-
],
|
|
21
|
-
api: [
|
|
22
|
-
{
|
|
23
|
-
name: 'styled',
|
|
24
|
-
kind: 'function',
|
|
25
|
-
signature:
|
|
26
|
-
'styled: ((tag: Tag, options?: StyledOptions) => TagTemplateFn) & { div: TagTemplateFn; span: TagTemplateFn; /* …all HTML tags via Proxy */ }',
|
|
27
|
-
summary:
|
|
28
|
-
"Component factory. `styled('div')`, `styled(MyComp)`, and `styled.div` (Proxy sugar) are all tagged templates returning a `ComponentFn` that injects a generated class. Tagged-template interpolations are called with the live `props` object (theme included), so a function interpolation reading `p.theme.color` / signal-driven values works and puts the component on the dynamic resolve path. Supports the polymorphic `as` prop and `$`-prefixed TRANSIENT props (consumed by styles, NOT forwarded to the DOM). Per-definition caching keys generated classes so repeat mounts skip re-resolution.",
|
|
29
|
-
example: `import { styled } from "@pyreon/styler"
|
|
30
|
-
|
|
31
|
-
const Button = styled("button")\`
|
|
32
|
-
background: \${(p) => p.theme.colors.primary};
|
|
33
|
-
padding: \${(p) => (p.$compact ? "4px" : "12px")};
|
|
34
|
-
\`
|
|
35
|
-
// <Button $compact onClick={...}>Go</Button> — $compact not forwarded to <button>`,
|
|
36
|
-
mistakes: [
|
|
37
|
-
"Expecting `$`-prefixed props to reach the DOM — they are transient by design (consumed by the template, stripped before forwarding). Use a non-`$` name if the attribute must land on the element",
|
|
38
|
-
'Destructuring `props` in the interpolation (`${({ theme }) => …}`) and being surprised it does not update on a whole-theme swap — read `props.theme` lazily; the theme context is reactive and the styled resolver re-runs on swap',
|
|
39
|
-
'Passing a resolved value where a function interpolation is needed for reactivity — `${signal()}` snapshots once at definition; use `${() => signal()}` (or `${(p) => p.x}`) to stay on the dynamic path',
|
|
40
|
-
'Using `styled.div` and expecting a different identity per call — the Proxy returns the same tag template fn shape; per-definition caches key on the template, not the call site',
|
|
41
|
-
],
|
|
42
|
-
seeAlso: ['css', 'useCSS', 'useTheme'],
|
|
43
|
-
},
|
|
44
|
-
{
|
|
45
|
-
name: 'css',
|
|
46
|
-
kind: 'function',
|
|
47
|
-
signature:
|
|
48
|
-
'css(strings: TemplateStringsArray, ...values: Interpolation[]): CSSResult',
|
|
49
|
-
summary:
|
|
50
|
-
'Tagged-template that returns a LAZY `CSSResult` — it is NOT a class name or a CSS string until resolved by `styled()`, `useCSS()`, or composition into another template. Compose reusable fragments with it (assign a `css` result to `const base`, then interpolate `base` inside a `styled` template). Resolution is deferred so it can read the props/theme of the consuming component at use time.',
|
|
51
|
-
example: `import { css, useCSS } from "@pyreon/styler"
|
|
52
|
-
|
|
53
|
-
const card = css\`border: 1px solid #ddd; padding: 16px;\`
|
|
54
|
-
function Card(props) {
|
|
55
|
-
const cls = useCSS(card)
|
|
56
|
-
return <div class={cls}>{props.children}</div>
|
|
57
|
-
}`,
|
|
58
|
-
mistakes: [
|
|
59
|
-
'Treating the `css` tagged-template return value as a string / class name — it is a lazy `CSSResult`; interpolating it into text (e.g. `class={card}`) renders `[object Object]`. Resolve via `useCSS` or embed in a `styled` template',
|
|
60
|
-
'Reading props/theme at `css` call time — the template is resolved later; put dynamic bits in function interpolations so they read the LIVE props at use',
|
|
61
|
-
],
|
|
62
|
-
seeAlso: ['styled', 'useCSS', 'keyframes'],
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
name: 'keyframes',
|
|
66
|
-
kind: 'function',
|
|
67
|
-
signature:
|
|
68
|
-
'keyframes(strings: TemplateStringsArray, ...values: Interpolation[]): KeyframesResult',
|
|
69
|
-
summary:
|
|
70
|
-
'Tagged-template returning a `KeyframesResult` whose string form is the GENERATED, content-hashed `@keyframes` animation NAME. Reference it inside a `css` / `styled` template as the `animation-name` value; the `@keyframes` rule is injected (deduped via FNV-1a) on first use.',
|
|
71
|
-
example: `import { keyframes, styled } from "@pyreon/styler"
|
|
72
|
-
|
|
73
|
-
const spin = keyframes\`from { transform: rotate(0) } to { transform: rotate(360deg) }\`
|
|
74
|
-
const Spinner = styled("div")\`animation: \${spin} 1s linear infinite;\``,
|
|
75
|
-
mistakes: [
|
|
76
|
-
'Expecting a CSS class — `keyframes` yields an animation-NAME token, used as the `animation` / `animation-name` value, not a class applied to an element',
|
|
77
|
-
'Defining `keyframes` inside the render body per mount — define once at module scope so the hashed rule is injected once and reused',
|
|
78
|
-
],
|
|
79
|
-
seeAlso: ['css', 'styled'],
|
|
80
|
-
},
|
|
81
|
-
{
|
|
82
|
-
name: 'createGlobalStyle',
|
|
83
|
-
kind: 'function',
|
|
84
|
-
signature:
|
|
85
|
-
'createGlobalStyle(strings: TemplateStringsArray, ...values: Interpolation[]): ComponentFn',
|
|
86
|
-
summary:
|
|
87
|
-
'Returns a `ComponentFn` that injects GLOBAL CSS (resets, `:root` tokens, body styles) when MOUNTED — it is not a side-effecting call. Render the returned component once near the app root; unmounting removes the global rule. Function interpolations make the global block dynamic (re-resolves on prop/theme change).',
|
|
88
|
-
example: `import { createGlobalStyle } from "@pyreon/styler"
|
|
89
|
-
|
|
90
|
-
const GlobalReset = createGlobalStyle\`
|
|
91
|
-
*, *::before, *::after { box-sizing: border-box }
|
|
92
|
-
body { margin: 0; font-family: \${(p) => p.theme.fonts.body}; }
|
|
93
|
-
\`
|
|
94
|
-
// render <GlobalReset /> once at the app root`,
|
|
95
|
-
mistakes: [
|
|
96
|
-
'Calling `createGlobalStyle` (the tagged template) and expecting the CSS to inject — nothing happens until the returned component is RENDERED. Mount `<GlobalReset />` once near the root',
|
|
97
|
-
'Mounting it in many components — duplicates the global rule lifetime management; mount exactly once',
|
|
98
|
-
],
|
|
99
|
-
seeAlso: ['styled', 'css'],
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
name: 'useCSS',
|
|
103
|
-
kind: 'hook',
|
|
104
|
-
signature:
|
|
105
|
-
'useCSS(template: CSSResult, props?: Record<string, any>, boost?: boolean): string',
|
|
106
|
-
summary:
|
|
107
|
-
'Resolves a `CSSResult` (from the `css` tagged template) to an injected class-name string inside a component. Pass `props` so function interpolations in the template read live values; `boost` opts into a faster cache path for hot, stable templates. The returned class is deduped/hashed by the active `StyleSheet`.',
|
|
108
|
-
example: `import { css, useCSS } from "@pyreon/styler"
|
|
109
|
-
|
|
110
|
-
const box = css\`color: \${(p) => p.danger ? "red" : "inherit"};\`
|
|
111
|
-
function Box(props) {
|
|
112
|
-
return <div class={useCSS(box, props)}>{props.children}</div>
|
|
113
|
-
}`,
|
|
114
|
-
mistakes: [
|
|
115
|
-
'Forgetting to pass `props` when the template has function interpolations — they then resolve against an empty object and the dynamic values are lost',
|
|
116
|
-
'Calling `useCSS` outside a component setup — it depends on the active sheet/theme context like any hook',
|
|
117
|
-
],
|
|
118
|
-
seeAlso: ['css', 'styled'],
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
name: 'useTheme',
|
|
122
|
-
kind: 'hook',
|
|
123
|
-
signature: 'useTheme<T extends object = Theme>(): T',
|
|
124
|
-
summary:
|
|
125
|
-
'Returns the current theme as a SNAPSHOT at call time. `ThemeContext` is a REACTIVE context — `useTheme()` reads it once, so the returned object is static unless the read happens inside a reactive scope. For values that must track whole-theme swaps inside an `effect` / `computed`, use `useThemeAccessor()` instead.',
|
|
126
|
-
example: `import { useTheme } from "@pyreon/styler"
|
|
127
|
-
|
|
128
|
-
function Badge() {
|
|
129
|
-
const t = useTheme()
|
|
130
|
-
return <span style={{ color: t.colors.primary }}>{/* … */}</span>
|
|
131
|
-
}`,
|
|
132
|
-
mistakes: [
|
|
133
|
-
'Destructuring `const { colors } = useTheme()` and expecting it to update on a user-preference theme swap — the snapshot is captured once. Use `useThemeAccessor()` and read inside the reactive scope, or rely on `styled` templates (their resolver tracks the theme)',
|
|
134
|
-
'Calling `useTheme()` at module scope — it must run during component setup where the context is available',
|
|
135
|
-
],
|
|
136
|
-
seeAlso: ['useThemeAccessor', 'ThemeProvider', 'styled'],
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
name: 'useThemeAccessor',
|
|
140
|
-
kind: 'hook',
|
|
141
|
-
signature: 'useThemeAccessor<T extends object = Theme>(): () => T',
|
|
142
|
-
summary:
|
|
143
|
-
'Returns the raw `() => T` theme accessor (not a snapshot). Call it inside an `effect` / `computed` / JSX thunk so the read TRACKS the reactive theme context — whole-theme swaps (user-preference themes) then re-run the consumer without a remount. This is the escape hatch `styled()` itself uses internally.',
|
|
144
|
-
example: `import { useThemeAccessor } from "@pyreon/styler"
|
|
145
|
-
import { effect } from "@pyreon/reactivity"
|
|
146
|
-
|
|
147
|
-
const theme = useThemeAccessor()
|
|
148
|
-
effect(() => applyChartPalette(theme().colors)) // re-runs on theme swap`,
|
|
149
|
-
mistakes: [
|
|
150
|
-
'Calling the accessor once at setup and caching the result — that defeats the point; call it INSIDE the reactive scope every time so the dependency is tracked',
|
|
151
|
-
'Reaching for this when a `styled` template would do — the template resolver already tracks the theme; use the accessor only for imperative/non-CSS theme reads',
|
|
152
|
-
],
|
|
153
|
-
seeAlso: ['useTheme', 'ThemeProvider'],
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
name: 'ThemeProvider',
|
|
157
|
-
kind: 'component',
|
|
158
|
-
signature:
|
|
159
|
-
'ThemeProvider(props: { theme: Theme | ((parent: Theme) => Theme); children?: VNodeChild }): VNodeChild',
|
|
160
|
-
summary:
|
|
161
|
-
'Provides a theme to the reactive `ThemeContext`. Nested providers compose — a function `theme` receives the parent theme so subtrees can extend rather than replace. Because the context is reactive, swapping the `theme` prop re-resolves every `styled` / `useCSS` consumer below without remounting the tree. Marked `nativeCompat` so it works inside `@pyreon/{react,preact,vue,solid}-compat` apps.',
|
|
162
|
-
example: `import { ThemeProvider } from "@pyreon/styler"
|
|
163
|
-
|
|
164
|
-
<ThemeProvider theme={{ colors: { primary: "#06f" } }}>
|
|
165
|
-
<App />
|
|
166
|
-
</ThemeProvider>`,
|
|
167
|
-
mistakes: [
|
|
168
|
-
'Replacing the whole theme in a nested provider when you meant to extend — pass `theme={(parent) => ({ ...parent, colors: { ...parent.colors, accent: "#0a0" } })}`',
|
|
169
|
-
'Expecting most apps to mount this directly — `<PyreonUI>` wraps it; use `ThemeProvider` standalone only outside the `@pyreon/ui-core` provider',
|
|
170
|
-
],
|
|
171
|
-
seeAlso: ['useTheme', 'useThemeAccessor', 'ThemeContext'],
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
name: 'ThemeContext',
|
|
175
|
-
kind: 'constant',
|
|
176
|
-
signature: 'ThemeContext: ReactiveContext<Theme>',
|
|
177
|
-
summary:
|
|
178
|
-
'The reactive context backing the theme. Created via `createReactiveContext<Theme>` — `useContext(ThemeContext)` returns a `() => Theme` accessor (which is what `useTheme()` / `useThemeAccessor()` wrap). Exposed for advanced consumers building their own theme-aware primitives; prefer the hooks for app code.',
|
|
179
|
-
example: `import { ThemeContext } from "@pyreon/styler"
|
|
180
|
-
import { useContext } from "@pyreon/core"
|
|
181
|
-
|
|
182
|
-
const themeAccessor = useContext(ThemeContext) // () => Theme`,
|
|
183
|
-
mistakes: [
|
|
184
|
-
'Treating `useContext(ThemeContext)` as the theme object — it is the ACCESSOR `() => Theme` (reactive context). Call it to read',
|
|
185
|
-
],
|
|
186
|
-
seeAlso: ['useTheme', 'useThemeAccessor', 'ThemeProvider'],
|
|
187
|
-
},
|
|
188
|
-
{
|
|
189
|
-
name: 'createSheet',
|
|
190
|
-
kind: 'function',
|
|
191
|
-
signature: 'createSheet(options?: StyleSheetOptions): StyleSheet',
|
|
192
|
-
summary:
|
|
193
|
-
'Creates an ISOLATED `StyleSheet` instance (its own FNV-1a dedup cache + rule registry) instead of the shared singleton `sheet`. Use for shadow-DOM roots, multi-window/iframe rendering, or test isolation where one request/realm must not share the global dedup cache. Most apps never need this — the singleton is correct for a single document.',
|
|
194
|
-
example: `import { createSheet } from "@pyreon/styler"
|
|
195
|
-
|
|
196
|
-
const shadowSheet = createSheet({ /* StyleSheetOptions */ })`,
|
|
197
|
-
mistakes: [
|
|
198
|
-
'Creating a fresh sheet per render — defeats dedup; create once per realm/root and reuse',
|
|
199
|
-
'Mixing the singleton and an isolated sheet for the same DOM — classes from one will not be deduped against the other; pick one per document root',
|
|
200
|
-
],
|
|
201
|
-
seeAlso: ['StyleSheet', 'sheet'],
|
|
202
|
-
},
|
|
203
|
-
{
|
|
204
|
-
name: 'StyleSheet',
|
|
205
|
-
kind: 'class',
|
|
206
|
-
signature: 'class StyleSheet { constructor(options?: StyleSheetOptions) }',
|
|
207
|
-
summary:
|
|
208
|
-
'The CSS injection engine: FNV-1a content hashing, a dedup cache (identical CSS → one rule), and SSR support (collect rules to a string on the server, hydrate on the client). `sheet` is the process singleton; `createSheet()` wraps `new StyleSheet()`. Direct instantiation is for custom integrations (server frameworks collecting critical CSS, test harnesses).',
|
|
209
|
-
example: `import { StyleSheet } from "@pyreon/styler"
|
|
210
|
-
|
|
211
|
-
const s = new StyleSheet({ /* options */ })`,
|
|
212
|
-
mistakes: [
|
|
213
|
-
'Instantiating `new StyleSheet()` in app code — use the exported `sheet` singleton (or `createSheet()` for explicit isolation); a stray instance will not be where `styled()` injects',
|
|
214
|
-
],
|
|
215
|
-
seeAlso: ['createSheet', 'sheet'],
|
|
216
|
-
},
|
|
217
|
-
{
|
|
218
|
-
name: 'sheet',
|
|
219
|
-
kind: 'constant',
|
|
220
|
-
signature: 'sheet: StyleSheet',
|
|
221
|
-
summary:
|
|
222
|
-
'The process-wide singleton `StyleSheet` that `styled()` / `css` / `keyframes` / `createGlobalStyle` inject into by default. Read it for SSR critical-CSS extraction or debugging the rule registry; do not mutate it directly.',
|
|
223
|
-
example: `import { sheet } from "@pyreon/styler"
|
|
224
|
-
// SSR: render the app, then read the collected rules off \`sheet\` for the <head>`,
|
|
225
|
-
seeAlso: ['StyleSheet', 'createSheet'],
|
|
226
|
-
},
|
|
227
|
-
{
|
|
228
|
-
name: 'resolve',
|
|
229
|
-
kind: 'function',
|
|
230
|
-
signature:
|
|
231
|
-
'resolve(strings: TemplateStringsArray, values: Interpolation[], props: Record<string, any>): string',
|
|
232
|
-
summary:
|
|
233
|
-
'Low-level: resolve a tagged-template (strings + interpolations) against a `props` object into a final CSS string (function interpolations invoked with `props`). The engine `styled()` / `useCSS` build on. Direct use is for custom CSS-in-JS layered on top of styler; app code should prefer `styled` / `css`.',
|
|
234
|
-
example: `import { resolve } from "@pyreon/styler"
|
|
235
|
-
|
|
236
|
-
const cssText = resolve(strings, values, { theme, $compact: true })`,
|
|
237
|
-
seeAlso: ['normalizeCSS', 'resolveValue', 'styled'],
|
|
238
|
-
},
|
|
239
|
-
{
|
|
240
|
-
name: 'normalizeCSS',
|
|
241
|
-
kind: 'function',
|
|
242
|
-
signature: 'normalizeCSS(css: string): string',
|
|
243
|
-
summary:
|
|
244
|
-
'Normalizes a raw CSS string (whitespace/format canonicalization) so identical-intent CSS hashes to the same FNV-1a key and dedupes. Memoized via an internal cache — call `clearNormCache()` to drop it (tests / long-lived processes).',
|
|
245
|
-
example: `import { normalizeCSS } from "@pyreon/styler"
|
|
246
|
-
|
|
247
|
-
normalizeCSS("color: red ;") // canonical form, dedup-stable`,
|
|
248
|
-
seeAlso: ['clearNormCache', 'resolve'],
|
|
249
|
-
},
|
|
250
|
-
{
|
|
251
|
-
name: 'resolveValue',
|
|
252
|
-
kind: 'function',
|
|
253
|
-
signature:
|
|
254
|
-
'resolveValue(value: Interpolation, props: Record<string, any>): string',
|
|
255
|
-
summary:
|
|
256
|
-
'Resolves a SINGLE interpolation against `props`: invokes function interpolations with `props`, flattens nested `CSSResult` / `KeyframesResult`, and stringifies the result. The per-interpolation primitive `resolve()` loops over.',
|
|
257
|
-
example: `import { resolveValue } from "@pyreon/styler"
|
|
258
|
-
|
|
259
|
-
resolveValue((p) => p.theme.colors.primary, { theme })`,
|
|
260
|
-
seeAlso: ['resolve', 'isDynamic'],
|
|
261
|
-
},
|
|
262
|
-
{
|
|
263
|
-
name: 'clearNormCache',
|
|
264
|
-
kind: 'function',
|
|
265
|
-
signature: 'clearNormCache(): void',
|
|
266
|
-
summary:
|
|
267
|
-
'Clears the `normalizeCSS` memo cache. Needed in test suites that assert on injection counts / sheet contents across cases, and in long-lived processes that churn unique CSS and want to bound the cache. No effect on already-injected rules.',
|
|
268
|
-
example: `import { clearNormCache } from "@pyreon/styler"
|
|
269
|
-
|
|
270
|
-
afterEach(() => clearNormCache())`,
|
|
271
|
-
seeAlso: ['normalizeCSS'],
|
|
272
|
-
},
|
|
273
|
-
{
|
|
274
|
-
name: 'buildProps',
|
|
275
|
-
kind: 'function',
|
|
276
|
-
signature:
|
|
277
|
-
'buildProps(rawProps: Record<string, any>, generatedCls: string, isDOM: boolean, customFilter?: (prop: string) => boolean): Record<string, any>',
|
|
278
|
-
summary:
|
|
279
|
-
"Builds the final prop object forwarded to the rendered element: merges the generated class, drops `$`-transient props, and (for DOM targets) filters non-DOM attributes — `customFilter` overrides per-component. **Copies DESCRIPTORS, not values**, so compiler-emitted reactive (`_rp` getter) props survive forwarding instead of collapsing to a static snapshot.",
|
|
280
|
-
example: `import { buildProps } from "@pyreon/styler"
|
|
281
|
-
|
|
282
|
-
const forwarded = buildProps(rawProps, "sc-abc123", true)`,
|
|
283
|
-
mistakes: [
|
|
284
|
-
'Re-implementing prop forwarding with `result[key] = source[key]` — that fires getters and freezes reactive props to a one-time value. styler uses descriptor copy specifically to preserve the `_rp` getter contract; any custom forwarder must do the same',
|
|
285
|
-
'Passing `isDOM: true` for a component target — DOM-attr filtering will strip props the wrapped component legitimately needs',
|
|
286
|
-
],
|
|
287
|
-
seeAlso: ['filterProps', 'styled'],
|
|
288
|
-
},
|
|
289
|
-
{
|
|
290
|
-
name: 'filterProps',
|
|
291
|
-
kind: 'function',
|
|
292
|
-
signature:
|
|
293
|
-
'filterProps(props: Record<string, unknown>): Record<string, unknown>',
|
|
294
|
-
summary:
|
|
295
|
-
'Returns a copy of `props` with `$`-transient and known non-DOM props removed — the DOM-safety filter `buildProps` applies for element targets. Exposed for consumers doing their own forwarding who still want the styler allowlist semantics. Descriptor-preserving, same reactive-prop rationale as `buildProps`.',
|
|
296
|
-
example: `import { filterProps } from "@pyreon/styler"
|
|
297
|
-
|
|
298
|
-
const domSafe = filterProps(props)`,
|
|
299
|
-
seeAlso: ['buildProps'],
|
|
300
|
-
},
|
|
301
|
-
{
|
|
302
|
-
name: 'isDynamic',
|
|
303
|
-
kind: 'function',
|
|
304
|
-
signature: 'isDynamic(v: Interpolation): boolean',
|
|
305
|
-
summary:
|
|
306
|
-
'True when an interpolation is a function (signal accessor / props reader) — i.e. the styled component must take the DYNAMIC resolve path (re-resolve per prop/theme change) rather than the static cached path. Used internally to decide the resolver branch; exported for tooling that mirrors that decision.',
|
|
307
|
-
example: `import { isDynamic } from "@pyreon/styler"
|
|
308
|
-
|
|
309
|
-
isDynamic((p) => p.color) // true → dynamic path
|
|
310
|
-
isDynamic("12px") // false → static, cached`,
|
|
311
|
-
seeAlso: ['resolve', 'styled'],
|
|
312
|
-
},
|
|
313
|
-
],
|
|
314
|
-
gotchas: [
|
|
315
|
-
{
|
|
316
|
-
label: 'css / keyframes return lazy values, not strings',
|
|
317
|
-
note: 'The `css` tagged template yields a `CSSResult` (resolved on use); `keyframes` stringifies to an animation NAME; `createGlobalStyle` returns a `ComponentFn` that must be MOUNTED. None of them inject CSS at call time — only on resolution/mount.',
|
|
318
|
-
},
|
|
319
|
-
{
|
|
320
|
-
label: 'Theme context is reactive',
|
|
321
|
-
note: '`useTheme()` is a snapshot; `useThemeAccessor()` is the tracking accessor. `styled` / `useCSS` templates already track the theme through their resolver, so whole-theme swaps re-resolve CSS + swap class names WITHOUT remounting the VNode. Destructuring `useTheme()` and reading outside a reactive scope freezes the value.',
|
|
322
|
-
},
|
|
323
|
-
{
|
|
324
|
-
label: 'Prop forwarding copies descriptors',
|
|
325
|
-
note: '`buildProps` / `filterProps` copy property DESCRIPTORS (not values) so compiler-emitted `_rp` getter props keep their reactive subscription end-to-end. Any custom prop-forwarding wrapper layered on styler MUST do the same — plain `result[k] = src[k]` silently collapses signal-driven props to a one-time snapshot.',
|
|
326
|
-
},
|
|
327
|
-
{
|
|
328
|
-
label: 'Singleton sheet by default',
|
|
329
|
-
note: 'All injection goes through the `sheet` singleton (FNV-1a dedup, SSR). `createSheet()` / `new StyleSheet()` are only for isolated realms (shadow DOM, iframes, test isolation) — mixing sheets for one document breaks dedup.',
|
|
330
|
-
},
|
|
331
|
-
],
|
|
332
|
-
})
|
package/src/resolve.ts
DELETED
|
@@ -1,225 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Interpolation resolver: converts tagged template strings + values into a
|
|
3
|
-
* final CSS string. Handles nested CSSResults, arrays, functions, and
|
|
4
|
-
* primitive values.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import type { DefaultTheme } from './ThemeProvider'
|
|
8
|
-
|
|
9
|
-
// Dev-time counter sink — populated by `@pyreon/perf-harness` on install().
|
|
10
|
-
// Guarded on call sites with `process.env.NODE_ENV !== 'production'` so prod bundles
|
|
11
|
-
// tree-shake the entire reference. No cross-package import, no publish surface.
|
|
12
|
-
const _countSink = globalThis as { __pyreon_count__?: (name: string, n?: number) => void }
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Props passed to interpolation functions inside tagged templates.
|
|
16
|
-
* Generic `P` allows consumers to type their custom props (e.g. transient $-prefixed):
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* styled('div')<{ $color: string }>`
|
|
20
|
-
* background: ${(props) => props.$color}; // props.$color is typed!
|
|
21
|
-
* `
|
|
22
|
-
*/
|
|
23
|
-
export type StyledProps<P extends object = Record<string, unknown>> = P & {
|
|
24
|
-
theme?: DefaultTheme & Record<string, unknown>
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export type Interpolation<P extends object = Record<string, unknown>> =
|
|
28
|
-
| string
|
|
29
|
-
| number
|
|
30
|
-
| boolean
|
|
31
|
-
| null
|
|
32
|
-
| undefined
|
|
33
|
-
| CSSResult
|
|
34
|
-
| Interpolation<P>[]
|
|
35
|
-
| ((props: StyledProps<P>) => Interpolation<P>)
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Lazy representation of a `css` tagged template. Stores the raw template
|
|
39
|
-
* strings and interpolation values without resolving them. Resolution is
|
|
40
|
-
* deferred until a styled component renders (or until explicitly resolved).
|
|
41
|
-
*/
|
|
42
|
-
export class CSSResult {
|
|
43
|
-
/**
|
|
44
|
-
* Memoized result of `isDynamic(this)`. Populated on first access from
|
|
45
|
-
* `shared.ts#isDynamic`. CSSResult instances are typically created once
|
|
46
|
-
* at module load (one per `css\`...\`` literal) and reused everywhere —
|
|
47
|
-
* a `styled()` component, a `useCSS` consumer, a nested interpolation,
|
|
48
|
-
* etc. Lazy-cache avoids rescanning whole sub-trees on every consumer.
|
|
49
|
-
* Ported from vitus-labs `c483cabc`.
|
|
50
|
-
*/
|
|
51
|
-
_isDynamic: boolean | undefined = undefined
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Memoized resolved CSS string for STATIC CSSResults — populated by
|
|
55
|
-
* `resolveValue` the first time a known-static nested CSSResult is
|
|
56
|
-
* resolved. Safe because props don't affect output when there are no
|
|
57
|
-
* function interpolations. Skipped for dynamic CSSResults (the resolved
|
|
58
|
-
* string depends on props each call). Common pattern: a shared static
|
|
59
|
-
* snippet interpolated into many dynamic components — pre-cache, that
|
|
60
|
-
* snippet was re-resolved per-render of every consumer. Ported from
|
|
61
|
-
* vitus-labs `754cd203`; measured upstream: 2.6M→6.5M ops/s (+149%,
|
|
62
|
-
* ~2.5× speedup on the 8-repeated-resolve micro).
|
|
63
|
-
*/
|
|
64
|
-
_staticResolved: string | undefined = undefined
|
|
65
|
-
|
|
66
|
-
constructor(
|
|
67
|
-
readonly strings: TemplateStringsArray,
|
|
68
|
-
readonly values: Interpolation[],
|
|
69
|
-
) {}
|
|
70
|
-
|
|
71
|
-
/** Resolve with empty props — useful for static templates, testing, and debugging. */
|
|
72
|
-
toString(): string {
|
|
73
|
-
return resolve(this.strings, this.values, {})
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/** Resolve a tagged template's strings + values into a final CSS string. */
|
|
78
|
-
export const resolve = (
|
|
79
|
-
strings: TemplateStringsArray,
|
|
80
|
-
values: Interpolation[],
|
|
81
|
-
props: Record<string, any>,
|
|
82
|
-
): string => {
|
|
83
|
-
if (process.env.NODE_ENV !== 'production') _countSink.__pyreon_count__?.('styler.resolve')
|
|
84
|
-
// Tagged templates guarantee strings.length === values.length + 1,
|
|
85
|
-
// so strings[0] and strings[i+1] are always defined — no ?? needed.
|
|
86
|
-
let result = strings[0] as string
|
|
87
|
-
for (let i = 0; i < values.length; i++) {
|
|
88
|
-
const v = values[i]
|
|
89
|
-
const s = strings[i + 1] as string
|
|
90
|
-
// Inline the most common value types to avoid function call overhead.
|
|
91
|
-
if (typeof v === 'function') {
|
|
92
|
-
const r = v(props)
|
|
93
|
-
result +=
|
|
94
|
-
(typeof r === 'string'
|
|
95
|
-
? r
|
|
96
|
-
: r == null || r === false || r === true
|
|
97
|
-
? ''
|
|
98
|
-
: resolveValue(r as Interpolation, props)) + s
|
|
99
|
-
} else if (v == null || v === false || v === true) {
|
|
100
|
-
result += s
|
|
101
|
-
} else if (typeof v === 'string') {
|
|
102
|
-
result += v + s
|
|
103
|
-
} else if (typeof v === 'number') {
|
|
104
|
-
result += v + s
|
|
105
|
-
} else {
|
|
106
|
-
result += resolveValue(v, props) + s
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
return result
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Normalize resolved CSS text for strict `insertRule` compatibility.
|
|
114
|
-
*
|
|
115
|
-
* Single-pass scanner that handles all cleanup in one traversal:
|
|
116
|
-
* - Strips block comments and line comments (preserves :// in URLs)
|
|
117
|
-
* - Collapses whitespace to single spaces
|
|
118
|
-
* - Removes redundant semicolons
|
|
119
|
-
* - Trims leading/trailing whitespace
|
|
120
|
-
*/
|
|
121
|
-
const normCache = new Map<string, string>()
|
|
122
|
-
/** Clear the normalizeCSS cache (called during HMR cleanup). */
|
|
123
|
-
export const clearNormCache = () => normCache.clear()
|
|
124
|
-
|
|
125
|
-
export const normalizeCSS = (css: string): string => {
|
|
126
|
-
const cached = normCache.get(css)
|
|
127
|
-
if (cached !== undefined) return cached
|
|
128
|
-
|
|
129
|
-
const len = css.length
|
|
130
|
-
let out = ''
|
|
131
|
-
let space = false // pending space to emit before next non-whitespace char
|
|
132
|
-
let last = 0 // charCode of last char written to output (0 = nothing yet)
|
|
133
|
-
|
|
134
|
-
for (let i = 0; i < len; i++) {
|
|
135
|
-
const c = css.charCodeAt(i)
|
|
136
|
-
|
|
137
|
-
// /* block comment */
|
|
138
|
-
if (c === 47 /* / */ && css.charCodeAt(i + 1) === 42 /* * */) {
|
|
139
|
-
const end = css.indexOf('*/', i + 2)
|
|
140
|
-
i = end === -1 ? len : end + 1
|
|
141
|
-
space = true
|
|
142
|
-
continue
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// // line comment (but not :// in URLs)
|
|
146
|
-
if (c === 47 /* / */ && css.charCodeAt(i + 1) === 47 /* / */ && last !== 58 /* : */) {
|
|
147
|
-
const nl = css.indexOf('\n', i + 2)
|
|
148
|
-
i = nl === -1 ? len : nl
|
|
149
|
-
space = true
|
|
150
|
-
continue
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// Whitespace → collapse
|
|
154
|
-
if (c === 32 || c === 9 || c === 10 || c === 13 || c === 12) {
|
|
155
|
-
space = true
|
|
156
|
-
continue
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Semicolon → skip if redundant (after start, {, }, or another ;)
|
|
160
|
-
if (c === 59 /* ; */) {
|
|
161
|
-
if (last === 0 || last === 123 /* { */ || last === 125 /* } */ || last === 59 /* ; */) {
|
|
162
|
-
continue
|
|
163
|
-
}
|
|
164
|
-
space = false
|
|
165
|
-
out += ';'
|
|
166
|
-
last = 59
|
|
167
|
-
continue
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// Regular char — emit pending space (but not at start of output)
|
|
171
|
-
if (space && last !== 0) out += ' '
|
|
172
|
-
space = false
|
|
173
|
-
|
|
174
|
-
out += css[i]
|
|
175
|
-
last = c
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Evict oldest ~10% to prevent memory leaks without cliff-edge drop
|
|
179
|
-
if (normCache.size > 2000) {
|
|
180
|
-
let count = 0
|
|
181
|
-
for (const key of normCache.keys()) {
|
|
182
|
-
if (count >= 200) break
|
|
183
|
-
normCache.delete(key)
|
|
184
|
-
count++
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
normCache.set(css, out)
|
|
188
|
-
|
|
189
|
-
return out
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export const resolveValue = (value: Interpolation, props: Record<string, any>): string => {
|
|
193
|
-
// null, undefined, false, true → empty (enables conditional: ${cond && css`...`})
|
|
194
|
-
if (value == null || value === false || value === true) return ''
|
|
195
|
-
|
|
196
|
-
// function interpolation → call with props/theme context, resolve result
|
|
197
|
-
if (typeof value === 'function') return resolveValue(value(props) as Interpolation, props)
|
|
198
|
-
|
|
199
|
-
// nested CSSResult → recursively resolve, with static-result memoization.
|
|
200
|
-
// When `_isDynamic === false` (populated by shared.ts#isDynamic at styled
|
|
201
|
-
// component creation), the resolved string is independent of props and can
|
|
202
|
-
// be cached on the instance. Saves re-walking strings/values for every
|
|
203
|
-
// consumer of a shared static snippet. Ported from vitus-labs `754cd203`;
|
|
204
|
-
// measured upstream: ~2.5× speedup on 8-repeated-resolve micro.
|
|
205
|
-
if (value instanceof CSSResult) {
|
|
206
|
-
if (value._isDynamic === false) {
|
|
207
|
-
if (value._staticResolved === undefined) {
|
|
208
|
-
value._staticResolved = resolve(value.strings, value.values, {})
|
|
209
|
-
}
|
|
210
|
-
return value._staticResolved
|
|
211
|
-
}
|
|
212
|
-
return resolve(value.strings, value.values, props)
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
// array of results (e.g. from makeItResponsive's breakpoints.map())
|
|
216
|
-
if (Array.isArray(value)) {
|
|
217
|
-
let arrayResult = ''
|
|
218
|
-
for (let i = 0; i < value.length; i++) {
|
|
219
|
-
arrayResult += resolveValue(value[i], props)
|
|
220
|
-
}
|
|
221
|
-
return arrayResult
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
return String(value)
|
|
225
|
-
}
|
package/src/shared.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared utilities used across multiple modules.
|
|
3
|
-
*/
|
|
4
|
-
import { CSSResult, type Interpolation } from './resolve'
|
|
5
|
-
|
|
6
|
-
/** Check if an interpolation value is dynamic (contains functions or nested dynamic CSSResults). */
|
|
7
|
-
export const isDynamic = (v: Interpolation): boolean => {
|
|
8
|
-
if (typeof v === 'function') return true
|
|
9
|
-
if (Array.isArray(v)) return v.some(isDynamic)
|
|
10
|
-
if (v instanceof CSSResult) {
|
|
11
|
-
// Memoize per-instance — CSSResults are created once at module level
|
|
12
|
-
// (one per `css\`...\`` literal) and reused across many `styled()` /
|
|
13
|
-
// `useCSS()` / nested-interpolation checks. Avoids rescanning whole
|
|
14
|
-
// sub-trees on every consumer. Ported from vitus-labs `c483cabc`.
|
|
15
|
-
const cached = v._isDynamic
|
|
16
|
-
if (cached !== undefined) return cached
|
|
17
|
-
const r = v.values.some(isDynamic)
|
|
18
|
-
v._isDynamic = r
|
|
19
|
-
return r
|
|
20
|
-
}
|
|
21
|
-
return false
|
|
22
|
-
}
|