Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save the0neWhoKnocks/cb3e1e9f61cdba7f9f32501b66d29604 to your computer and use it in GitHub Desktop.
Save the0neWhoKnocks/cb3e1e9f61cdba7f9f32501b66d29604 to your computer and use it in GitHub Desktop.
Remove Svelte classes

Remove Duplicate Svelte Classes (Loader)

Svelte has an open issue because of a "feature" that adds duplicate classses which breaks the expected cascade in favor of some inexplicable specificity. Instead of having to go through all your override/modifier rules and add !important to them, you can just add this WP plugin to strip out the duplicate classes.

// in ./.webpack/loader.remove-duplicate-svelte-classes.js

module.exports = function removeDuplicateSvelteClasses(source) {
  const SVELTE_RULE_REGEX = /\.svelte-[a-z0-9]+/g;
  const ruleMatches = (source.match(SVELTE_RULE_REGEX) || []);
  const rules = [...(new Set(ruleMatches)).values()];
  const matchedDupes = rules.reduce((arr, rule) => {
    const dupeRuleRegEx = new RegExp(`${rule}${rule}${Array(10).fill(`(?:${rule})?`).join('')}`, 'g');
    const matches = source.match(dupeRuleRegEx);
    
    if (matches) arr.push(...matches);
    
    return arr;
  }, []);
  let newSrc = source;
  
  if (matchedDupes.length) {
    // sort and reverse so that the longer dupe rules get replaced first
    const uniqueDupes = [...(new Set(matchedDupes)).values()].sort().reverse();
    
    uniqueDupes.forEach((dupeRule) => {
      const singleRule = `.${dupeRule.split('.')[1]}`;
      const allDupes = new RegExp(`(?<dupe>${dupeRule.replace(/\./g, '\\.')})(?<extra>[^{]+)?{`, 'g');
      
      [...newSrc.matchAll(allDupes)].reverse().forEach((m) => {
        const { groups: { extra = '' }, index } = m;
        const firstPart = newSrc.substr(0, index);
        const afterDupe = newSrc.substr(index + dupeRule.length, extra.length);
        const lastPart = newSrc.substr(index + dupeRule.length + extra.length, newSrc.length);
        
        // In order to maintain source maps, insert blank whitespace before the
        // starting brace. Inserting the space anywhere else could break rules.
        // Not inserting the space causes sourcemaps to point to incorrect rules.
        newSrc = `${firstPart}${singleRule}${afterDupe}${''.padEnd(dupeRule.length - singleRule.length)}${lastPart}`;
      });
    });
    
    // console.log(newSrc);
    // console.log(this.resourcePath);
  }
  
  return newSrc;
}
// in 'webpack.config.js`

const mode = process.env.NODE_ENV || 'development';
const dev = mode === 'development';

const conf = {
  devtool: dev && 'source-map',
  // --
  module: {
    rules: [
      {
        test: /\.(svelte|html)$/,
        use: {
          loader: 'svelte-loader',
          // Svelte compiler options: https://svelte.dev/docs#compile-time-svelte-compile
          options: {
            compilerOptions: { dev },
            emitCss: true,
            hotReload: false,
          },
        },
      },
      {
        test: /\.css$/, // For any CSS files that are extracted and inlined by Svelte
        use: [
          MiniCssExtractPlugin.loader,
          // translates CSS into CommonJS
          {
            loader: 'css-loader',
            options: { sourceMap: dev },
          },
          // remove duplicate svelte classes
          { loader: resolve('./.webpack/loader.remove-duplicate-svelte-classes') },
        ],
      },
    ],
  },
  // --
};

module.exports = conf;

Remove Duplicate Svelte Classes (Plugin)

Svelte has an open issue because of a "feature" that adds duplicate classses which breaks the expected cascade in favor of some inexplicable specificity. Instead of having to go through all your override/modifier rules and add !important to them, you can just add this WP plugin to strip out the duplicate classes.

// in 'webpack.config.js`

const mode = process.env.NODE_ENV || 'development';
const dev = mode === 'development';

class RemoveDupeCSSClassPlugin {
  constructor() {
    this.SVELTE_RULE_REGEX = /\.svelte-[a-z0-9]+/g;
  }
  
  apply(compiler) {
    compiler.hooks.emit.tapAsync('RemoveDupeCSSClassPlugin', (compilation, callback) => {
      const { assets } = compilation;
      const files = Object.keys(assets).filter(a => a.startsWith('css/'));
      
      files.forEach((f) => {
        const asset = assets[f];
        const { _children } = asset;
        let cssObj = asset;
        
        // '_children' exists when source maps are enabled and are separate files
        if (_children && _children.length) {
          cssObj = _children[0]; // [1] is the line that references the sourceMap file
        }
        
        const ruleMatches = (cssObj._value.match(this.SVELTE_RULE_REGEX) || []);
        const rules = [...(new Set(ruleMatches)).values()];
        const matchedDupes = rules.reduce((arr, rule) => {
          const dupeRuleRegEx = new RegExp(`${rule}${rule}${Array(10).fill(`(?:${rule})?`).join('')}`, 'g');
          const matches = cssObj._value.match(dupeRuleRegEx);
          
          if (matches) arr.push(...matches);
          
          return arr;
        }, []);
        // sort and reverse so that the longer dupe rules get replaced first
        const uniqueDupes = [...(new Set(matchedDupes)).values()].sort().reverse();
        
        uniqueDupes.forEach((dupeRule) => {
          const singleRule = `.${dupeRule.split('.')[1]}`;
          const regEx = new RegExp(dupeRule, 'g');
          
          cssObj._valueAsBuffer = Buffer.from(
            cssObj._valueAsBuffer.toString().replace(regEx, singleRule),
            'utf8'
          );
        });
      });
      
      callback();
    });
  }
}

const conf = {
  devtool: dev && 'eval-source-map',
  // --
  plugins: [
    // extract CSS first
    new MiniCssExtractPlugin(),
    // then remove any duplicate class rules 
    new RemoveDupeCSSClassPlugin(),
  ],
  // --
};

module.exports = conf;

Remove All Svelte Classes

If you want all classes removed in JS & CSS.

// in 'webpack.config.js`

const mode = process.env.NODE_ENV || 'development';
const dev = mode === 'development';

const { CachedSource, PrefixSource } = require('webpack-sources'); // eslint-disable-line node/no-extraneous-require
class RemoveSvelteCSSClassNamePlugin {
  static className = 'svelte-no-no';
  
  constructor() {
    const patterns = [
      // For .js files there'll likely be a leading space and trailing double quote since it always adds it at the end of the classname.
      `(?:\\\\"|\\s)(?<jsMatch>${RemoveSvelteCSSClassNamePlugin.className})\\\\"`,
      // For .css files there'll be a leading dot.
      `(?<cssClass>\\.${RemoveSvelteCSSClassNamePlugin.className})`,
      // Adds `<CLASSNAME>-` suffixes to CSS @keyframes names
      `(?:\\s|:)(?<cssKeyframe>${RemoveSvelteCSSClassNamePlugin.className}-)`,
    ];
    this.SVELTE_RULE_REGEX = new RegExp(`(?:${patterns.join('|')})`, 'g');
  }
  
  removeClasses(src) {
    if (src._valueAsBuffer) {
      const newVal = src._valueAsBuffer.toString().replace(
        this.SVELTE_RULE_REGEX,
        (m, ...rest) => {
          // named groups are always last
          const match = Object.values(rest.at(-1)).find(v => v);
          
          // For now, all DOM classes can't be removed because Svelte uses
          // `classList.add` which if called with nothing errors out. Parsing
          // that out of a Webpack module export would be a huge headache.
          if (m === `\\"${RemoveSvelteCSSClassNamePlugin.className}\\"`) {
            const str = rest.at(-2);
            const offset = rest.at(-3);
            
            if ( str.substr(offset - 50, 50).includes('toggle_class') ) return m;
          }
          
          return m.replace(match, '');
        }
      );
      
      src._value = newVal;
      src._valueAsBuffer = Buffer.from(newVal, 'utf8');
      src._valueAsString = newVal;
    }
  }
  
  iterateCachedSources(src) {
    const isIterable = (_src) => (
      (
        _src instanceof CachedSource
        || _src instanceof PrefixSource
      )
      && _src._source._children
    );
    
    if (isIterable(src)) {
      src._source._children.forEach((subSrc) => {
        (isIterable(subSrc))
          ? this.iterateCachedSources(subSrc)
          : this.removeClasses(subSrc);
      });
    }
    else this.removeClasses(src);
  }
  
  apply(compiler) {
    compiler.hooks.emit.tapAsync('RemoveSvelteCSSClassNamePlugin', (compilation, callback) => {
      const { assets } = compilation;
      const files = Object.keys(assets).filter(a => {
        return ( a.endsWith('.css') || a.endsWith('.js') ) && !a.includes('vendor');
      });
      
      files.forEach((f) => {
        const asset = assets[f];
        let items;
        
        // normalize items to an Array to simplifiy processing
        if (asset instanceof CachedSource) items = asset._source._children;
        else items = [asset];
        
        items.forEach((src) => {
          this.iterateCachedSources(src);
        });
      });
      
      callback();
    });
  }
}

const conf = {
  devtool: dev && 'eval-source-map',
  // --
  module: {
    rules: [
      {
        test: /\.(svelte|html)$/,
        use: {
          loader: 'svelte-loader',
          options: {
            compilerOptions: { // Svelte compiler options: https://svelte.dev/docs#compile-time-svelte-compile
              // --
              cssHash: () => RemoveSvelteCSSClassNamePlugin.className, // set a name I can easily find and remove
              // --
            },
            // --
          },
        },
      },
  // --
  plugins: [
    // extract CSS first
    new MiniCssExtractPlugin(),
    // then remove any duplicate class rules 
    new RemoveSvelteCSSClassNamePlugin(),
  ],
  // --
};

module.exports = conf;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment