diff --git a/packages/beasties/README.md b/packages/beasties/README.md index d2abc28..be3d188 100644 --- a/packages/beasties/README.md +++ b/packages/beasties/README.md @@ -244,6 +244,24 @@ You can estimate the contents of your viewport roughly and add a
``` +You can mark more than one element with `data-beasties-container`. CSS is then inlined if it matches inside _any_ of the containers, which is useful when the above-the-fold content is spread across multiple disconnected regions. + +```html + + +
+ /* evaluated */ +
+
+ /* ignored */ +
+ + + +``` + _Note: This is an easy way to improve the performance of Beasties_ ### Skipping individual stylesheets diff --git a/packages/beasties/src/dom.ts b/packages/beasties/src/dom.ts index ca14240..392fe02 100644 --- a/packages/beasties/src/dom.ts +++ b/packages/beasties/src/dom.ts @@ -65,16 +65,18 @@ export function createDocument(html: string) { // Extend Element.prototype with DOM manipulation methods. extendElement(Element.prototype) - // Beasties container is the viewport to evaluate critical CSS - let beastiesContainer: Node | HTMLDocument = document.querySelector('[data-beasties-container]') as Node + // Beasties containers are the viewport to evaluate critical CSS. + let beastiesContainers: (Node | HTMLDocument)[] = document.querySelectorAll('[data-beasties-container]') as Node[] - if (!beastiesContainer) { + if (!beastiesContainers.length) { document.documentElement?.setAttribute('data-beasties-container', '') - beastiesContainer = document.documentElement || document + beastiesContainers = [document.documentElement || document] } - document.beastiesContainer = beastiesContainer - buildCache(beastiesContainer) + document.beastiesContainers = beastiesContainers + for (const container of beastiesContainers) { + buildCache(container) + } return document } @@ -257,7 +259,11 @@ export interface HTMLDocument extends ParsedDocument { exists: (sel: string) => boolean querySelector: (sel: string) => Node querySelectorAll: (sel: string) => Node[] + /** + * @deprecated + */ beastiesContainer: HTMLDocument | Node + beastiesContainers: (HTMLDocument | Node)[] } function extendDocument(document: ParsedDocument): asserts document is HTMLDocument { @@ -336,6 +342,12 @@ function extendDocument(document: ParsedDocument): asserts document is HTMLDocum return selectAll(sel, this) }, }, + + beastiesContainer: { + get() { + return this.beastiesContainers?.[0] + }, + }, }) } diff --git a/packages/beasties/src/index.ts b/packages/beasties/src/index.ts index 68689ba..e85bcb8 100644 --- a/packages/beasties/src/index.ts +++ b/packages/beasties/src/index.ts @@ -493,7 +493,7 @@ export default class Beasties { const name = style.$$name ? style.$$name.replace(LEADING_SLASH_RE, '') : 'inline CSS' const options = this.options - const beastiesContainer = document.beastiesContainer! + const beastiesContainers = document.beastiesContainers! let keyframesMode = options.keyframes ?? 'critical' // we also accept a boolean value for options.keyframes if (keyframesMode === true) @@ -612,7 +612,7 @@ export default class Beasties { return false try { - return beastiesContainer.exists(sel) + return beastiesContainers.some(container => container.exists(sel)) } catch (e) { failedSelectors.push(`${sel} -> ${(e as Error).message || (e as Error).toString()}`) diff --git a/packages/beasties/test/beasties.test.ts b/packages/beasties/test/beasties.test.ts index 2d2b21c..56c33af 100644 --- a/packages/beasties/test/beasties.test.ts +++ b/packages/beasties/test/beasties.test.ts @@ -78,6 +78,96 @@ describe('beasties', () => { `) }) + it('should inline critical CSS from multiple beasties containers', async () => { + const beasties = new Beasties() + const result = await beasties.process(trim` + + + +
+
A
+
+
+
B
+
+
+
C
+
+ + + `) + expect(result).toContain('.a{color:red}') + expect(result).toContain('.c{color:green}') + expect(result).not.toContain('.b{color:blue}') + }) + + it('should inline remote stylesheets from multiple beasties containers', async () => { + const beasties = new Beasties({ + reduceInlineStyles: false, + path: '/', + }) + const assets: Record = { + '/style.css': trim` + .a { color: red } + .b { color: blue } + .c { color: green } + `, + } + beasties.readFile = filename => assets[filename.replace(/^\w:/, '').replace(/\\/g, '/')]! + const result = await beasties.process(trim` + + + + + +
+
A
+
+
+
B
+
+
+
C
+
+ + + `) + expect(result).toContain('') + expect(result).not.toContain('.b{color:blue}') + expect(result).toContain('') + }) + + it('should inline critical CSS from nested beasties containers', async () => { + const beasties = new Beasties() + const result = await beasties.process(trim` + + + +
+
outer
+
+
inner
+
+
+
+
B
+
+ + + `) + expect(result).toContain('.outer{color:red}') + expect(result).toContain('.inner{color:green}') + expect(result).not.toContain('.b{color:blue}') + }) + it('run on HTML file', async () => { const beasties = new Beasties({ reduceInlineStyles: false, diff --git a/packages/beasties/test/dom.test.ts b/packages/beasties/test/dom.test.ts index 8092a00..ab9aba3 100644 --- a/packages/beasties/test/dom.test.ts +++ b/packages/beasties/test/dom.test.ts @@ -11,7 +11,7 @@ describe('dom', () => { `) - const container = doc.beastiesContainer + const container = doc.beastiesContainers[0]! /* ".parent .child" (descendant combinator) can't be resolved by the @@ -29,7 +29,7 @@ describe('dom', () => { `) - const container = doc.beastiesContainer + const container = doc.beastiesContainers[0]! /* CSS comma means OR — both selectors are logically equivalent, @@ -43,28 +43,32 @@ describe('dom', () => { const doc = createDocument(`
text
`) - expect(doc.beastiesContainer.exists('.hero')).toBe(true) + const container = doc.beastiesContainers[0]! + expect(container.exists('.hero')).toBe(true) }) it('returns false for simple class selector that does not exist', () => { const doc = createDocument(`
text
`) - expect(doc.beastiesContainer.exists('.missing')).toBe(false) + const container = doc.beastiesContainers[0]! + expect(container.exists('.missing')).toBe(false) }) it('returns true for simple id selector that exists', () => { const doc = createDocument(`
text
`) - expect(doc.beastiesContainer.exists('#main')).toBe(true) + const container = doc.beastiesContainers[0]! + expect(container.exists('#main')).toBe(true) }) it('returns false for simple id selector that does not exist', () => { const doc = createDocument(`
text
`) - expect(doc.beastiesContainer.exists('#nope')).toBe(false) + const container = doc.beastiesContainers[0]! + expect(container.exists('#nope')).toBe(false) }) }) })