Skip to content

Instantly share code, notes, and snippets.

@holloway
Created February 12, 2019 21:31
Show Gist options
  • Save holloway/091f75e61cbac984aab06fa1a661a7fb to your computer and use it in GitHub Desktop.
Save holloway/091f75e61cbac984aab06fa1a661a7fb to your computer and use it in GitHub Desktop.
JSDOM Styled-Components/Emotion CSS Rules scraper/tester that implements sheet.insertRule
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