@pyreon/styler 0.23.0 → 0.24.1
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 +22 -2
- package/lib/index.js +56 -23
- package/package.json +5 -5
- package/src/__tests__/resolve.test.ts +59 -0
- package/src/__tests__/shared.test.ts +42 -0
- package/src/forward.ts +20 -6
- package/src/globalStyle.ts +8 -3
- package/src/hash.ts +2 -4
- package/src/resolve.ts +38 -2
- package/src/shared.ts +11 -1
- package/src/sheet.ts +17 -9
package/lib/index.d.ts
CHANGED
|
@@ -60,6 +60,27 @@ type Interpolation<P extends object = Record<string, unknown>> = string | number
|
|
|
60
60
|
declare class CSSResult {
|
|
61
61
|
readonly strings: TemplateStringsArray;
|
|
62
62
|
readonly values: Interpolation[];
|
|
63
|
+
/**
|
|
64
|
+
* Memoized result of `isDynamic(this)`. Populated on first access from
|
|
65
|
+
* `shared.ts#isDynamic`. CSSResult instances are typically created once
|
|
66
|
+
* at module load (one per `css\`...\`` literal) and reused everywhere —
|
|
67
|
+
* a `styled()` component, a `useCSS` consumer, a nested interpolation,
|
|
68
|
+
* etc. Lazy-cache avoids rescanning whole sub-trees on every consumer.
|
|
69
|
+
* Ported from vitus-labs `c483cabc`.
|
|
70
|
+
*/
|
|
71
|
+
_isDynamic: boolean | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Memoized resolved CSS string for STATIC CSSResults — populated by
|
|
74
|
+
* `resolveValue` the first time a known-static nested CSSResult is
|
|
75
|
+
* resolved. Safe because props don't affect output when there are no
|
|
76
|
+
* function interpolations. Skipped for dynamic CSSResults (the resolved
|
|
77
|
+
* string depends on props each call). Common pattern: a shared static
|
|
78
|
+
* snippet interpolated into many dynamic components — pre-cache, that
|
|
79
|
+
* snippet was re-resolved per-render of every consumer. Ported from
|
|
80
|
+
* vitus-labs `754cd203`; measured upstream: 2.6M→6.5M ops/s (+149%,
|
|
81
|
+
* ~2.5× speedup on the 8-repeated-resolve micro).
|
|
82
|
+
*/
|
|
83
|
+
_staticResolved: string | undefined;
|
|
63
84
|
constructor(strings: TemplateStringsArray, values: Interpolation[]);
|
|
64
85
|
/** Resolve with empty props — useful for static templates, testing, and debugging. */
|
|
65
86
|
toString(): string;
|
|
@@ -121,8 +142,7 @@ declare const createGlobalStyle: (strings: TemplateStringsArray, ...values: Inte
|
|
|
121
142
|
*/
|
|
122
143
|
/** FNV-1a offset basis — starting state for streaming hash. */
|
|
123
144
|
declare const HASH_INIT = 2166136261;
|
|
124
|
-
/**
|
|
125
|
-
* Feed a string segment into the running hash state.
|
|
145
|
+
/** Feed a string segment into the running hash state.
|
|
126
146
|
* Streaming: hashUpdate(hashUpdate(HASH_INIT, 'ab'), 'cd') === hash('abcd').
|
|
127
147
|
*/
|
|
128
148
|
declare const hashUpdate: (init: number, str: string) => number;
|
package/lib/index.js
CHANGED
|
@@ -11,6 +11,27 @@ const _countSink$2 = globalThis;
|
|
|
11
11
|
var CSSResult = class {
|
|
12
12
|
strings;
|
|
13
13
|
values;
|
|
14
|
+
/**
|
|
15
|
+
* Memoized result of `isDynamic(this)`. Populated on first access from
|
|
16
|
+
* `shared.ts#isDynamic`. CSSResult instances are typically created once
|
|
17
|
+
* at module load (one per `css\`...\`` literal) and reused everywhere —
|
|
18
|
+
* a `styled()` component, a `useCSS` consumer, a nested interpolation,
|
|
19
|
+
* etc. Lazy-cache avoids rescanning whole sub-trees on every consumer.
|
|
20
|
+
* Ported from vitus-labs `c483cabc`.
|
|
21
|
+
*/
|
|
22
|
+
_isDynamic = void 0;
|
|
23
|
+
/**
|
|
24
|
+
* Memoized resolved CSS string for STATIC CSSResults — populated by
|
|
25
|
+
* `resolveValue` the first time a known-static nested CSSResult is
|
|
26
|
+
* resolved. Safe because props don't affect output when there are no
|
|
27
|
+
* function interpolations. Skipped for dynamic CSSResults (the resolved
|
|
28
|
+
* string depends on props each call). Common pattern: a shared static
|
|
29
|
+
* snippet interpolated into many dynamic components — pre-cache, that
|
|
30
|
+
* snippet was re-resolved per-render of every consumer. Ported from
|
|
31
|
+
* vitus-labs `754cd203`; measured upstream: 2.6M→6.5M ops/s (+149%,
|
|
32
|
+
* ~2.5× speedup on the 8-repeated-resolve micro).
|
|
33
|
+
*/
|
|
34
|
+
_staticResolved = void 0;
|
|
14
35
|
constructor(strings, values) {
|
|
15
36
|
this.strings = strings;
|
|
16
37
|
this.values = values;
|
|
@@ -100,7 +121,13 @@ const normalizeCSS = (css) => {
|
|
|
100
121
|
const resolveValue = (value, props) => {
|
|
101
122
|
if (value == null || value === false || value === true) return "";
|
|
102
123
|
if (typeof value === "function") return resolveValue(value(props), props);
|
|
103
|
-
if (value instanceof CSSResult)
|
|
124
|
+
if (value instanceof CSSResult) {
|
|
125
|
+
if (value._isDynamic === false) {
|
|
126
|
+
if (value._staticResolved === void 0) value._staticResolved = resolve(value.strings, value.values, {});
|
|
127
|
+
return value._staticResolved;
|
|
128
|
+
}
|
|
129
|
+
return resolve(value.strings, value.values, props);
|
|
130
|
+
}
|
|
104
131
|
if (Array.isArray(value)) {
|
|
105
132
|
let arrayResult = "";
|
|
106
133
|
for (let i = 0; i < value.length; i++) arrayResult += resolveValue(value[i], props);
|
|
@@ -129,7 +156,7 @@ const css = (strings, ...values) => new CSSResult(strings, values);
|
|
|
129
156
|
* elements (which causes warnings). Props starting with `$` are
|
|
130
157
|
* transient (styling-only) and are always filtered out.
|
|
131
158
|
*/
|
|
132
|
-
const
|
|
159
|
+
const HTML_PROPS_LIST = [
|
|
133
160
|
"className",
|
|
134
161
|
"class",
|
|
135
162
|
"dangerouslySetInnerHTML",
|
|
@@ -311,7 +338,9 @@ const HTML_PROPS = new Set([
|
|
|
311
338
|
"value",
|
|
312
339
|
"width",
|
|
313
340
|
"wrap"
|
|
314
|
-
]
|
|
341
|
+
];
|
|
342
|
+
const HTML_PROPS = Object.create(null);
|
|
343
|
+
for (const k of HTML_PROPS_LIST) HTML_PROPS[k] = true;
|
|
315
344
|
/**
|
|
316
345
|
* Filters props for HTML elements. Keeps valid HTML attrs, data-*, aria-*.
|
|
317
346
|
* Rejects unknown props and $-prefixed transient props.
|
|
@@ -325,7 +354,7 @@ const filterProps = (props) => {
|
|
|
325
354
|
filtered[key] = props[key];
|
|
326
355
|
continue;
|
|
327
356
|
}
|
|
328
|
-
if (HTML_PROPS
|
|
357
|
+
if (key in HTML_PROPS) filtered[key] = props[key];
|
|
329
358
|
}
|
|
330
359
|
return filtered;
|
|
331
360
|
};
|
|
@@ -373,7 +402,7 @@ const buildProps = (rawProps, generatedCls, isDOM, customFilter) => {
|
|
|
373
402
|
copyDescriptor(key);
|
|
374
403
|
continue;
|
|
375
404
|
}
|
|
376
|
-
if (HTML_PROPS
|
|
405
|
+
if (key in HTML_PROPS) copyDescriptor(key);
|
|
377
406
|
}
|
|
378
407
|
return result;
|
|
379
408
|
};
|
|
@@ -387,7 +416,13 @@ const buildProps = (rawProps, generatedCls, isDOM, customFilter) => {
|
|
|
387
416
|
const isDynamic = (v) => {
|
|
388
417
|
if (typeof v === "function") return true;
|
|
389
418
|
if (Array.isArray(v)) return v.some(isDynamic);
|
|
390
|
-
if (v instanceof CSSResult)
|
|
419
|
+
if (v instanceof CSSResult) {
|
|
420
|
+
const cached = v._isDynamic;
|
|
421
|
+
if (cached !== void 0) return cached;
|
|
422
|
+
const r = v.values.some(isDynamic);
|
|
423
|
+
v._isDynamic = r;
|
|
424
|
+
return r;
|
|
425
|
+
}
|
|
391
426
|
return false;
|
|
392
427
|
};
|
|
393
428
|
|
|
@@ -402,16 +437,12 @@ const isDynamic = (v) => {
|
|
|
402
437
|
/** FNV-1a offset basis — starting state for streaming hash. */
|
|
403
438
|
const HASH_INIT = 2166136261;
|
|
404
439
|
const FNV_PRIME = 16777619;
|
|
405
|
-
/**
|
|
406
|
-
* Feed a string segment into the running hash state.
|
|
440
|
+
/** Feed a string segment into the running hash state.
|
|
407
441
|
* Streaming: hashUpdate(hashUpdate(HASH_INIT, 'ab'), 'cd') === hash('abcd').
|
|
408
442
|
*/
|
|
409
443
|
const hashUpdate = (init, str) => {
|
|
410
444
|
let h = init;
|
|
411
|
-
for (let i = 0; i < str.length; i++)
|
|
412
|
-
h ^= str.charCodeAt(i);
|
|
413
|
-
h = Math.imul(h, FNV_PRIME);
|
|
414
|
-
}
|
|
445
|
+
for (let i = 0; i < str.length; i++) h = Math.imul(h ^ str.charCodeAt(i), FNV_PRIME);
|
|
415
446
|
return h;
|
|
416
447
|
};
|
|
417
448
|
/** Finalize a hash state into a base-36 class name suffix. */
|
|
@@ -565,13 +596,14 @@ var StyleSheet = class {
|
|
|
565
596
|
};
|
|
566
597
|
const atRules = [];
|
|
567
598
|
const baseParts = [];
|
|
599
|
+
const len = cssText.length;
|
|
568
600
|
let depth = 0;
|
|
569
601
|
let atStart = -1;
|
|
570
602
|
let lastBase = 0;
|
|
571
|
-
for (let i = 0; i <
|
|
572
|
-
const ch = cssText
|
|
573
|
-
if (ch ===
|
|
574
|
-
else if (ch ===
|
|
603
|
+
for (let i = 0; i < len; i++) {
|
|
604
|
+
const ch = cssText.charCodeAt(i);
|
|
605
|
+
if (ch === 123) depth++;
|
|
606
|
+
else if (ch === 125) {
|
|
575
607
|
depth--;
|
|
576
608
|
if (depth === 0 && atStart >= 0) {
|
|
577
609
|
const openBrace = cssText.indexOf("{", atStart);
|
|
@@ -581,7 +613,7 @@ var StyleSheet = class {
|
|
|
581
613
|
atStart = -1;
|
|
582
614
|
lastBase = i + 1;
|
|
583
615
|
}
|
|
584
|
-
} else if (depth === 0 && ch ===
|
|
616
|
+
} else if (depth === 0 && ch === 64 && atStart < 0) {
|
|
585
617
|
const remaining = cssText.slice(i, i + 20);
|
|
586
618
|
if (/^@(?:media|supports|container)\b/.test(remaining)) {
|
|
587
619
|
const baseBefore = cssText.slice(lastBase, i).trim();
|
|
@@ -676,12 +708,13 @@ var StyleSheet = class {
|
|
|
676
708
|
*/
|
|
677
709
|
splitRules(cssText) {
|
|
678
710
|
const rules = [];
|
|
711
|
+
const len = cssText.length;
|
|
679
712
|
let depth = 0;
|
|
680
713
|
let start = 0;
|
|
681
|
-
for (let i = 0; i <
|
|
682
|
-
const ch = cssText
|
|
683
|
-
if (ch ===
|
|
684
|
-
else if (ch ===
|
|
714
|
+
for (let i = 0; i < len; i++) {
|
|
715
|
+
const ch = cssText.charCodeAt(i);
|
|
716
|
+
if (ch === 123) depth++;
|
|
717
|
+
else if (ch === 125) {
|
|
685
718
|
depth--;
|
|
686
719
|
if (depth === 0) {
|
|
687
720
|
const rule = cssText.slice(start, i + 1).trim();
|
|
@@ -897,7 +930,7 @@ nativeCompat(ThemeProvider);
|
|
|
897
930
|
const createGlobalStyle = (strings, ...values) => {
|
|
898
931
|
if (!values.some(isDynamic)) {
|
|
899
932
|
const cssText = normalizeCSS(resolve(strings, values, {}));
|
|
900
|
-
if (cssText.
|
|
933
|
+
if (cssText.length > 0) sheet.insertGlobal(cssText);
|
|
901
934
|
const StaticGlobal = () => null;
|
|
902
935
|
return StaticGlobal;
|
|
903
936
|
}
|
|
@@ -907,7 +940,7 @@ const createGlobalStyle = (strings, ...values) => {
|
|
|
907
940
|
...props,
|
|
908
941
|
theme
|
|
909
942
|
}));
|
|
910
|
-
if (cssText.
|
|
943
|
+
if (cssText.length > 0) sheet.insertGlobal(cssText);
|
|
911
944
|
return null;
|
|
912
945
|
};
|
|
913
946
|
return DynamicGlobal;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pyreon/styler",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.1",
|
|
4
4
|
"description": "Lightweight CSS-in-JS engine for Pyreon",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -43,8 +43,8 @@
|
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@pyreon/manifest": "0.13.1",
|
|
46
|
-
"@pyreon/test-utils": "^0.13.
|
|
47
|
-
"@pyreon/typescript": "^0.
|
|
46
|
+
"@pyreon/test-utils": "^0.13.11",
|
|
47
|
+
"@pyreon/typescript": "^0.24.1",
|
|
48
48
|
"@vitest/browser-playwright": "^4.1.4",
|
|
49
49
|
"@vitus-labs/tools-rolldown": "^2.4.0"
|
|
50
50
|
},
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"node": ">= 22"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
-
"@pyreon/core": "^0.
|
|
56
|
-
"@pyreon/reactivity": "^0.
|
|
55
|
+
"@pyreon/core": "^0.24.1",
|
|
56
|
+
"@pyreon/reactivity": "^0.24.1"
|
|
57
57
|
}
|
|
58
58
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from "vitest"
|
|
2
2
|
import { css } from "../css"
|
|
3
3
|
import { CSSResult, normalizeCSS, resolve, resolveValue } from "../resolve"
|
|
4
|
+
import { isDynamic } from "../shared"
|
|
4
5
|
|
|
5
6
|
// Helper to create a TemplateStringsArray
|
|
6
7
|
const tsa = (strings: readonly string[]): TemplateStringsArray => {
|
|
@@ -247,3 +248,61 @@ describe("normalizeCSS", () => {
|
|
|
247
248
|
})
|
|
248
249
|
})
|
|
249
250
|
})
|
|
251
|
+
|
|
252
|
+
// Behavioural lock-in for the CSSResult._staticResolved cache (ported from
|
|
253
|
+
// vitus-labs `754cd203` + lock-in `60fc25c1`). Common pattern: a shared
|
|
254
|
+
// static snippet interpolated into many dynamic components. Without this
|
|
255
|
+
// cache the snippet's resolve work was paid once per dynamic render of
|
|
256
|
+
// every consumer; with it, the resolve fires once per snippet, total.
|
|
257
|
+
describe("CSSResult._staticResolved cache", () => {
|
|
258
|
+
it("populates _staticResolved on first resolveValue of a known-static CSSResult", () => {
|
|
259
|
+
const inner = css`color: red;`
|
|
260
|
+
// Pre-classify as static via isDynamic (the same call shared.ts makes
|
|
261
|
+
// at styled-component creation time).
|
|
262
|
+
isDynamic(inner)
|
|
263
|
+
expect(inner._isDynamic).toBe(false)
|
|
264
|
+
expect(inner._staticResolved).toBe(undefined)
|
|
265
|
+
|
|
266
|
+
resolveValue(inner, {})
|
|
267
|
+
expect(inner._staticResolved).toBe("color: red;")
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it("returns cached _staticResolved on subsequent resolveValue calls", () => {
|
|
271
|
+
const inner = css`padding: 12px;`
|
|
272
|
+
isDynamic(inner)
|
|
273
|
+
resolveValue(inner, {})
|
|
274
|
+
expect(inner._staticResolved).toBe("padding: 12px;")
|
|
275
|
+
|
|
276
|
+
// Mutate values to a sentinel that would change the resolved output if
|
|
277
|
+
// recomputed. The cache MUST return the prior result.
|
|
278
|
+
;(inner as unknown as { values: unknown[] }).values = ["SENTINEL"]
|
|
279
|
+
expect(resolveValue(inner, {})).toBe("padding: 12px;")
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it("does NOT cache dynamic CSSResults (props vary per call)", () => {
|
|
283
|
+
const dyn = css`color: ${(p: Record<string, unknown>) => p.c as string};`
|
|
284
|
+
isDynamic(dyn)
|
|
285
|
+
expect(dyn._isDynamic).toBe(true)
|
|
286
|
+
|
|
287
|
+
// Resolve twice with different props; cache should not be populated.
|
|
288
|
+
resolveValue(dyn, { c: "red" })
|
|
289
|
+
expect(dyn._staticResolved).toBe(undefined)
|
|
290
|
+
resolveValue(dyn, { c: "blue" })
|
|
291
|
+
expect(dyn._staticResolved).toBe(undefined)
|
|
292
|
+
})
|
|
293
|
+
|
|
294
|
+
it("skips cache when _isDynamic is undefined (not yet classified)", () => {
|
|
295
|
+
// Construct a CSSResult directly without going through isDynamic.
|
|
296
|
+
// resolveValue's cache check is `_isDynamic === false` (strict), so an
|
|
297
|
+
// unclassified CSSResult falls through to the regular resolve path
|
|
298
|
+
// — the regular path takes the SECOND branch (`return resolve(...)`)
|
|
299
|
+
// and does NOT populate the cache.
|
|
300
|
+
const tpl = Object.assign(["color: ", ";"], {
|
|
301
|
+
raw: ["color: ", ";"],
|
|
302
|
+
}) as unknown as TemplateStringsArray
|
|
303
|
+
const r = new CSSResult(tpl, ["red"])
|
|
304
|
+
expect(r._isDynamic).toBe(undefined)
|
|
305
|
+
expect(resolveValue(r, {})).toBe("color: red;")
|
|
306
|
+
expect(r._staticResolved).toBe(undefined) // cache stays unpopulated
|
|
307
|
+
})
|
|
308
|
+
})
|
|
@@ -88,4 +88,46 @@ describe('isDynamic', () => {
|
|
|
88
88
|
)
|
|
89
89
|
expect(isDynamic(result)).toBe(true)
|
|
90
90
|
})
|
|
91
|
+
|
|
92
|
+
// Behavioural lock-in for the CSSResult._isDynamic memoization (ported
|
|
93
|
+
// from vitus-labs `c483cabc` + lock-in `60fc25c1`). Without these tests
|
|
94
|
+
// a future regression that removed the cache would only show up as a
|
|
95
|
+
// perf regression, never as a test failure.
|
|
96
|
+
describe('CSSResult _isDynamic memoization', () => {
|
|
97
|
+
it('populates _isDynamic on first call for dynamic templates', () => {
|
|
98
|
+
const r = css`color: ${() => 'red'};`
|
|
99
|
+
expect(r._isDynamic).toBe(undefined)
|
|
100
|
+
isDynamic(r)
|
|
101
|
+
expect(r._isDynamic).toBe(true)
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('populates _isDynamic on first call for static templates', () => {
|
|
105
|
+
const r = css`color: ${'red'};`
|
|
106
|
+
expect(r._isDynamic).toBe(undefined)
|
|
107
|
+
isDynamic(r)
|
|
108
|
+
expect(r._isDynamic).toBe(false)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('returns cached result on subsequent calls without rescanning values', () => {
|
|
112
|
+
const r = css`color: ${() => 'red'};`
|
|
113
|
+
const first = isDynamic(r)
|
|
114
|
+
expect(first).toBe(true)
|
|
115
|
+
expect(r._isDynamic).toBe(true)
|
|
116
|
+
|
|
117
|
+
// Mutate values to a sentinel that would invert the answer if rescanned.
|
|
118
|
+
// The memoized path must NOT consult `values` again — it should return
|
|
119
|
+
// the cached `_isDynamic` directly.
|
|
120
|
+
;(r as unknown as { values: unknown[] }).values = ['static-only']
|
|
121
|
+
expect(isDynamic(r)).toBe(true) // still uses cached value, not rescan
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
it('memoizes nested CSSResults independently', () => {
|
|
125
|
+
const inner = css`color: ${() => 'red'};`
|
|
126
|
+
const outer = css`${inner}`
|
|
127
|
+
isDynamic(outer)
|
|
128
|
+
// Recursing through outer populates inner too.
|
|
129
|
+
expect(inner._isDynamic).toBe(true)
|
|
130
|
+
expect(outer._isDynamic).toBe(true)
|
|
131
|
+
})
|
|
132
|
+
})
|
|
91
133
|
})
|
package/src/forward.ts
CHANGED
|
@@ -4,8 +4,15 @@
|
|
|
4
4
|
* transient (styling-only) and are always filtered out.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
// Common HTML attributes, event handlers, and ARIA/data attributes
|
|
8
|
-
|
|
7
|
+
// Common HTML attributes, event handlers, and ARIA/data attributes.
|
|
8
|
+
//
|
|
9
|
+
// Using a plain object with `key in HTML_PROPS` instead of `Set.has(key)`:
|
|
10
|
+
// V8 inlines `in` checks via hidden-class lookups (the object has a fixed
|
|
11
|
+
// shape at module load and never changes), which is meaningfully faster
|
|
12
|
+
// than going through the Set protocol on hot prop-filter paths. Measured
|
|
13
|
+
// upstream (vitus-labs `be471b19`): +19% on the 5-lookup mix benchmark
|
|
14
|
+
// (4 hits + 1 miss).
|
|
15
|
+
const HTML_PROPS_LIST = [
|
|
9
16
|
// Core props
|
|
10
17
|
'className',
|
|
11
18
|
'class',
|
|
@@ -190,7 +197,13 @@ const HTML_PROPS = new Set([
|
|
|
190
197
|
'value',
|
|
191
198
|
'width',
|
|
192
199
|
'wrap',
|
|
193
|
-
]
|
|
200
|
+
] as const
|
|
201
|
+
|
|
202
|
+
// Build the lookup object once at module load. `null`-prototype keeps the
|
|
203
|
+
// object's hidden class lean and means `in` checks don't accidentally pick
|
|
204
|
+
// up `Object.prototype` keys.
|
|
205
|
+
const HTML_PROPS: Record<string, true> = Object.create(null)
|
|
206
|
+
for (const k of HTML_PROPS_LIST) HTML_PROPS[k] = true
|
|
194
207
|
|
|
195
208
|
/**
|
|
196
209
|
* Filters props for HTML elements. Keeps valid HTML attrs, data-*, aria-*.
|
|
@@ -212,8 +225,9 @@ export const filterProps = (props: Record<string, unknown>): Record<string, unkn
|
|
|
212
225
|
continue
|
|
213
226
|
}
|
|
214
227
|
|
|
215
|
-
// Keep known HTML props
|
|
216
|
-
|
|
228
|
+
// Keep known HTML props — `in` against the null-prototype lookup
|
|
229
|
+
// object beats `Set.has` on the hot DOM-filter path.
|
|
230
|
+
if (key in HTML_PROPS) {
|
|
217
231
|
filtered[key] = props[key]
|
|
218
232
|
}
|
|
219
233
|
}
|
|
@@ -288,7 +302,7 @@ export const buildProps = (
|
|
|
288
302
|
copyDescriptor(key)
|
|
289
303
|
continue
|
|
290
304
|
}
|
|
291
|
-
if (HTML_PROPS
|
|
305
|
+
if (key in HTML_PROPS) copyDescriptor(key)
|
|
292
306
|
}
|
|
293
307
|
return result
|
|
294
308
|
}
|
package/src/globalStyle.ts
CHANGED
|
@@ -26,8 +26,11 @@ export const createGlobalStyle = (
|
|
|
26
26
|
if (!hasDynamicValues) {
|
|
27
27
|
const cssText = normalizeCSS(resolve(strings, values, {}))
|
|
28
28
|
|
|
29
|
-
// Inject into sheet immediately
|
|
30
|
-
|
|
29
|
+
// Inject into sheet immediately. `normalizeCSS` already strips
|
|
30
|
+
// leading/trailing whitespace, so a length check is equivalent to the
|
|
31
|
+
// prior `.trim()` (no O(n) whitespace scan, no string allocation).
|
|
32
|
+
// Ported from vitus-labs `be471b19`.
|
|
33
|
+
if (cssText.length > 0) sheet.insertGlobal(cssText)
|
|
31
34
|
|
|
32
35
|
const StaticGlobal: ComponentFn = () => null
|
|
33
36
|
return StaticGlobal
|
|
@@ -39,7 +42,9 @@ export const createGlobalStyle = (
|
|
|
39
42
|
const allProps = { ...props, theme }
|
|
40
43
|
const cssText = normalizeCSS(resolve(strings, values, allProps))
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
// Length check — `normalizeCSS` already trims. Ported from
|
|
46
|
+
// vitus-labs `be471b19`.
|
|
47
|
+
if (cssText.length > 0) sheet.insertGlobal(cssText)
|
|
43
48
|
|
|
44
49
|
return null
|
|
45
50
|
}
|
package/src/hash.ts
CHANGED
|
@@ -10,15 +10,13 @@ export const HASH_INIT = 2166136261
|
|
|
10
10
|
|
|
11
11
|
const FNV_PRIME = 16777619
|
|
12
12
|
|
|
13
|
-
/**
|
|
14
|
-
* Feed a string segment into the running hash state.
|
|
13
|
+
/** Feed a string segment into the running hash state.
|
|
15
14
|
* Streaming: hashUpdate(hashUpdate(HASH_INIT, 'ab'), 'cd') === hash('abcd').
|
|
16
15
|
*/
|
|
17
16
|
export const hashUpdate = (init: number, str: string): number => {
|
|
18
17
|
let h = init
|
|
19
18
|
for (let i = 0; i < str.length; i++) {
|
|
20
|
-
h
|
|
21
|
-
h = Math.imul(h, FNV_PRIME)
|
|
19
|
+
h = Math.imul(h ^ str.charCodeAt(i), FNV_PRIME)
|
|
22
20
|
}
|
|
23
21
|
return h
|
|
24
22
|
}
|
package/src/resolve.ts
CHANGED
|
@@ -40,6 +40,29 @@ export type Interpolation<P extends object = Record<string, unknown>> =
|
|
|
40
40
|
* deferred until a styled component renders (or until explicitly resolved).
|
|
41
41
|
*/
|
|
42
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
|
+
|
|
43
66
|
constructor(
|
|
44
67
|
readonly strings: TemplateStringsArray,
|
|
45
68
|
readonly values: Interpolation[],
|
|
@@ -173,8 +196,21 @@ export const resolveValue = (value: Interpolation, props: Record<string, any>):
|
|
|
173
196
|
// function interpolation → call with props/theme context, resolve result
|
|
174
197
|
if (typeof value === 'function') return resolveValue(value(props) as Interpolation, props)
|
|
175
198
|
|
|
176
|
-
// nested CSSResult → recursively resolve
|
|
177
|
-
|
|
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
|
+
}
|
|
178
214
|
|
|
179
215
|
// array of results (e.g. from makeItResponsive's breakpoints.map())
|
|
180
216
|
if (Array.isArray(value)) {
|
package/src/shared.ts
CHANGED
|
@@ -7,6 +7,16 @@ import { CSSResult, type Interpolation } from './resolve'
|
|
|
7
7
|
export const isDynamic = (v: Interpolation): boolean => {
|
|
8
8
|
if (typeof v === 'function') return true
|
|
9
9
|
if (Array.isArray(v)) return v.some(isDynamic)
|
|
10
|
-
if (v instanceof CSSResult)
|
|
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
|
+
}
|
|
11
21
|
return false
|
|
12
22
|
}
|
package/src/sheet.ts
CHANGED
|
@@ -218,16 +218,21 @@ export class StyleSheet {
|
|
|
218
218
|
|
|
219
219
|
const atRules: string[] = []
|
|
220
220
|
const baseParts: string[] = []
|
|
221
|
+
const len = cssText.length
|
|
221
222
|
let depth = 0
|
|
222
223
|
let atStart = -1
|
|
223
224
|
let lastBase = 0
|
|
224
225
|
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
// `charCodeAt(i)` returns a primitive int; `cssText[i]` allocates a
|
|
227
|
+
// fresh 1-char string in V8 per iteration. On long stylesheets with
|
|
228
|
+
// at-rule blocks the per-char allocation dominates. Ported from
|
|
229
|
+
// vitus-labs `c483cabc`.
|
|
230
|
+
for (let i = 0; i < len; i++) {
|
|
231
|
+
const ch = cssText.charCodeAt(i)
|
|
227
232
|
|
|
228
|
-
if (ch ===
|
|
233
|
+
if (ch === 123 /* { */) {
|
|
229
234
|
depth++
|
|
230
|
-
} else if (ch ===
|
|
235
|
+
} else if (ch === 125 /* } */) {
|
|
231
236
|
depth--
|
|
232
237
|
if (depth === 0 && atStart >= 0) {
|
|
233
238
|
// End of a tracked at-rule block — extract and wrap with selector
|
|
@@ -240,7 +245,7 @@ export class StyleSheet {
|
|
|
240
245
|
atStart = -1
|
|
241
246
|
lastBase = i + 1
|
|
242
247
|
}
|
|
243
|
-
} else if (depth === 0 && ch ===
|
|
248
|
+
} else if (depth === 0 && ch === 64 /* @ */ && atStart < 0) {
|
|
244
249
|
// Check if this starts a splittable at-rule (not @keyframes, @font-face, etc.)
|
|
245
250
|
const remaining = cssText.slice(i, i + 20)
|
|
246
251
|
if (/^@(?:media|supports|container)\b/.test(remaining)) {
|
|
@@ -377,13 +382,16 @@ export class StyleSheet {
|
|
|
377
382
|
*/
|
|
378
383
|
private splitRules(cssText: string): string[] {
|
|
379
384
|
const rules: string[] = []
|
|
385
|
+
const len = cssText.length
|
|
380
386
|
let depth = 0
|
|
381
387
|
let start = 0
|
|
382
388
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
389
|
+
// `charCodeAt(i)` returns a primitive int; `cssText[i]` allocates a
|
|
390
|
+
// fresh 1-char string per iteration. Ported from vitus-labs `c483cabc`.
|
|
391
|
+
for (let i = 0; i < len; i++) {
|
|
392
|
+
const ch = cssText.charCodeAt(i)
|
|
393
|
+
if (ch === 123 /* { */) depth++
|
|
394
|
+
else if (ch === 125 /* } */) {
|
|
387
395
|
depth--
|
|
388
396
|
if (depth === 0) {
|
|
389
397
|
const rule = cssText.slice(start, i + 1).trim()
|