Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 20 additions & 2 deletions packages/core/src/sheet.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,15 @@ const isSheetAccessible = (/** @type {CSSStyleSheet} */ sheet) => {
}
}

/** Whether `root` supports constructable stylesheets (`new CSSStyleSheet()` + `adoptedStyleSheets`). */
const supportsConstructedStylesheets = (/** @type {DocumentOrShadowRoot} */ root) => {
try {
return Array.isArray(Object(root).adoptedStyleSheets) && typeof CSSStyleSheet !== 'undefined' && 'replaceSync' in CSSStyleSheet.prototype
} catch (e) {
return false
}
}

export const createSheet = (/** @type {DocumentOrShadowRoot} */ root) => {
/** @type {SheetGroup} Object hosting the hydrated stylesheet. */
let groupSheet
Expand Down Expand Up @@ -71,8 +80,8 @@ export const createSheet = (/** @type {DocumentOrShadowRoot} */ root) => {
}
}

/** @type {StyleSheetList} */
const sheets = Object(root).styleSheets || []
/** @type {(CSSStyleSheet)[]} Stylesheets from `<style>`/`<link>` elements, plus any adopted stylesheets. */
const sheets = [...(Object(root).styleSheets || []), ...(Object(root).adoptedStyleSheets || [])]

// iterate all stylesheets until a hydratable stylesheet is found
for (const sheet of sheets) {
Expand Down Expand Up @@ -139,6 +148,15 @@ export const createSheet = (/** @type {DocumentOrShadowRoot} */ root) => {
if (!root) {
return createCSSMediaRule('', 'text/css')
}

// constructable stylesheets aren't gated by CSP style-src, since no <style>
// element is ever inserted into the DOM — prefer them when available.
if (supportsConstructedStylesheets(root)) {
const adoptedSheet = new CSSStyleSheet()
root.adoptedStyleSheets = [...root.adoptedStyleSheets, adoptedSheet]
return adoptedSheet
}

const styleEl = document.createElement('style')
const nonce = getNonce()
if (nonce) {
Expand Down
97 changes: 97 additions & 0 deletions packages/core/tests/internal/fakeDom.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
/**
* Minimal in-memory CSSOM/DOM mocks that exercise sheet.js's browser-dependent branches
* (constructable stylesheets, and the classic <style>-element fallback) without a real
* browser — jsdom doesn't implement constructable stylesheets, so it can't be used here.
*/

class FakeCSSMediaGroupRule {
constructor() {
this.type = 4
this.cssRules = []
}
get cssText() {
return `@media{${this.cssRules.map((rule) => rule.cssText).join('')}}`
}
insertRule(cssText, index) {
this.cssRules.splice(index, 0, { type: 1, cssText })
}
deleteRule(index) {
this.cssRules.splice(index, 1)
}
}

/**
* Reformats a `--sxs{--sxs:N}` marker into the spaced-out shape a real browser's CSSOM
* serializer returns for a simple style rule (`selector { prop: value; }`), since sheet.js's
* hydration scan parses that exact shape back out via `cssText.slice(14, -3)`.
*/
const formatMarkerRuleCssText = (/** @type {string} */ insertedCssText) => {
const index = insertedCssText.slice('--sxs{--sxs:'.length, -1)
return `--sxs { --sxs: ${index}; }`
}

export class FakeCSSStyleSheet {
constructor() {
this.cssRules = []
}
insertRule(cssText, index) {
const rule = cssText === '@media{}' ? new FakeCSSMediaGroupRule() : { type: 1, cssText: formatMarkerRuleCssText(cssText) }
this.cssRules.splice(index, 0, rule)
return index
}
deleteRule(index) {
this.cssRules.splice(index, 1)
}
}
// only needs to exist for sheet.js's `'replaceSync' in CSSStyleSheet.prototype` feature-detection
FakeCSSStyleSheet.prototype.replaceSync = function replaceSync() {}

/** A fake `DocumentOrShadowRoot`: enough surface for sheet.js — `styleSheets`, `adoptedStyleSheets`, `head.appendChild`. */
export const createFakeRoot = () => {
const root = {
styleSheets: [],
adoptedStyleSheets: [],
head: {
appendChild(styleEl) {
styleEl.sheet = new FakeCSSStyleSheet()
root.styleSheets.push(styleEl.sheet)
return styleEl
},
},
}
return root
}

/**
* Installs the globals sheet.js's code reads directly: `CSSStyleSheet` (feature-detection +
* construction), and `document`/`window` for the `<style>`-element fallback path
* (`document.createElement`, `getNonce`). The test runner executes every test file in one
* shared Node process, so these must be restored afterwards or they'd leak into unrelated
* test files — e.g. making `createStitches()` elsewhere pick up a fake `root` by default.
* Returns a restore function; call it once all assertions for this file have run.
*/
export const installFakeGlobals = () => {
const previous = {
CSSStyleSheet: globalThis.CSSStyleSheet,
window: globalThis.window,
document: globalThis.document,
}

globalThis.CSSStyleSheet = FakeCSSStyleSheet
globalThis.window = {}
globalThis.document = {
createElement: (tagName) => ({
tagName,
attributes: {},
setAttribute(name, value) {
this.attributes[name] = value
},
}),
}

return () => {
globalThis.CSSStyleSheet = previous.CSSStyleSheet
globalThis.window = previous.window
globalThis.document = previous.document
}
}
76 changes: 76 additions & 0 deletions packages/core/tests/sheet-adopted-stylesheets.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { createSheet, names } from '../src/sheet.js'
import { createFakeRoot, FakeCSSStyleSheet, installFakeGlobals } from './internal/fakeDom.js'

// sheet.js reads `CSSStyleSheet`/`document`/`window` as ambient globals, so they're faked here.
// restoreGlobals() runs synchronously right after describe() below — every test() body in this
// file is itself synchronous, so by the time describe()'s own internal `await call()` yields,
// all assertions below have already executed and it's safe to restore the real globals before
// the test runner moves on to the next file (it shares one Node process across all test files).
const restoreGlobals = installFakeGlobals()

describe('sheet.js — constructable stylesheets', () => {
test('uses adoptedStyleSheets when supported, and never appends a <style> element', () => {
const root = createFakeRoot()

const groupSheet = createSheet(root)

expect(root.adoptedStyleSheets.length).toBe(1)
expect(groupSheet.sheet).toBe(root.adoptedStyleSheets[0])
expect(root.adoptedStyleSheets[0]).toBeInstanceOf(FakeCSSStyleSheet)
expect(root.styleSheets.length).toBe(0)
})

test('applies and serializes rules through the adopted stylesheet', () => {
const root = createFakeRoot()
const groupSheet = createSheet(root)

groupSheet.rules.styled.apply('.a{color:red}')

expect(groupSheet.toString()).toBe('--sxs{--sxs:2}@media{.a{color:red}}')
})

test('reset() rehydrates from the adopted stylesheet instead of re-adopting or duplicating groups', () => {
const root = createFakeRoot()
const groupSheet = createSheet(root)

groupSheet.rules.styled.apply('.a{color:red}')
groupSheet.reset()

// must not have adopted a second stylesheet
expect(root.adoptedStyleSheets.length).toBe(1)

// the rule applied before reset() must still be there
expect(groupSheet.toString()).toBe('--sxs{--sxs:2}@media{.a{color:red}}')

// applying after reset() must append to the rehydrated group, not a duplicate one
groupSheet.rules.styled.apply('.b{color:blue}')
expect(groupSheet.toString()).toBe('--sxs{--sxs:2}@media{.a{color:red}.b{color:blue}}')

// every group's marker/media pair must appear exactly once, not duplicated by the rescan
const markerRuleCount = root.adoptedStyleSheets[0].cssRules.filter((rule) => rule.cssText.startsWith('--sxs')).length
expect(markerRuleCount).toBe(names.length)

groupSheet.reset()
expect(root.adoptedStyleSheets.length).toBe(1)
})

test('falls back to a <style> element when constructable stylesheets are unsupported', () => {
const originalReplaceSync = FakeCSSStyleSheet.prototype.replaceSync
delete FakeCSSStyleSheet.prototype.replaceSync

try {
const root = createFakeRoot()
const groupSheet = createSheet(root)

expect(root.adoptedStyleSheets.length).toBe(0)
expect(root.styleSheets.length).toBe(1)

groupSheet.rules.styled.apply('.c{color:green}')
expect(groupSheet.toString()).toBe('--sxs{--sxs:2}@media{.c{color:green}}')
} finally {
FakeCSSStyleSheet.prototype.replaceSync = originalReplaceSync
}
})
})

restoreGlobals()
Loading