Skip to content

Instantly share code, notes, and snippets.

@samthor
Last active August 2, 2022 14:01
Show Gist options
  • Select an option

  • Save samthor/ee78b434b0f9aa525c5d235979b830aa to your computer and use it in GitHub Desktop.

Select an option

Save samthor/ee78b434b0f9aa525c5d235979b830aa to your computer and use it in GitHub Desktop.

Revisions

  1. samthor revised this gist Aug 3, 2019. 1 changed file with 60 additions and 39 deletions.
    99 changes: 60 additions & 39 deletions polyfill.js
    Original file line number Diff line number Diff line change
    @@ -1,21 +1,18 @@

    /**
    * @fileoverview Polyfill for adoptedStyleSheets and friends on ShadowRoot only.
    *
    * This ensures that <style> nodes are prepended to a ShadowRoot for corresponding CSSStyleSheet
    * instances set under adoptedStyleSheets. It doesn't hide these <style> nodes from introspection
    * in any way (i.e., patching DOM methods), but is just useful when .innerHTML is updated.
    */

    const attachedTo = Symbol('attachedTo'); // on CSSStyleSheet, set of live nodes
    const styleNode = Symbol('styleNode'); // backing <style> for CSSStyleSheet
    const styleSheet = Symbol('styleSheet'); // live node pointing to CSSStyleSheet
    const adoptedStyleSheets = Symbol('adoptedStyleSheets'); // backing for array

    function interceptCSSStyleSheet(css) {
    const style = document.createElement('style');
    style.textContent = css;
    document.documentElement.appendChild(style);
    const sheet = style.sheet;

    sheet[attachedTo] = new Set();
    sheet[styleNode] = style;

    style.remove();
    return sheet;
    }
    const adoptedStyleSheets = Symbol('adopted'); // backing for array
    const styleSheetOwner = Symbol('sso'); // which CSSStyleSheet owns <style>
    const instantiatedAdoptedStyleSheets = Symbol('real'); // sheets created for this node
    const adoptedStyleSheetsObserver = Symbol('observer');

    let hasConstructableStyleSheets = false;
    try {
    @@ -24,31 +21,45 @@ try {
    } catch (e) {
    const realCSSStyleSheet = window.CSSStyleSheet;

    // not really a ctor, just but returns a real CSSStyleSheet
    window.CSSStyleSheet = interceptCSSStyleSheet;
    // This isn't a constructor, just wraps to return a real CSSStyleSheet.
    window.CSSStyleSheet = function interceptCSSStyleSheet(css) {
    const style = document.createElement('style');
    style.textContent = css;
    document.documentElement.appendChild(style);
    const sheet = style.sheet;

    sheet[attachedTo] = new Set();
    sheet[styleNode] = style;

    style.remove();
    return sheet;
    };

    // polyfill methods on real CSSStyleSheet
    // Polyfill methods on real CSSStyleSheet.
    realCSSStyleSheet.prototype.replaceSync = function(css) {
    this[styleNode].textContent = css;
    this[attachedTo].forEach((node) => {
    node.textContent = css;
    node.textContent = css; // update all live instances
    });
    };

    // TODO: we'd need .replace() and friends too
    }

    if (!('adoptedStyleSheets' in ShadowRoot.prototype)) {
    function rectifyAdoptedStyleSheets() {
    const expected = this[adoptedStyleSheets];

    // check that nodes [0,n] are our stylesheets
    // Check that nodes [0,n] point back to the expected CSSStyleSheet.
    // nb. this doesn't guard against users making local changes to textContent
    let ok = true;
    for (let i = 0; i < expected.length; ++i) {
    const check = this.childNodes[i];
    if (!check) {
    ok = false;
    break;
    }
    if (check[styleSheet] !== expected[i]) {
    if (check[styleSheetOwner] !== expected[i]) {
    ok = false;
    break;
    }
    @@ -57,23 +68,24 @@ if (!('adoptedStyleSheets' in ShadowRoot.prototype)) {
    return;
    }

    // nuke all old stylesheets
    Array.from(this.childNodes).forEach((node) => {
    if (node[styleSheet]) {
    const sheet = node[styleSheet];
    sheet[attachedTo].remove(this);
    node.remove(); // clear all old nodes since we recreate them
    }
    // Nuke all previous instantiated sheets.
    const previousAdopted = this[instantiatedAdoptedStyleSheets] || [];
    previousAdopted.forEach((node) => {
    const sheet = node[styleSheetOwner];
    sheet[attachedTo].delete(node);
    node.remove();
    });

    // prepare clones of targeted CSSStyleSheet nodes
    // Prepare clones of target CSSStyleSheet instances.
    const toPrepend = expected.map((sheet) => {
    const clone = sheet[styleNode].cloneNode(true);
    clone[styleSheet] = sheet;
    sheet[attachedTo].add(clone);
    return clone;
    const node = sheet[styleNode].cloneNode(true);
    sheet[attachedTo].add(node);
    node[styleSheetOwner] = sheet;
    return node;
    });

    // Save for next update, prepend to Node.
    this[instantiatedAdoptedStyleSheets] = toPrepend;
    this.prepend(...toPrepend);
    }

    @@ -82,13 +94,22 @@ if (!('adoptedStyleSheets' in ShadowRoot.prototype)) {
    return this[adoptedStyleSheets] || [];
    },
    set(v) {
    const prev = this[adoptedStyleSheets];
    this[adoptedStyleSheets] = v;
    this[adoptedStyleSheets] = Object.freeze((v || []).slice());
    let observer = this[adoptedStyleSheetsObserver];

    if (!prev) {
    // TODO: Clear MutationObserer if adoptedStyleSheets are removed
    const mo = new MutationObserver(rectifyAdoptedStyleSheets.bind(this));
    mo.observe(this, {childList: true});
    if (v && v.length) {
    // insert MutationObserver if required, to detect .innerHTML changes etc
    if (!observer) {
    observer = new MutationObserver(rectifyAdoptedStyleSheets.bind(this));
    observer.observe(this, {childList: true});
    this[adoptedStyleSheetsObserver] = observer;
    }
    } else {
    // clear MutationObserver if no longer required
    if (observer) {
    observer.disconnect();
    this[adoptedStyleSheetsObserver] = null;
    }
    }

    rectifyAdoptedStyleSheets.call(this);
  2. samthor revised this gist Aug 3, 2019. 1 changed file with 98 additions and 0 deletions.
    98 changes: 98 additions & 0 deletions polyfill.js
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,98 @@

    const attachedTo = Symbol('attachedTo'); // on CSSStyleSheet, set of live nodes
    const styleNode = Symbol('styleNode'); // backing <style> for CSSStyleSheet
    const styleSheet = Symbol('styleSheet'); // live node pointing to CSSStyleSheet
    const adoptedStyleSheets = Symbol('adoptedStyleSheets'); // backing for array

    function interceptCSSStyleSheet(css) {
    const style = document.createElement('style');
    style.textContent = css;
    document.documentElement.appendChild(style);
    const sheet = style.sheet;

    sheet[attachedTo] = new Set();
    sheet[styleNode] = style;

    style.remove();
    return sheet;
    }

    let hasConstructableStyleSheets = false;
    try {
    new CSSStyleSheet();
    hasConstructableStyleSheets = true;
    } catch (e) {
    const realCSSStyleSheet = window.CSSStyleSheet;

    // not really a ctor, just but returns a real CSSStyleSheet
    window.CSSStyleSheet = interceptCSSStyleSheet;

    // polyfill methods on real CSSStyleSheet
    realCSSStyleSheet.prototype.replaceSync = function(css) {
    this[styleNode].textContent = css;
    this[attachedTo].forEach((node) => {
    node.textContent = css;
    });
    };
    }

    if (!('adoptedStyleSheets' in ShadowRoot.prototype)) {
    function rectifyAdoptedStyleSheets() {
    const expected = this[adoptedStyleSheets];

    // check that nodes [0,n] are our stylesheets
    let ok = true;
    for (let i = 0; i < expected.length; ++i) {
    const check = this.childNodes[i];
    if (!check) {
    ok = false;
    break;
    }
    if (check[styleSheet] !== expected[i]) {
    ok = false;
    break;
    }
    }
    if (ok) {
    return;
    }

    // nuke all old stylesheets
    Array.from(this.childNodes).forEach((node) => {
    if (node[styleSheet]) {
    const sheet = node[styleSheet];
    sheet[attachedTo].remove(this);
    node.remove(); // clear all old nodes since we recreate them
    }
    });

    // prepare clones of targeted CSSStyleSheet nodes
    const toPrepend = expected.map((sheet) => {
    const clone = sheet[styleNode].cloneNode(true);
    clone[styleSheet] = sheet;
    sheet[attachedTo].add(clone);
    return clone;
    });

    this.prepend(...toPrepend);
    }

    Object.defineProperty(ShadowRoot.prototype, 'adoptedStyleSheets', {
    get() {
    return this[adoptedStyleSheets] || [];
    },
    set(v) {
    const prev = this[adoptedStyleSheets];
    this[adoptedStyleSheets] = v;

    if (!prev) {
    // TODO: Clear MutationObserer if adoptedStyleSheets are removed
    const mo = new MutationObserver(rectifyAdoptedStyleSheets.bind(this));
    mo.observe(this, {childList: true});
    }

    rectifyAdoptedStyleSheets.call(this);
    }
    });

    }
  3. samthor created this gist Aug 2, 2019.
    21 changes: 21 additions & 0 deletions css-modules-plugin.mjs
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,21 @@
    import fs from 'fs';

    // as per https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/master/CSSModules/v1Explainer.md
    export default function cssModules() {
    return {
    name: 'css-modules',

    async load(id) {
    if (!id.endsWith('.css')) {
    return;
    }
    const raw = await fs.promises.readFile(id, 'utf-8');
    const encoded = JSON.stringify(raw);
    return `
    const sheet = new CSSStyleSheet();
    sheet.replaceSync(${encoded});
    export default sheet;
    `;
    }
    };
    }