Created
February 12, 2019 21:31
-
-
Save holloway/091f75e61cbac984aab06fa1a661a7fb to your computer and use it in GitHub Desktop.
JSDOM Styled-Components/Emotion CSS Rules scraper/tester that implements sheet.insertRule
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import jsdom from 'jsdom'; | |
export const beforeParse = (window: any) => { | |
// Styled-Components doesn't write textNodes within ie `<style> .thing { font-weight: bold } </style>` | |
// instead it access the style element's `sheet` property and calls `insertRule` | |
// which is much faster, but that means it doesn't make innerText for us to scrape. | |
// These `insertRule` rules are available at `(theStyleElement).sheet.cssRules` and | |
// that's an array of `cssText` strings. | |
// | |
// See https://spectrum.chat/styled-components/help/why-is-the-inline-style-tag-empty~c43006f6-50d5-4d7d-857d-ca7fd6b68de3 | |
// | |
// So now that we know that we'll need to call those methods to extract the CSS Rules, | |
// but unfortunately JSDOM yet doesn't implement that API. | |
// | |
// See https://github.com/jsdom/jsdom/issues/2223 | |
// | |
// So this is our bare-bones implementation of the APIs that Styled-Components uses | |
// so that we can extract CSS. | |
const realCreateElement = window.document.createElement; | |
const debug = true; | |
window.document.createElement = (...createElementArgs) => { | |
if (debug) | |
console.log('beforeParse createElement with', ...createElementArgs); | |
// intercept document.createElement calls | |
const newElement = realCreateElement.call( | |
window.document, | |
...createElementArgs | |
); | |
const tagName = createElementArgs[0]; | |
if (tagName.toLowerCase() === 'style') { | |
if (debug) console.log('beforeParse found "style" with'); | |
// Styled-Components makes a <style> element, so we'll | |
// intercept that and return a custom element with our | |
// own properties that emulate the CSSOM interface; | |
// | |
// https://developer.mozilla.org/en-US/docs/Web/API/CSSStyleSheet | |
// | |
const cssRules = []; | |
Object.defineProperty( | |
newElement, | |
'sheet', | |
({ | |
get: (...args) => { | |
const sheet: Object = { | |
insertRule: (...insertRuleArgs): number => { | |
if (debug) | |
console.log('beforeParse insertRule', ...insertRuleArgs); | |
const cssText = insertRuleArgs[0]; | |
const ruleIndex = insertRuleArgs[1]; | |
cssRules.push(cssText); | |
return cssRules.length - 1; | |
}, | |
deleteRule: (...deleteRuleArgs): void => { | |
if (debug) | |
console.log('beforeParse deleteRule', ...deleteRuleArgs); | |
const ruleIndex = deleteRuleArgs[0]; | |
cssRules.pop(); | |
}, | |
}; | |
Object.defineProperty( | |
sheet, | |
'cssRules', | |
({ | |
get: (...getCssRuleArgs) => { | |
if (debug) | |
console.log('beforeParse cssRules', ...getCssRuleArgs); | |
// implement a read-only interface to prevent | |
// mutation | |
return [...cssRules]; | |
}, | |
}: Object) | |
); | |
return sheet; | |
}, | |
}: Object) | |
); | |
} | |
return newElement; | |
}; | |
}; | |
export const getJSDOMOptions = (url: string) => { | |
const virtualConsole = new jsdom.VirtualConsole(); // supress console.log | |
const options = { | |
beforeParse, // needed to intercept Styled-Components | |
virtualConsole, | |
includeNodeLocations: true, | |
pretendToBeVisual: true, | |
resources: 'usable', // needed to load external resources (script src=... references) | |
runScripts: 'dangerously', // needed to execute external scripts | |
}; | |
return options; | |
}; | |
export const getUrl = async (url: string) => { | |
const { JSDOM } = jsdom; | |
const options = getJSDOMOptions(url); | |
dom = await JSDOM.fromURL(url, options); | |
// Wait for document to fully load all external assets | |
await new Promise(resolve => { | |
dom.window.document.addEventListener('load', resolve); | |
}); | |
// wait for Styled-Components to render <style> and call insertRules. | |
await new Promise(resolve => { | |
setTimeout(resolve, 500); | |
}); | |
[...document.querySelectorAll("style")].forEach(elm => { | |
console.log(elm.sheet.cssRules); | |
}) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment