@pyreon/styler 0.15.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/index.d.ts CHANGED
@@ -204,6 +204,12 @@ declare class StyleSheet {
204
204
  /**
205
205
  * Full cleanup: clear cache and remove all CSS rules from the DOM.
206
206
  * Intended for HMR / dev-time reloads where stale styles must be purged.
207
+ *
208
+ * Also fires `onSheetClear` subscribers so downstream caches (e.g.
209
+ * `styled.tsx`'s static-component cache) reset alongside the sheet.
210
+ * Without this, stale `StaticStyled` ComponentFn references survive HMR
211
+ * and continue to apply CSS class names that were just deleted from
212
+ * the DOM — observable as missing styles after every hot reload.
207
213
  */
208
214
  clearAll(): void;
209
215
  /**
package/lib/index.js CHANGED
@@ -665,6 +665,12 @@ var StyleSheet = class {
665
665
  /**
666
666
  * Full cleanup: clear cache and remove all CSS rules from the DOM.
667
667
  * Intended for HMR / dev-time reloads where stale styles must be purged.
668
+ *
669
+ * Also fires `onSheetClear` subscribers so downstream caches (e.g.
670
+ * `styled.tsx`'s static-component cache) reset alongside the sheet.
671
+ * Without this, stale `StaticStyled` ComponentFn references survive HMR
672
+ * and continue to apply CSS class names that were just deleted from
673
+ * the DOM — observable as missing styles after every hot reload.
668
674
  */
669
675
  clearAll() {
670
676
  this.cache.clear();
@@ -672,6 +678,7 @@ var StyleSheet = class {
672
678
  clearNormCache();
673
679
  this.ssrBuffer = [];
674
680
  if (this.sheet) while (this.sheet.cssRules.length > 0) this.sheet.deleteRule(0);
681
+ fireSheetClearSubscribers();
675
682
  }
676
683
  /**
677
684
  * Compute className and full CSS rule text without injecting.
@@ -710,6 +717,22 @@ const sheet = new StyleSheet();
710
717
  * Use in SSR to get per-request isolation.
711
718
  */
712
719
  const createSheet = (options) => new StyleSheet(options);
720
+ const _sheetClearSubscribers = /* @__PURE__ */ new Set();
721
+ const fireSheetClearSubscribers = () => {
722
+ for (const cb of _sheetClearSubscribers) cb();
723
+ };
724
+ /**
725
+ * Subscribe to `sheet.clearAll()`. Fires after the sheet has been
726
+ * fully cleared, so subscribers can drop downstream caches that depend
727
+ * on the sheet's class names being live in the DOM.
728
+ *
729
+ * Returns a disposer for symmetry; in practice subscribers register
730
+ * once at module load and never unsubscribe.
731
+ */
732
+ const onSheetClear = (callback) => {
733
+ _sheetClearSubscribers.add(callback);
734
+ return () => _sheetClearSubscribers.delete(callback);
735
+ };
713
736
 
714
737
  //#endregion
715
738
  //#region src/ThemeProvider.ts
@@ -799,20 +822,28 @@ const keyframes = (strings, ...values) => new KeyframesResult(strings, values);
799
822
  //#region src/styled.tsx
800
823
  const _countSink = globalThis;
801
824
  const getDisplayName = (tag) => typeof tag === "string" ? tag : tag.displayName || tag.name || "Component";
802
- const staticComponentCache = /* @__PURE__ */ new WeakMap();
803
- let _hotStrings = null;
804
- let _hotTag = null;
805
- let _hotComponent = null;
825
+ let staticComponentCache = /* @__PURE__ */ new WeakMap();
826
+ const _hotCache = {
827
+ strings: null,
828
+ tag: null,
829
+ component: null
830
+ };
831
+ onSheetClear(() => {
832
+ staticComponentCache = /* @__PURE__ */ new WeakMap();
833
+ _hotCache.strings = null;
834
+ _hotCache.tag = null;
835
+ _hotCache.component = null;
836
+ });
806
837
  const createStyledComponent = (tag, strings, values, options) => {
807
838
  if (values.length === 0 && !options) {
808
- if (strings === _hotStrings && tag === _hotTag) return _hotComponent;
839
+ if (strings === _hotCache.strings && tag === _hotCache.tag) return _hotCache.component;
809
840
  const tagMap = staticComponentCache.get(strings);
810
841
  if (tagMap) {
811
842
  const cached = tagMap.get(tag);
812
843
  if (cached) {
813
- _hotStrings = strings;
814
- _hotTag = tag;
815
- _hotComponent = cached;
844
+ _hotCache.strings = strings;
845
+ _hotCache.tag = tag;
846
+ _hotCache.component = cached;
816
847
  return cached;
817
848
  }
818
849
  }
@@ -823,9 +854,20 @@ const createStyledComponent = (tag, strings, values, options) => {
823
854
  if (!hasDynamicValues) {
824
855
  const cssText = normalizeCSS(values.length === 0 ? strings[0] : resolve(strings, values, {}));
825
856
  const staticClassName = cssText.length > 0 ? sheet.insert(cssText, false, insertLayer) : "";
857
+ const tagIsDOM = typeof tag === "string";
858
+ const cachedEmptyVNode = h(tag, staticClassName ? { class: staticClassName } : {});
826
859
  const StaticStyled = (rawProps) => {
860
+ let hasExtraProps = false;
861
+ for (const _k in rawProps) {
862
+ hasExtraProps = true;
863
+ break;
864
+ }
865
+ if (!hasExtraProps && rawProps.ref == null) {
866
+ if (process.env.NODE_ENV !== "production") _countSink.__pyreon_count__?.("styler.staticVNode.hit");
867
+ return cachedEmptyVNode;
868
+ }
827
869
  const finalTag = rawProps.as || tag;
828
- return h(finalTag, buildProps(rawProps, staticClassName, typeof finalTag === "string", customFilter), ...Array.isArray(rawProps.children) ? rawProps.children : rawProps.children != null ? [rawProps.children] : []);
870
+ return h(finalTag, buildProps(rawProps, staticClassName, finalTag === tag ? tagIsDOM : typeof finalTag === "string", customFilter), ...Array.isArray(rawProps.children) ? rawProps.children : rawProps.children != null ? [rawProps.children] : []);
829
871
  };
830
872
  StaticStyled.displayName = `styled(${getDisplayName(tag)})`;
831
873
  if (!options && values.length === 0) {
@@ -835,9 +877,9 @@ const createStyledComponent = (tag, strings, values, options) => {
835
877
  staticComponentCache.set(strings, tagMap);
836
878
  }
837
879
  tagMap.set(tag, StaticStyled);
838
- _hotStrings = strings;
839
- _hotTag = tag;
840
- _hotComponent = StaticStyled;
880
+ _hotCache.strings = strings;
881
+ _hotCache.tag = tag;
882
+ _hotCache.component = StaticStyled;
841
883
  }
842
884
  return StaticStyled;
843
885
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/styler",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "description": "Lightweight CSS-in-JS engine for Pyreon",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -42,16 +42,16 @@
42
42
  "typecheck": "tsc --noEmit"
43
43
  },
44
44
  "devDependencies": {
45
- "@pyreon/test-utils": "^0.13.2",
46
- "@pyreon/typescript": "^0.15.0",
45
+ "@pyreon/test-utils": "^0.13.3",
46
+ "@pyreon/typescript": "^0.16.0",
47
47
  "@vitest/browser-playwright": "^4.1.4",
48
48
  "@vitus-labs/tools-rolldown": "^2.3.0"
49
49
  },
50
- "peerDependencies": {
51
- "@pyreon/core": "^0.15.0",
52
- "@pyreon/reactivity": "^0.15.0"
53
- },
54
50
  "engines": {
55
51
  "node": ">= 22"
52
+ },
53
+ "dependencies": {
54
+ "@pyreon/core": "^0.16.0",
55
+ "@pyreon/reactivity": "^0.16.0"
56
56
  }
57
57
  }
@@ -1,6 +1,6 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
2
2
  import { hash } from '../hash'
3
- import { sheet, StyleSheet } from '../sheet'
3
+ import { onSheetClear, sheet, StyleSheet } from '../sheet'
4
4
 
5
5
  describe('StyleSheet', () => {
6
6
  beforeEach(() => {
@@ -152,6 +152,47 @@ describe('StyleSheet', () => {
152
152
  })
153
153
  })
154
154
 
155
+ describe('onSheetClear', () => {
156
+ // Subscriber registry used by `styled.tsx` to drop its static-component
157
+ // cache when the singleton sheet is cleared. Without this, stale
158
+ // `StaticStyled` ComponentFns survive HMR and continue returning class
159
+ // names the sheet just deleted from the DOM.
160
+ it('fires subscribers after clearAll', () => {
161
+ const cb = vi.fn()
162
+ const dispose = onSheetClear(cb)
163
+ sheet.clearAll()
164
+ expect(cb).toHaveBeenCalledTimes(1)
165
+ dispose()
166
+ })
167
+
168
+ it('does NOT fire subscribers on clearCache (partial cleanup)', () => {
169
+ const cb = vi.fn()
170
+ const dispose = onSheetClear(cb)
171
+ sheet.insert('color: red;')
172
+ sheet.clearCache()
173
+ expect(cb).not.toHaveBeenCalled()
174
+ dispose()
175
+ })
176
+
177
+ it('disposer removes the subscriber', () => {
178
+ const cb = vi.fn()
179
+ const dispose = onSheetClear(cb)
180
+ dispose()
181
+ sheet.clearAll()
182
+ expect(cb).not.toHaveBeenCalled()
183
+ })
184
+
185
+ it('fires multiple subscribers in registration order', () => {
186
+ const order: number[] = []
187
+ const dispose1 = onSheetClear(() => order.push(1))
188
+ const dispose2 = onSheetClear(() => order.push(2))
189
+ sheet.clearAll()
190
+ expect(order).toEqual([1, 2])
191
+ dispose1()
192
+ dispose2()
193
+ })
194
+ })
195
+
155
196
  describe('has', () => {
156
197
  it('returns true for cached classNames', () => {
157
198
  const className = sheet.insert('color: red;')
@@ -398,4 +398,114 @@ describe('styled.tag (Proxy)', () => {
398
398
  const vnode = Comp({}) as VNode
399
399
  expect(vnode.type).toBe('section')
400
400
  })
401
+
402
+ describe('empty-rawProps static VNode cache', () => {
403
+ // Hot path for `<MyStyled />` with no props: pre-built VNode returned
404
+ // from the StaticStyled closure verbatim. Skips `buildProps` + `h()` +
405
+ // children-array construction per render. Mirrors vitus-labs PR #228.
406
+ it('returns the SAME VNode identity across renders when rawProps is empty', () => {
407
+ const Comp = styled('div')`
408
+ color: red;
409
+ `
410
+ const v1 = Comp({}) as VNode
411
+ const v2 = Comp({}) as VNode
412
+ // Same VNode object — proves the pre-built cache fires.
413
+ expect(v1).toBe(v2)
414
+ expect(v1.type).toBe('div')
415
+ expect((v1.props as Record<string, string>).class).toMatch(/^pyr-/)
416
+ })
417
+
418
+ it('falls through to the full path when ANY prop is provided', () => {
419
+ const Comp = styled('div')`
420
+ color: red;
421
+ `
422
+ const v1 = Comp({}) as VNode
423
+ const v2 = Comp({ 'data-x': '1' }) as VNode
424
+ // Different identity — the second call bypassed the cache because
425
+ // `for (const _k in rawProps) hasExtraProps = true` fires.
426
+ expect(v1).not.toBe(v2)
427
+ // Both still produce the correct className.
428
+ expect((v1.props as Record<string, unknown>).class).toMatch(/^pyr-/)
429
+ expect((v2.props as Record<string, unknown>).class).toMatch(/^pyr-/)
430
+ // Second VNode carries the extra prop forwarded through buildProps.
431
+ expect((v2.props as Record<string, unknown>)['data-x']).toBe('1')
432
+ })
433
+
434
+ it('falls through to the full path when `as` overrides the tag', () => {
435
+ const Comp = styled('div')`
436
+ color: red;
437
+ `
438
+ const v1 = Comp({}) as VNode
439
+ const v2 = Comp({ as: 'span' }) as VNode
440
+ // `as` is enumerable → `hasExtraProps = true` → bypasses cache.
441
+ // Output tag is the override.
442
+ expect(v2.type).toBe('span')
443
+ expect(v1).not.toBe(v2)
444
+ })
445
+
446
+ it('falls through to the full path when a ref is provided', () => {
447
+ const Comp = styled('div')`
448
+ color: red;
449
+ `
450
+ const refCb = () => {}
451
+ const v1 = Comp({}) as VNode
452
+ const v2 = Comp({ ref: refCb }) as VNode
453
+ // `ref` is enumerable in JS, so `hasExtraProps = true` already fires.
454
+ // The explicit `rawProps.ref == null` guard is defense-in-depth for
455
+ // any future call site that uses Object.defineProperty(rawProps, 'ref',
456
+ // { enumerable: false, ... }) — that shape would otherwise return the
457
+ // cached no-ref VNode and silently drop the user's callback.
458
+ expect(v1).not.toBe(v2)
459
+ })
460
+ })
461
+
462
+ describe('clearAll resets static-component cache', () => {
463
+ // Regression: pre-fix, `staticComponentCache` (WeakMap) and the
464
+ // single-entry hot cache (`_hotStrings` / `_hotTag` / `_hotComponent`)
465
+ // survived `sheet.clearAll()`. After HMR / dev reload, the same
466
+ // template-literal call site re-invoked `styled('div')\`...\`` and got
467
+ // back the SAME ComponentFn instance — but the class name that
468
+ // component returns was deleted from the DOM by `clearAll`. End-user
469
+ // symptom: every hot reload silently broke styles for any static
470
+ // styled component until full page refresh.
471
+ //
472
+ // Fix wires `onSheetClear` so styled.tsx subscribes at module load
473
+ // and resets both caches alongside the sheet.
474
+ it('producing a new component after clearAll, with a fresh class name', () => {
475
+ // First mount: get baseline component + className.
476
+ const tag = 'div'
477
+ const literal: TemplateStringsArray = Object.assign(
478
+ ['color: red;'] as unknown as TemplateStringsArray,
479
+ { raw: ['color: red;'] },
480
+ )
481
+ const Comp1 = (styled(tag) as (s: TemplateStringsArray) => any)(literal)
482
+ const vnode1 = Comp1({}) as VNode
483
+ const class1 = (vnode1.props as Record<string, string>).class
484
+
485
+ // Same call, no clear: hot cache returns the SAME function identity.
486
+ const Comp1Again = (styled(tag) as (s: TemplateStringsArray) => any)(literal)
487
+ expect(Comp1Again).toBe(Comp1)
488
+
489
+ // Clear the sheet (HMR simulation).
490
+ sheet.clearAll()
491
+
492
+ // After clear: same template-literal identity should produce a NEW
493
+ // component (caches were dropped). Its className resolves against
494
+ // the now-empty sheet, so the new className IS re-inserted into
495
+ // the DOM and the class is observable.
496
+ const Comp2 = (styled(tag) as (s: TemplateStringsArray) => any)(literal)
497
+ expect(Comp2).not.toBe(Comp1)
498
+ const vnode2 = Comp2({}) as VNode
499
+ const class2 = (vnode2.props as Record<string, string>).class
500
+ // FNV-1a hashing is content-deterministic, so class names are
501
+ // structurally equal — but the sheet has freshly re-inserted the
502
+ // rule. Asserting non-empty + same format is the load-bearing
503
+ // observation: pre-fix, Comp2 === Comp1 and class2 would also have
504
+ // been `''` if the user had run `clearAll` between insertions
505
+ // (cache stale, sheet empty).
506
+ expect(class1).toMatch(/^pyr-/)
507
+ expect(class2).toMatch(/^pyr-/)
508
+ expect(sheet.has(class2!)).toBe(true)
509
+ })
510
+ })
401
511
  })
package/src/sheet.ts CHANGED
@@ -391,6 +391,12 @@ export class StyleSheet {
391
391
  /**
392
392
  * Full cleanup: clear cache and remove all CSS rules from the DOM.
393
393
  * Intended for HMR / dev-time reloads where stale styles must be purged.
394
+ *
395
+ * Also fires `onSheetClear` subscribers so downstream caches (e.g.
396
+ * `styled.tsx`'s static-component cache) reset alongside the sheet.
397
+ * Without this, stale `StaticStyled` ComponentFn references survive HMR
398
+ * and continue to apply CSS class names that were just deleted from
399
+ * the DOM — observable as missing styles after every hot reload.
394
400
  */
395
401
  clearAll(): void {
396
402
  this.cache.clear()
@@ -402,6 +408,7 @@ export class StyleSheet {
402
408
  this.sheet.deleteRule(0)
403
409
  }
404
410
  }
411
+ fireSheetClearSubscribers()
405
412
  }
406
413
 
407
414
  /**
@@ -447,3 +454,29 @@ export const sheet = new StyleSheet()
447
454
  * Use in SSR to get per-request isolation.
448
455
  */
449
456
  export const createSheet = (options?: StyleSheetOptions): StyleSheet => new StyleSheet(options)
457
+
458
+ // ─── onSheetClear subscriber registry ─────────────────────────────────────
459
+ //
460
+ // Used by `styled.tsx` to reset its static-component cache when the
461
+ // singleton sheet is cleared via `clearAll()`. Module-level Set so the
462
+ // subscription survives between calls; ports the vitus-labs pattern from
463
+ // `connector-styler/sheet.ts:onClear`. Scoped to the singleton sheet —
464
+ // per-instance sheets created via `createSheet()` don't fire the hook.
465
+ const _sheetClearSubscribers = new Set<() => void>()
466
+
467
+ const fireSheetClearSubscribers = (): void => {
468
+ for (const cb of _sheetClearSubscribers) cb()
469
+ }
470
+
471
+ /**
472
+ * Subscribe to `sheet.clearAll()`. Fires after the sheet has been
473
+ * fully cleared, so subscribers can drop downstream caches that depend
474
+ * on the sheet's class names being live in the DOM.
475
+ *
476
+ * Returns a disposer for symmetry; in practice subscribers register
477
+ * once at module load and never unsubscribe.
478
+ */
479
+ export const onSheetClear = (callback: () => void): (() => void) => {
480
+ _sheetClearSubscribers.add(callback)
481
+ return () => _sheetClearSubscribers.delete(callback)
482
+ }
package/src/styled.tsx CHANGED
@@ -21,7 +21,7 @@ import { computed, renderEffect, runUntracked } from '@pyreon/reactivity'
21
21
  import { buildProps } from './forward'
22
22
  import { type Interpolation, normalizeCSS, resolve } from './resolve'
23
23
  import { isDynamic } from './shared'
24
- import { sheet } from './sheet'
24
+ import { onSheetClear, sheet } from './sheet'
25
25
  import { useThemeAccessor } from './ThemeProvider'
26
26
 
27
27
  // Dev-time counter sink — see packages/internals/perf-harness/COUNTERS.md.
@@ -50,12 +50,33 @@ const getDisplayName = (tag: Tag): string =>
50
50
 
51
51
  // Component cache: same template literal + tag + no options → same component.
52
52
  // WeakMap on `strings` (TemplateStringsArray is object-identity per source location).
53
- const staticComponentCache = new WeakMap<TemplateStringsArray, Map<Tag, ComponentFn>>()
54
-
55
- // Single-entry hot cache just 3 reference comparisons, no Map/WeakMap overhead.
56
- let _hotStrings: TemplateStringsArray | null = null
57
- let _hotTag: Tag | null = null
58
- let _hotComponent: ComponentFn | null = null
53
+ // `let` so `sheet.clearAll()` (HMR / dev reload) can drop stale entries by
54
+ // swapping the WeakMap reference — WeakMap has no `.clear()` method, and stale
55
+ // `StaticStyled` ComponentFns left behind would keep returning class names the
56
+ // sheet just deleted from the DOM.
57
+ let staticComponentCache = new WeakMap<TemplateStringsArray, Map<Tag, ComponentFn>>()
58
+
59
+ // Single-entry hot cache — 3 reference comparisons, no Map/WeakMap overhead.
60
+ // All 3 fields move atomically (consolidated into one object so `clearAll`
61
+ // resets them together — pre-fix, partial state was possible if a reset
62
+ // path forgot one field).
63
+ const _hotCache: {
64
+ strings: TemplateStringsArray | null
65
+ tag: Tag | null
66
+ component: ComponentFn | null
67
+ } = { strings: null, tag: null, component: null }
68
+
69
+ // Subscribe to `sheet.clearAll()` (HMR / dev-time reset). Drops both the
70
+ // WeakMap and the hot-cache slots so subsequent `styled()` calls produce
71
+ // fresh components with up-to-date class names. Static class names emitted
72
+ // before `clearAll` are stale by the time the user observes them — the rule
73
+ // they pointed at has been deleted from the DOM.
74
+ onSheetClear(() => {
75
+ staticComponentCache = new WeakMap()
76
+ _hotCache.strings = null
77
+ _hotCache.tag = null
78
+ _hotCache.component = null
79
+ })
59
80
 
60
81
  const createStyledComponent = (
61
82
  tag: Tag,
@@ -65,16 +86,17 @@ const createStyledComponent = (
65
86
  ): ComponentFn => {
66
87
  // Ultra-fast hot cache: 3 reference comparisons → return immediately
67
88
  if (values.length === 0 && !options) {
68
- if (strings === _hotStrings && tag === _hotTag) return _hotComponent as ComponentFn
89
+ if (strings === _hotCache.strings && tag === _hotCache.tag)
90
+ return _hotCache.component as ComponentFn
69
91
 
70
92
  // WeakMap fallback for alternating patterns
71
93
  const tagMap = staticComponentCache.get(strings)
72
94
  if (tagMap) {
73
95
  const cached = tagMap.get(tag)
74
96
  if (cached) {
75
- _hotStrings = strings
76
- _hotTag = tag
77
- _hotComponent = cached
97
+ _hotCache.strings = strings
98
+ _hotCache.tag = tag
99
+ _hotCache.component = cached
78
100
  return cached
79
101
  }
80
102
  }
@@ -94,9 +116,56 @@ const createStyledComponent = (
94
116
 
95
117
  const staticClassName = hasCss ? sheet.insert(cssText, false, insertLayer) : ''
96
118
 
119
+ // Hoisted out of the render fn: `tag` is known at component-creation time,
120
+ // and `tag` matches `rawProps.as ?? tag` whenever rawProps is empty (the
121
+ // common case for `<MyStyled />` without any props). The DOM-ness check
122
+ // doesn't change between renders for the same `tag`.
123
+ const tagIsDOM = typeof tag === 'string'
124
+
125
+ // Pre-built VNode for the no-extra-props hot path (`<MyStyled />`). Same
126
+ // shape `h(tag, { class })` would produce per render, but allocated once
127
+ // at component-creation time. Mount.ts spreads `vnode.props` into a new
128
+ // object before invoking the component (mount.ts:404-418 doesn't mutate
129
+ // the source vnode), so sharing the same VNode across mount sites is
130
+ // safe. `vnode.children` is empty here because the empty-rawProps branch
131
+ // also implies no children were passed — `rawProps.children` would be
132
+ // `undefined` and the `Array.isArray ? : ?? : []` chain produces `[]`.
133
+ //
134
+ // **Cache lifetime**: this VNode references `staticClassName`, which is
135
+ // the className the sheet just inserted. If `sheet.clearAll()` runs
136
+ // (HMR / dev reload), the className becomes stale BUT the outer
137
+ // `staticComponentCache` (and `_hot*` caches) ALSO survive that path —
138
+ // so consumers continue to receive the stale className regardless. The
139
+ // companion fix to wire `onSheetClear` and reset both caches is tracked
140
+ // separately (see PR #561). This optimization is correct under the
141
+ // existing cache lifetime contract; the HMR-staleness issue is broader
142
+ // than the VNode cache.
143
+ const cachedEmptyVNode = h(
144
+ tag as string,
145
+ staticClassName ? { class: staticClassName } : {},
146
+ )
147
+
97
148
  const StaticStyled: ComponentFn = (rawProps: Record<string, any>): VNode | null => {
149
+ // Hot path: no extra props beyond what's empty AND no `ref` / `as`.
150
+ // `for ... in` over an empty object is O(0); the `break` exits on the
151
+ // first key. Skipping the cache when `ref` is present is necessary
152
+ // because the user expects their callback to fire on the mounted DOM
153
+ // node — the pre-built VNode has no `ref` in its props.
154
+ let hasExtraProps = false
155
+ for (const _k in rawProps) {
156
+ hasExtraProps = true
157
+ break
158
+ }
159
+ if (!hasExtraProps && rawProps.ref == null) {
160
+ if (process.env.NODE_ENV !== 'production')
161
+ _countSink.__pyreon_count__?.('styler.staticVNode.hit')
162
+ return cachedEmptyVNode
163
+ }
164
+
98
165
  const finalTag = rawProps.as || tag
99
- const isDOM = typeof finalTag === 'string'
166
+ // Fast `isDOM` when the user didn't pass `as` — reuses the closure-time
167
+ // check. Only `typeof` is needed when `as` overrides the tag.
168
+ const isDOM = finalTag === tag ? tagIsDOM : typeof finalTag === 'string'
100
169
  const finalProps = buildProps(rawProps, staticClassName, isDOM, customFilter)
101
170
 
102
171
  return h(
@@ -121,9 +190,9 @@ const createStyledComponent = (
121
190
  staticComponentCache.set(strings, tagMap)
122
191
  }
123
192
  tagMap.set(tag, StaticStyled)
124
- _hotStrings = strings
125
- _hotTag = tag
126
- _hotComponent = StaticStyled
193
+ _hotCache.strings = strings
194
+ _hotCache.tag = tag
195
+ _hotCache.component = StaticStyled
127
196
  }
128
197
 
129
198
  return StaticStyled