tailwind-to-style 3.2.0 → 3.2.2
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/README.md +37 -2
- package/dist/core/tws.cjs +1 -1
- package/dist/core/tws.esm.js +1 -1
- package/dist/core/twsx.cjs +111 -35
- package/dist/core/twsx.esm.js +111 -35
- package/dist/core/twsx.esm.js.map +1 -1
- package/dist/core/twsxVariants.cjs +114 -35
- package/dist/core/twsxVariants.esm.js +114 -35
- package/dist/core/twsxVariants.esm.js.map +1 -1
- package/dist/cx.cjs +1 -1
- package/dist/cx.esm.js +1 -1
- package/dist/index.cjs +123 -35
- package/dist/index.esm.js +123 -35
- package/dist/index.esm.js.map +1 -1
- package/dist/index.min.js +1 -1
- package/dist/index.min.js.map +1 -1
- package/dist/utils/index.cjs +1 -1
- package/dist/utils/index.esm.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# tailwind-to-style
|
|
2
2
|
|
|
3
3
|
[📦 View on npm](https://www.npmjs.com/package/tailwind-to-style)
|
|
4
|
+
| [🌐 Landing Page](https://bigetion.github.io/tailwind-to-style/landing.html)
|
|
5
|
+
| [🛝 Playground](https://bigetion.github.io/tailwind-to-style/sandbox.html)
|
|
4
6
|
|
|
5
7
|
[](https://www.npmjs.com/package/tailwind-to-style)
|
|
6
8
|
[](https://github.com/Bigetion/tailwind-to-style/actions)
|
|
@@ -179,6 +181,8 @@ tws('p-0.5 m-1.5 gap-2.5')
|
|
|
179
181
|
|
|
180
182
|
Generates real CSS from Tailwind classes with full selector support, SCSS-like nesting, and auto-injects a `<style>` tag into the DOM.
|
|
181
183
|
|
|
184
|
+
> **HMR-safe** — each `twsx()` call owns a stable slot in the injected style tag keyed by its top-level selectors. When you edit styles during development, the old slot is **replaced** (not appended), so changes are reflected immediately without a hard refresh.
|
|
185
|
+
|
|
182
186
|
```javascript
|
|
183
187
|
import { twsx } from 'tailwind-to-style'
|
|
184
188
|
|
|
@@ -621,10 +625,41 @@ function App() {
|
|
|
621
625
|
|
|
622
626
|
### Vue
|
|
623
627
|
|
|
628
|
+
```vue
|
|
629
|
+
<script setup>
|
|
630
|
+
import 'tailwind-to-style/preflight.css'
|
|
631
|
+
import { tws, twsx } from 'tailwind-to-style'
|
|
632
|
+
|
|
633
|
+
// twsx() at the top level of <script setup> is HMR-safe.
|
|
634
|
+
// When you edit the classes, Vite's HMR re-runs this block and
|
|
635
|
+
// the old CSS slot is replaced automatically — no hard refresh needed.
|
|
636
|
+
twsx({
|
|
637
|
+
'html': 'bg-gray-100 min-h-screen flex items-center justify-center',
|
|
638
|
+
'.card': [
|
|
639
|
+
'bg-white p-5 border border-gray-300 rounded-xl shadow-md',
|
|
640
|
+
{
|
|
641
|
+
'&:hover': 'shadow-xl',
|
|
642
|
+
'> .title': 'text-xl font-bold text-gray-900 mb-3',
|
|
643
|
+
'> .body': 'text-sm text-gray-700',
|
|
644
|
+
},
|
|
645
|
+
],
|
|
646
|
+
})
|
|
647
|
+
</script>
|
|
648
|
+
|
|
649
|
+
<template>
|
|
650
|
+
<div class="card">
|
|
651
|
+
<div class="title">Card Title</div>
|
|
652
|
+
<p class="body">Styled with twsx — hot reload works out of the box.</p>
|
|
653
|
+
</div>
|
|
654
|
+
</template>
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
For simple inline styles, use `tws()` with the reactive system:
|
|
658
|
+
|
|
624
659
|
```vue
|
|
625
660
|
<script setup>
|
|
626
661
|
import { tws } from 'tailwind-to-style'
|
|
627
|
-
const btnStyle = tws('bg-blue-500 text-white px-4 py-2 rounded-lg
|
|
662
|
+
const btnStyle = tws('bg-blue-500 text-white px-4 py-2 rounded-lg', true)
|
|
628
663
|
</script>
|
|
629
664
|
|
|
630
665
|
<template>
|
|
@@ -670,7 +705,7 @@ v3.2.0 includes major performance optimizations:
|
|
|
670
705
|
- **Pre-compiled regex** — compiled once at module load, reused for every call
|
|
671
706
|
- **Multi-level LRU caching** — class resolution, CSS generation, config lookups
|
|
672
707
|
- **Bounded caches** — Maps capped at 5,000 entries, Sets at 10,000 to prevent memory leaks
|
|
673
|
-
-
|
|
708
|
+
- **Slot-based CSS injection** — each `twsx()` call owns a named slot; updates rebuild the tag instead of appending, preventing HMR accumulation
|
|
674
709
|
- **FNV-1a hashing** — 100x faster than `JSON.stringify` for cache keys
|
|
675
710
|
|
|
676
711
|
```
|
package/dist/core/tws.cjs
CHANGED
package/dist/core/tws.esm.js
CHANGED
package/dist/core/twsx.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* tailwind-to-style v3.2.
|
|
2
|
+
* tailwind-to-style v3.2.2
|
|
3
3
|
* Runtime Tailwind CSS to inline styles converter
|
|
4
4
|
*
|
|
5
5
|
* @author Bigetion
|
|
@@ -8371,7 +8371,7 @@ function processClass(cls, selector, styles) {
|
|
|
8371
8371
|
|
|
8372
8372
|
// Get cssObject from singleton cache
|
|
8373
8373
|
const cssObject = tailwindCache.getOrGenerate(generateTailwindCssString, convertCssToObject);
|
|
8374
|
-
let declarations = cssObject[baseClassName] || cssObject[baseClassName.replace(ESCAPE_SLASH_REGEX, "
|
|
8374
|
+
let declarations = cssObject[baseClassName] || cssObject[baseClassName.replace(ESCAPE_SLASH_REGEX, "\\/")] || cssObject[baseClassName.replace(ESCAPE_DOT_REGEX, "\\.")];
|
|
8375
8375
|
if (!declarations && baseClassName.includes("[")) {
|
|
8376
8376
|
const match = CUSTOM_VALUE_FULL_REGEX.exec(baseClassName);
|
|
8377
8377
|
if (match) {
|
|
@@ -8593,11 +8593,26 @@ function flattenStyleObject(obj) {
|
|
|
8593
8593
|
flatArray.push(item);
|
|
8594
8594
|
} else if (isSelectorObject(item)) {
|
|
8595
8595
|
const nested = flattenStyleObject(item, currentSelector);
|
|
8596
|
-
|
|
8596
|
+
// Merge nested results, handling & that resolves to same selector
|
|
8597
|
+
for (const ns in nested) {
|
|
8598
|
+
if (ns === currentSelector) {
|
|
8599
|
+
// & resolved to same selector — include in this array
|
|
8600
|
+
if (Array.isArray(nested[ns])) {
|
|
8601
|
+
flatArray.push(...nested[ns]);
|
|
8602
|
+
} else {
|
|
8603
|
+
flatArray.push(nested[ns]);
|
|
8604
|
+
}
|
|
8605
|
+
} else {
|
|
8606
|
+
result[ns] = nested[ns];
|
|
8607
|
+
}
|
|
8608
|
+
}
|
|
8597
8609
|
}
|
|
8598
8610
|
}
|
|
8599
8611
|
if (flatArray.length > 0) {
|
|
8600
8612
|
result[currentSelector] = result[currentSelector] || [];
|
|
8613
|
+
if (!Array.isArray(result[currentSelector])) {
|
|
8614
|
+
result[currentSelector] = [result[currentSelector]];
|
|
8615
|
+
}
|
|
8601
8616
|
result[currentSelector].push(...flatArray);
|
|
8602
8617
|
}
|
|
8603
8618
|
} else if (isSelectorObject(val)) {
|
|
@@ -8729,6 +8744,20 @@ function twsxNoCache(obj) {
|
|
|
8729
8744
|
// Process each selector within the media query
|
|
8730
8745
|
for (const innerSelector in val) {
|
|
8731
8746
|
const innerVal = val[innerSelector];
|
|
8747
|
+
|
|
8748
|
+
// Handle @css string directive inside media queries
|
|
8749
|
+
if (typeof innerVal === "string") {
|
|
8750
|
+
const trimmedInner = innerVal.trim();
|
|
8751
|
+
if (trimmedInner.startsWith('@css')) {
|
|
8752
|
+
const cssMatch = trimmedInner.match(/^@css\s*\{([\s\S]*)\}\s*$/);
|
|
8753
|
+
if (cssMatch) {
|
|
8754
|
+
const rawCss = cssMatch[1].trim();
|
|
8755
|
+
styles[selector][innerSelector] = styles[selector][innerSelector] || '';
|
|
8756
|
+
styles[selector][innerSelector] += rawCss.split(';').filter(d => d.trim()).map(d => d.trim() + ';').join(' ') + '\n';
|
|
8757
|
+
continue;
|
|
8758
|
+
}
|
|
8759
|
+
}
|
|
8760
|
+
}
|
|
8732
8761
|
const baseClass = typeof innerVal === "string" ? expandGroupedClass(innerVal) : "";
|
|
8733
8762
|
|
|
8734
8763
|
// Process Tailwind classes for this selector
|
|
@@ -8768,6 +8797,17 @@ function twsxNoCache(obj) {
|
|
|
8768
8797
|
} else if (Array.isArray(val)) {
|
|
8769
8798
|
for (const item of val) {
|
|
8770
8799
|
if (typeof item === "string") {
|
|
8800
|
+
// Handle @css strings inside arrays
|
|
8801
|
+
const trimmedItem = item.trim();
|
|
8802
|
+
if (trimmedItem.startsWith('@css')) {
|
|
8803
|
+
const cssMatch = trimmedItem.match(/^@css\s*\{([\s\S]*)\}\s*$/);
|
|
8804
|
+
if (cssMatch) {
|
|
8805
|
+
const rawCss = cssMatch[1].trim();
|
|
8806
|
+
styles[selector] = styles[selector] || '';
|
|
8807
|
+
styles[selector] += rawCss.split(';').filter(d => d.trim()).map(d => d.trim() + ';').join(' ') + '\n';
|
|
8808
|
+
continue;
|
|
8809
|
+
}
|
|
8810
|
+
}
|
|
8771
8811
|
baseClass += (baseClass ? " " : "") + expandGroupedClass(item);
|
|
8772
8812
|
} else if (typeof item === "object" && item !== null) {
|
|
8773
8813
|
Object.assign(nested, item);
|
|
@@ -8958,27 +8998,40 @@ function fastObjectHash(obj) {
|
|
|
8958
8998
|
*/
|
|
8959
8999
|
function twsx(obj) {
|
|
8960
9000
|
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
8961
|
-
// Create fast hash key from input (100x faster than JSON.stringify)
|
|
9001
|
+
// Create fast hash key from input content (100x faster than JSON.stringify)
|
|
8962
9002
|
const cacheKey = fastObjectHash(obj, options);
|
|
9003
|
+
const {
|
|
9004
|
+
inject = true
|
|
9005
|
+
} = options;
|
|
9006
|
+
|
|
9007
|
+
// Derive a STABLE registry key from the object's top-level selector KEYS only.
|
|
9008
|
+
// This key is independent of the CSS class values, so when styles are edited
|
|
9009
|
+
// during HMR (same selectors, different Tailwind classes) the old slot is
|
|
9010
|
+
// replaced rather than a new slot being created — preventing CSS accumulation.
|
|
9011
|
+
const registryKey = obj && typeof obj === "object" && !Array.isArray(obj) ? Object.keys(obj).sort().join("|") : cacheKey; // fallback for edge cases
|
|
8963
9012
|
|
|
8964
9013
|
// Check cache first
|
|
8965
9014
|
if (_twsxInputCache.has(cacheKey)) {
|
|
8966
9015
|
const cached = _twsxInputCache.get(cacheKey);
|
|
8967
9016
|
|
|
8968
|
-
//
|
|
8969
|
-
const {
|
|
8970
|
-
inject = true
|
|
8971
|
-
} = options;
|
|
9017
|
+
// Re-inject with registryKey so the slot stays registered (no-op when unchanged).
|
|
8972
9018
|
if (inject && (IS_BROWSER || _ssrCollecting)) {
|
|
8973
|
-
autoInjectCss(cached);
|
|
9019
|
+
autoInjectCss(cached, registryKey);
|
|
8974
9020
|
}
|
|
8975
9021
|
return cached;
|
|
8976
9022
|
}
|
|
8977
9023
|
|
|
8978
|
-
// Cache miss:
|
|
8979
|
-
|
|
9024
|
+
// Cache miss: generate CSS without internal auto-injection so that twsx owns
|
|
9025
|
+
// the injection and can pass the selector-based registryKey for slot replacement.
|
|
9026
|
+
const result = twsxNoCache(obj, {
|
|
9027
|
+
...options,
|
|
9028
|
+
inject: false
|
|
9029
|
+
});
|
|
8980
9030
|
_twsxInputCache.set(cacheKey, result);
|
|
8981
9031
|
evictMap(_twsxInputCache);
|
|
9032
|
+
if (inject && (IS_BROWSER || _ssrCollecting)) {
|
|
9033
|
+
autoInjectCss(result, registryKey);
|
|
9034
|
+
}
|
|
8982
9035
|
return result;
|
|
8983
9036
|
}
|
|
8984
9037
|
|
|
@@ -8998,12 +9051,54 @@ function getCssHash(str) {
|
|
|
8998
9051
|
|
|
8999
9052
|
// Enhanced auto-inject CSS with performance monitoring & SSR support
|
|
9000
9053
|
const injectedCssHashSet = new Set();
|
|
9054
|
+
|
|
9055
|
+
// Registry of sourceKey → cssBlock for smart slot-based replacement.
|
|
9056
|
+
// Prevents stale CSS chunks from accumulating across HMR cycles.
|
|
9057
|
+
const _cssBlockRegistry = new Map();
|
|
9058
|
+
|
|
9059
|
+
/**
|
|
9060
|
+
* Rebuild the single twsx style tag from the full CSS block registry.
|
|
9061
|
+
* Called whenever a block is added or updated.
|
|
9062
|
+
*/
|
|
9063
|
+
function rebuildStyleTag() {
|
|
9064
|
+
let styleTag = document.getElementById("twsx-auto-style");
|
|
9065
|
+
if (!styleTag) {
|
|
9066
|
+
styleTag = document.createElement("style");
|
|
9067
|
+
styleTag.id = "twsx-auto-style";
|
|
9068
|
+
styleTag.setAttribute("data-twsx", "");
|
|
9069
|
+
document.head.appendChild(styleTag);
|
|
9070
|
+
}
|
|
9071
|
+
styleTag.textContent = [..._cssBlockRegistry.values()].join("\n");
|
|
9072
|
+
}
|
|
9001
9073
|
function autoInjectCss(cssString) {
|
|
9074
|
+
let sourceKey = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
|
|
9002
9075
|
const marker = performanceMonitor.start("css:inject");
|
|
9003
9076
|
try {
|
|
9004
9077
|
// SSR mode: collect CSS strings instead of DOM injection
|
|
9005
9078
|
if (_ssrCollecting) ;
|
|
9006
9079
|
if (IS_BROWSER) {
|
|
9080
|
+
if (sourceKey) {
|
|
9081
|
+
// Slot-based update: each unique twsx(obj) call owns its own CSS block.
|
|
9082
|
+
// When styles change (new content, same logical call site via cacheKey)
|
|
9083
|
+
// the old slot is replaced and the style tag is fully rebuilt, so no
|
|
9084
|
+
// stale rules from previous HMR cycles can pile up.
|
|
9085
|
+
const existing = _cssBlockRegistry.get(sourceKey);
|
|
9086
|
+
if (existing === cssString) {
|
|
9087
|
+
// Identical content – nothing to do.
|
|
9088
|
+
performanceMonitor.end(marker);
|
|
9089
|
+
return;
|
|
9090
|
+
}
|
|
9091
|
+
_cssBlockRegistry.set(sourceKey, cssString);
|
|
9092
|
+
rebuildStyleTag();
|
|
9093
|
+
if (_cssBlockRegistry.size % 10 === 0) {
|
|
9094
|
+
logger.debug(`CSS registry stats: ${_cssBlockRegistry.size} blocks registered`);
|
|
9095
|
+
}
|
|
9096
|
+
performanceMonitor.end(marker);
|
|
9097
|
+
return;
|
|
9098
|
+
}
|
|
9099
|
+
|
|
9100
|
+
// Fallback path (e.g. direct autoInjectCss calls without a sourceKey):
|
|
9101
|
+
// keep the original hash-based dedup + append behaviour.
|
|
9007
9102
|
const cssHash = getCssHash(cssString);
|
|
9008
9103
|
if (injectedCssHashSet.has(cssHash)) {
|
|
9009
9104
|
performanceMonitor.end(marker);
|
|
@@ -9019,30 +9114,11 @@ function autoInjectCss(cssString) {
|
|
|
9019
9114
|
document.head.appendChild(styleTag);
|
|
9020
9115
|
}
|
|
9021
9116
|
|
|
9022
|
-
//
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
|
|
9026
|
-
|
|
9027
|
-
const rules = cssString.split('}').filter(r => r.trim());
|
|
9028
|
-
for (const rule of rules) {
|
|
9029
|
-
const trimmed = rule.trim();
|
|
9030
|
-
if (trimmed) {
|
|
9031
|
-
try {
|
|
9032
|
-
sheet.insertRule(trimmed + '}', sheet.cssRules.length);
|
|
9033
|
-
} catch (e) {
|
|
9034
|
-
// Fallback for complex rules (e.g., @keyframes, @media)
|
|
9035
|
-
styleTag.textContent += `\n${trimmed}}`;
|
|
9036
|
-
}
|
|
9037
|
-
}
|
|
9038
|
-
}
|
|
9039
|
-
} else {
|
|
9040
|
-
styleTag.textContent += `\n${cssString}`;
|
|
9041
|
-
}
|
|
9042
|
-
} catch (e) {
|
|
9043
|
-
// Ultimate fallback
|
|
9044
|
-
styleTag.textContent += `\n${cssString}`;
|
|
9045
|
-
}
|
|
9117
|
+
// Append CSS to style tag using textContent for reliability
|
|
9118
|
+
// Note: insertRule + textContent mixing destroys CSSOM rules,
|
|
9119
|
+
// so we use textContent exclusively for consistent behavior
|
|
9120
|
+
// with @keyframes, @media, and other nested CSS blocks.
|
|
9121
|
+
styleTag.textContent += `\n${cssString}`;
|
|
9046
9122
|
|
|
9047
9123
|
// Log injection stats periodically
|
|
9048
9124
|
if (injectedCssHashSet.size % 10 === 0) {
|
package/dist/core/twsx.esm.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* tailwind-to-style v3.2.
|
|
2
|
+
* tailwind-to-style v3.2.2
|
|
3
3
|
* Runtime Tailwind CSS to inline styles converter
|
|
4
4
|
*
|
|
5
5
|
* @author Bigetion
|
|
@@ -8369,7 +8369,7 @@ function processClass(cls, selector, styles) {
|
|
|
8369
8369
|
|
|
8370
8370
|
// Get cssObject from singleton cache
|
|
8371
8371
|
const cssObject = tailwindCache.getOrGenerate(generateTailwindCssString, convertCssToObject);
|
|
8372
|
-
let declarations = cssObject[baseClassName] || cssObject[baseClassName.replace(ESCAPE_SLASH_REGEX, "
|
|
8372
|
+
let declarations = cssObject[baseClassName] || cssObject[baseClassName.replace(ESCAPE_SLASH_REGEX, "\\/")] || cssObject[baseClassName.replace(ESCAPE_DOT_REGEX, "\\.")];
|
|
8373
8373
|
if (!declarations && baseClassName.includes("[")) {
|
|
8374
8374
|
const match = CUSTOM_VALUE_FULL_REGEX.exec(baseClassName);
|
|
8375
8375
|
if (match) {
|
|
@@ -8591,11 +8591,26 @@ function flattenStyleObject(obj) {
|
|
|
8591
8591
|
flatArray.push(item);
|
|
8592
8592
|
} else if (isSelectorObject(item)) {
|
|
8593
8593
|
const nested = flattenStyleObject(item, currentSelector);
|
|
8594
|
-
|
|
8594
|
+
// Merge nested results, handling & that resolves to same selector
|
|
8595
|
+
for (const ns in nested) {
|
|
8596
|
+
if (ns === currentSelector) {
|
|
8597
|
+
// & resolved to same selector — include in this array
|
|
8598
|
+
if (Array.isArray(nested[ns])) {
|
|
8599
|
+
flatArray.push(...nested[ns]);
|
|
8600
|
+
} else {
|
|
8601
|
+
flatArray.push(nested[ns]);
|
|
8602
|
+
}
|
|
8603
|
+
} else {
|
|
8604
|
+
result[ns] = nested[ns];
|
|
8605
|
+
}
|
|
8606
|
+
}
|
|
8595
8607
|
}
|
|
8596
8608
|
}
|
|
8597
8609
|
if (flatArray.length > 0) {
|
|
8598
8610
|
result[currentSelector] = result[currentSelector] || [];
|
|
8611
|
+
if (!Array.isArray(result[currentSelector])) {
|
|
8612
|
+
result[currentSelector] = [result[currentSelector]];
|
|
8613
|
+
}
|
|
8599
8614
|
result[currentSelector].push(...flatArray);
|
|
8600
8615
|
}
|
|
8601
8616
|
} else if (isSelectorObject(val)) {
|
|
@@ -8727,6 +8742,20 @@ function twsxNoCache(obj) {
|
|
|
8727
8742
|
// Process each selector within the media query
|
|
8728
8743
|
for (const innerSelector in val) {
|
|
8729
8744
|
const innerVal = val[innerSelector];
|
|
8745
|
+
|
|
8746
|
+
// Handle @css string directive inside media queries
|
|
8747
|
+
if (typeof innerVal === "string") {
|
|
8748
|
+
const trimmedInner = innerVal.trim();
|
|
8749
|
+
if (trimmedInner.startsWith('@css')) {
|
|
8750
|
+
const cssMatch = trimmedInner.match(/^@css\s*\{([\s\S]*)\}\s*$/);
|
|
8751
|
+
if (cssMatch) {
|
|
8752
|
+
const rawCss = cssMatch[1].trim();
|
|
8753
|
+
styles[selector][innerSelector] = styles[selector][innerSelector] || '';
|
|
8754
|
+
styles[selector][innerSelector] += rawCss.split(';').filter(d => d.trim()).map(d => d.trim() + ';').join(' ') + '\n';
|
|
8755
|
+
continue;
|
|
8756
|
+
}
|
|
8757
|
+
}
|
|
8758
|
+
}
|
|
8730
8759
|
const baseClass = typeof innerVal === "string" ? expandGroupedClass(innerVal) : "";
|
|
8731
8760
|
|
|
8732
8761
|
// Process Tailwind classes for this selector
|
|
@@ -8766,6 +8795,17 @@ function twsxNoCache(obj) {
|
|
|
8766
8795
|
} else if (Array.isArray(val)) {
|
|
8767
8796
|
for (const item of val) {
|
|
8768
8797
|
if (typeof item === "string") {
|
|
8798
|
+
// Handle @css strings inside arrays
|
|
8799
|
+
const trimmedItem = item.trim();
|
|
8800
|
+
if (trimmedItem.startsWith('@css')) {
|
|
8801
|
+
const cssMatch = trimmedItem.match(/^@css\s*\{([\s\S]*)\}\s*$/);
|
|
8802
|
+
if (cssMatch) {
|
|
8803
|
+
const rawCss = cssMatch[1].trim();
|
|
8804
|
+
styles[selector] = styles[selector] || '';
|
|
8805
|
+
styles[selector] += rawCss.split(';').filter(d => d.trim()).map(d => d.trim() + ';').join(' ') + '\n';
|
|
8806
|
+
continue;
|
|
8807
|
+
}
|
|
8808
|
+
}
|
|
8769
8809
|
baseClass += (baseClass ? " " : "") + expandGroupedClass(item);
|
|
8770
8810
|
} else if (typeof item === "object" && item !== null) {
|
|
8771
8811
|
Object.assign(nested, item);
|
|
@@ -8956,27 +8996,40 @@ function fastObjectHash(obj) {
|
|
|
8956
8996
|
*/
|
|
8957
8997
|
function twsx(obj) {
|
|
8958
8998
|
let options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
|
|
8959
|
-
// Create fast hash key from input (100x faster than JSON.stringify)
|
|
8999
|
+
// Create fast hash key from input content (100x faster than JSON.stringify)
|
|
8960
9000
|
const cacheKey = fastObjectHash(obj, options);
|
|
9001
|
+
const {
|
|
9002
|
+
inject = true
|
|
9003
|
+
} = options;
|
|
9004
|
+
|
|
9005
|
+
// Derive a STABLE registry key from the object's top-level selector KEYS only.
|
|
9006
|
+
// This key is independent of the CSS class values, so when styles are edited
|
|
9007
|
+
// during HMR (same selectors, different Tailwind classes) the old slot is
|
|
9008
|
+
// replaced rather than a new slot being created — preventing CSS accumulation.
|
|
9009
|
+
const registryKey = obj && typeof obj === "object" && !Array.isArray(obj) ? Object.keys(obj).sort().join("|") : cacheKey; // fallback for edge cases
|
|
8961
9010
|
|
|
8962
9011
|
// Check cache first
|
|
8963
9012
|
if (_twsxInputCache.has(cacheKey)) {
|
|
8964
9013
|
const cached = _twsxInputCache.get(cacheKey);
|
|
8965
9014
|
|
|
8966
|
-
//
|
|
8967
|
-
const {
|
|
8968
|
-
inject = true
|
|
8969
|
-
} = options;
|
|
9015
|
+
// Re-inject with registryKey so the slot stays registered (no-op when unchanged).
|
|
8970
9016
|
if (inject && (IS_BROWSER || _ssrCollecting)) {
|
|
8971
|
-
autoInjectCss(cached);
|
|
9017
|
+
autoInjectCss(cached, registryKey);
|
|
8972
9018
|
}
|
|
8973
9019
|
return cached;
|
|
8974
9020
|
}
|
|
8975
9021
|
|
|
8976
|
-
// Cache miss:
|
|
8977
|
-
|
|
9022
|
+
// Cache miss: generate CSS without internal auto-injection so that twsx owns
|
|
9023
|
+
// the injection and can pass the selector-based registryKey for slot replacement.
|
|
9024
|
+
const result = twsxNoCache(obj, {
|
|
9025
|
+
...options,
|
|
9026
|
+
inject: false
|
|
9027
|
+
});
|
|
8978
9028
|
_twsxInputCache.set(cacheKey, result);
|
|
8979
9029
|
evictMap(_twsxInputCache);
|
|
9030
|
+
if (inject && (IS_BROWSER || _ssrCollecting)) {
|
|
9031
|
+
autoInjectCss(result, registryKey);
|
|
9032
|
+
}
|
|
8980
9033
|
return result;
|
|
8981
9034
|
}
|
|
8982
9035
|
|
|
@@ -8996,12 +9049,54 @@ function getCssHash(str) {
|
|
|
8996
9049
|
|
|
8997
9050
|
// Enhanced auto-inject CSS with performance monitoring & SSR support
|
|
8998
9051
|
const injectedCssHashSet = new Set();
|
|
9052
|
+
|
|
9053
|
+
// Registry of sourceKey → cssBlock for smart slot-based replacement.
|
|
9054
|
+
// Prevents stale CSS chunks from accumulating across HMR cycles.
|
|
9055
|
+
const _cssBlockRegistry = new Map();
|
|
9056
|
+
|
|
9057
|
+
/**
|
|
9058
|
+
* Rebuild the single twsx style tag from the full CSS block registry.
|
|
9059
|
+
* Called whenever a block is added or updated.
|
|
9060
|
+
*/
|
|
9061
|
+
function rebuildStyleTag() {
|
|
9062
|
+
let styleTag = document.getElementById("twsx-auto-style");
|
|
9063
|
+
if (!styleTag) {
|
|
9064
|
+
styleTag = document.createElement("style");
|
|
9065
|
+
styleTag.id = "twsx-auto-style";
|
|
9066
|
+
styleTag.setAttribute("data-twsx", "");
|
|
9067
|
+
document.head.appendChild(styleTag);
|
|
9068
|
+
}
|
|
9069
|
+
styleTag.textContent = [..._cssBlockRegistry.values()].join("\n");
|
|
9070
|
+
}
|
|
8999
9071
|
function autoInjectCss(cssString) {
|
|
9072
|
+
let sourceKey = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null;
|
|
9000
9073
|
const marker = performanceMonitor.start("css:inject");
|
|
9001
9074
|
try {
|
|
9002
9075
|
// SSR mode: collect CSS strings instead of DOM injection
|
|
9003
9076
|
if (_ssrCollecting) ;
|
|
9004
9077
|
if (IS_BROWSER) {
|
|
9078
|
+
if (sourceKey) {
|
|
9079
|
+
// Slot-based update: each unique twsx(obj) call owns its own CSS block.
|
|
9080
|
+
// When styles change (new content, same logical call site via cacheKey)
|
|
9081
|
+
// the old slot is replaced and the style tag is fully rebuilt, so no
|
|
9082
|
+
// stale rules from previous HMR cycles can pile up.
|
|
9083
|
+
const existing = _cssBlockRegistry.get(sourceKey);
|
|
9084
|
+
if (existing === cssString) {
|
|
9085
|
+
// Identical content – nothing to do.
|
|
9086
|
+
performanceMonitor.end(marker);
|
|
9087
|
+
return;
|
|
9088
|
+
}
|
|
9089
|
+
_cssBlockRegistry.set(sourceKey, cssString);
|
|
9090
|
+
rebuildStyleTag();
|
|
9091
|
+
if (_cssBlockRegistry.size % 10 === 0) {
|
|
9092
|
+
logger.debug(`CSS registry stats: ${_cssBlockRegistry.size} blocks registered`);
|
|
9093
|
+
}
|
|
9094
|
+
performanceMonitor.end(marker);
|
|
9095
|
+
return;
|
|
9096
|
+
}
|
|
9097
|
+
|
|
9098
|
+
// Fallback path (e.g. direct autoInjectCss calls without a sourceKey):
|
|
9099
|
+
// keep the original hash-based dedup + append behaviour.
|
|
9005
9100
|
const cssHash = getCssHash(cssString);
|
|
9006
9101
|
if (injectedCssHashSet.has(cssHash)) {
|
|
9007
9102
|
performanceMonitor.end(marker);
|
|
@@ -9017,30 +9112,11 @@ function autoInjectCss(cssString) {
|
|
|
9017
9112
|
document.head.appendChild(styleTag);
|
|
9018
9113
|
}
|
|
9019
9114
|
|
|
9020
|
-
//
|
|
9021
|
-
|
|
9022
|
-
|
|
9023
|
-
|
|
9024
|
-
|
|
9025
|
-
const rules = cssString.split('}').filter(r => r.trim());
|
|
9026
|
-
for (const rule of rules) {
|
|
9027
|
-
const trimmed = rule.trim();
|
|
9028
|
-
if (trimmed) {
|
|
9029
|
-
try {
|
|
9030
|
-
sheet.insertRule(trimmed + '}', sheet.cssRules.length);
|
|
9031
|
-
} catch (e) {
|
|
9032
|
-
// Fallback for complex rules (e.g., @keyframes, @media)
|
|
9033
|
-
styleTag.textContent += `\n${trimmed}}`;
|
|
9034
|
-
}
|
|
9035
|
-
}
|
|
9036
|
-
}
|
|
9037
|
-
} else {
|
|
9038
|
-
styleTag.textContent += `\n${cssString}`;
|
|
9039
|
-
}
|
|
9040
|
-
} catch (e) {
|
|
9041
|
-
// Ultimate fallback
|
|
9042
|
-
styleTag.textContent += `\n${cssString}`;
|
|
9043
|
-
}
|
|
9115
|
+
// Append CSS to style tag using textContent for reliability
|
|
9116
|
+
// Note: insertRule + textContent mixing destroys CSSOM rules,
|
|
9117
|
+
// so we use textContent exclusively for consistent behavior
|
|
9118
|
+
// with @keyframes, @media, and other nested CSS blocks.
|
|
9119
|
+
styleTag.textContent += `\n${cssString}`;
|
|
9044
9120
|
|
|
9045
9121
|
// Log injection stats periodically
|
|
9046
9122
|
if (injectedCssHashSet.size % 10 === 0) {
|