Skip to content

Commit fe88051

Browse files
committed
feat: preserve classes created by explicit tw calls during SSR
1 parent d5765a7 commit fe88051

File tree

9 files changed

+294
-167
lines changed

9 files changed

+294
-167
lines changed

.changeset/little-pets-begin.md

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
---
2+
'twind': patch
3+
---
4+
5+
feat: preserve classes created by explicit `tw` calls during SSR
6+
7+
Previously `inline` and `extract` cleared the `tw` instance before parsing the html assuming that all classes are available via `class` attributes. That led to missing styles from `injectGlobal` or explicit `tw` calls.
8+
9+
This change introduces a `snaphot` method on `tw` and sheet instances which allows to preserve the classes that are created by explicit `tw` calls.
10+
11+
**Default Mode** _(nothing changed here)_
12+
13+
```js
14+
import { inline } from 'twind'
15+
16+
function render() {
17+
return inline(renderApp())
18+
}
19+
```
20+
21+
**Library Mode**
22+
23+
```js
24+
import { tw, stringify } from 'twind'
25+
26+
function render() {
27+
// remember global classes
28+
const restore = tw.snapshot()
29+
30+
// generated html
31+
const html = renderApp()
32+
33+
// create CSS
34+
const css = stringify(tw.target)
35+
36+
// restore global classes
37+
restore()
38+
39+
// inject as last element into the head
40+
return html.replace('</head>', `<style data-twind>${css}</style></head>`)
41+
}
42+
```

packages/twind/src/extract.ts

-60
This file was deleted.

packages/twind/src/index.ts

+1-3
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,10 @@
77

88
export * from './animation'
99
export * from './colors'
10-
export * from './consume'
1110
export * from './css'
1211
export * from './cx'
1312
export * from './define-config'
14-
export * from './extract'
1513
export * from './inject-global'
16-
export * from './inline'
1714
export * from './install'
1815
export * from './keyframes'
1916
export * from './nested'
@@ -24,5 +21,6 @@ export * from './style'
2421
export * from './twind'
2522
export * from './tx'
2623
export * from './sheets'
24+
export * from './ssr'
2725
export * from './types'
2826
export * from './utils'

packages/twind/src/inline.ts

-83
This file was deleted.

packages/twind/src/sheets.ts

+46-7
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,20 @@ export function cssom(element?: CSSStyleSheet | Element | null | false): Sheet<C
2121
return {
2222
target,
2323

24+
snapshot() {
25+
// collect current rules
26+
const rules = Array.from(target.cssRules, (rule) => rule.cssText)
27+
28+
return () => {
29+
// remove all existing rules
30+
this.clear()
31+
32+
// add all snapshot rules back
33+
// eslint-disable-next-line @typescript-eslint/unbound-method
34+
rules.forEach(this.insert as (cssText: string, index: number) => void)
35+
}
36+
},
37+
2438
clear() {
2539
// remove all added rules
2640
for (let index = target.cssRules.length; index--; ) {
@@ -32,10 +46,10 @@ export function cssom(element?: CSSStyleSheet | Element | null | false): Sheet<C
3246
target.ownerNode?.remove()
3347
},
3448

35-
insert(css, index) {
49+
insert(cssText, index) {
3650
try {
3751
// Insert
38-
target.insertRule(css, index)
52+
target.insertRule(cssText, index)
3953
} catch (error) {
4054
// Empty rule to keep index valid — not using `*{}` as that would show up in all rules (DX)
4155
target.insertRule(':root{}', index)
@@ -44,8 +58,8 @@ export function cssom(element?: CSSStyleSheet | Element | null | false): Sheet<C
4458
// lets filter them to prevent unnecessary warnings
4559
// ::-moz-focus-inner
4660
// :-moz-focusring
47-
if (!/:-[mwo]/.test(css)) {
48-
console.warn(error, css)
61+
if (!/:-[mwo]/.test(cssText)) {
62+
console.warn(error, cssText)
4963
}
5064
}
5165
},
@@ -60,6 +74,20 @@ export function dom(element?: Element | null | false): Sheet<HTMLStyleElement> {
6074
return {
6175
target,
6276

77+
snapshot() {
78+
// collect current rules
79+
const rules = Array.from(target.childNodes, (node) => node.textContent as string)
80+
81+
return () => {
82+
// remove all existing rules
83+
this.clear()
84+
85+
// add all snapshot rules back
86+
// eslint-disable-next-line @typescript-eslint/unbound-method
87+
rules.forEach(this.insert as (cssText: string, index: number) => void)
88+
}
89+
},
90+
6391
clear() {
6492
target.textContent = ''
6593
},
@@ -68,8 +96,8 @@ export function dom(element?: Element | null | false): Sheet<HTMLStyleElement> {
6896
target.remove()
6997
},
7098

71-
insert(css, index) {
72-
target.insertBefore(document.createTextNode(css), target.childNodes[index] || null)
99+
insert(cssText, index) {
100+
target.insertBefore(document.createTextNode(cssText), target.childNodes[index] || null)
73101
},
74102

75103
resume: noop,
@@ -78,9 +106,20 @@ export function dom(element?: Element | null | false): Sheet<HTMLStyleElement> {
78106

79107
export function virtual(includeResumeData?: boolean): Sheet<string[]> {
80108
const target: string[] = []
109+
81110
return {
82111
target,
83112

113+
snapshot() {
114+
// collect current rules
115+
const rules = [...target]
116+
117+
return () => {
118+
// remove all existing rules and add all snapshot rules back
119+
target.splice(0, target.length, ...rules)
120+
}
121+
},
122+
84123
clear() {
85124
target.length = 0
86125
},
@@ -128,7 +167,7 @@ export function stringify(target: unknown): string {
128167
// string[] | CSSStyleSheet | HTMLStyleElement
129168
return (
130169
// prefer the raw text content of a CSSStyleSheet as it may include the resume data
131-
((target as CSSStyleSheet).ownerNode || (target as HTMLStyleElement))?.textContent ||
170+
((target as CSSStyleSheet).ownerNode || (target as HTMLStyleElement)).textContent ||
132171
((target as CSSStyleSheet).cssRules
133172
? Array.from((target as CSSStyleSheet).cssRules, (rule) => rule.cssText)
134173
: asArray(target)

0 commit comments

Comments
 (0)