<commit_before>const fs = require('fs-extra');
const globby = require('globby');
const uppercamelcase = require('uppercamelcase');
const path = require('path');
const cheerio = require('cheerio');
const prettier = require('prettier');
const SVGO = require('svgo');

const svgo = new SVGO({
  plugins: [
    {
      removeViewBox: false,
    },
    {
      removeXMLNS: true,
    },
  ],
});

const rootDir = path.join(__dirname);
const buildDir = path.join(rootDir, 'src/icons');

/**
 * Transpile Icons
 * @param {array} icons
 * @returns {Promise<[{ id, fileName, location, icon}]>} - Returns an array of objects about each icon with props: id, fileName, location, & icon
 */
async function transpileIcons(icons) {
  await fs.mkdirp(buildDir);

  return Promise.all(
    icons.map(async icon => {
      // icons.forEach(async (i) => {
      const svg = await fs.readFile(icon, 'utf-8');
      const id = path.basename(icon, '.svg').replace(/ /g, '-');

      const $ = cheerio.load(svg, {
        xmlMode: true,
      });

      const fileName = path
        .basename(icon)
        .replace(/ /g, '-')
        .replace('.svg', '.js');
      const location = path.join(buildDir, fileName);

      $('*').each((index, el) => {
        Object.keys(el.attribs).forEach(x => {
          if (x === 'stroke') {
            $(el).attr(x, 'currentColor');
          }

          if (x === 'fill') {
            $(el).attr(x, 'currentColor');
          }
        });

        if (el.name === 'svg') {
          $(el).attr('otherProps', '...');
        }

        // Remove artboard rectangle that sometimes pops up in exported SVGs
        if ($(el).attr('id') === 'Rectangle-6') {
          $(el).remove();
        }

        $(el).removeAttr('xmlns:xlink');

        if ($(el).attr('xlink:href')) {
          const xlinkHrefVal = $(el).attr('xlink:href');
          $(el).removeAttr('xlink:href');
          $(el).attr('xlinkHref', xlinkHrefVal);
        }

        if ($(el).attr('xlink:href')) {
          const xlinkHrefVal = $(el).attr('xlink:href');
          $(el).removeAttr('xlink:href');
          $(el).attr('xlinkHref', xlinkHrefVal);
        }
      });

      const result = await svgo.optimize(svg);
      const optimizedSVG = result.data;

      const element = `
  import { h } from '@bolt/core';

  export const ${uppercamelcase(
    id,
  )} = ({ bgColor, fgColor, size, ...otherProps }) => {
      return (
        ${$(optimizedSVG)
          .toString()
          .replace('fill="#FFF"', 'fill={fgColor}')
          .replace('stroke="#FFF"', 'stroke={fgColor}')
          .replace(new RegExp(/ stroke=".*?"/, 'g'), ' stroke={bgColor}')
          .replace(
            new RegExp(/ fill="(?!#fff|none).*?"/, 'g'),
            ' fill={bgColor}',
          )
          .replace('viewBox', '{...otherProps} viewBox') // tack on extra props next to viewBox attribute
          .replace('d="M0 0h24v24H0z"', '')
          .replace(/width=".*?"/, 'width={size}')
          .replace(/height=".*?"/, 'height={size}')
          .replace('otherProps="..."', '{...otherProps}')}
      )
};
`;

      const component = prettier.format(element, {
        singleQuote: true,
        trailingComma: 'es5',
        bracketSpacing: true,
        parser: 'flow',
      });

      await fs.outputFile(location, component, 'utf-8');
      // Later when we call `const icons = await transpileIcons(iconPaths);` - `icons` will be an array of these objects:
      return {
        location,
        id,
        icon,
        fileName,
      };
    }),
  );
}

function alphabetizeIconList(a, b) {
  if (a.id < b.id) return -1;
  if (a.id > b.id) return 1;
  return 0;
}

async function build() {
  try {
    const iconPaths = await globby(path.join(rootDir, 'src/svgs/**/*.svg'));
    await fs.remove(path.join(rootDir, 'src/icons')); // Clean folder
    await fs.outputFile(path.join(rootDir, 'src', 'index.js'), '', 'utf-8');
    const icons = await transpileIcons(iconPaths);
    const allExports = icons
      .sort(alphabetizeIconList) // we alphabetize the list so multiple compiles on same set doesn't result in a change that git notices
      .map(icon => `export * from './icons/${icon.id}';`); // building up `export` lines
    allExports.push(''); // Adding empty item to end of array so file has empty line at bottom to conform to `.editorconfig`
    await fs.outputFile(
      path.join(rootDir, 'src', 'index.js'),
      allExports.join('\n'),
      'utf-8',
    );
    console.log(`Built ${iconPaths.length} icons.`);
  } catch (error) {
    console.error(error);
    console.error(
      'Error trying to run "npm run build" for "@bolt/components-icons".',
    );
    process.exitCode = 1;
  }
}

build();
<commit_msg>refactor: minify SVG icons by default now; add support for colored SVG icons that won't strip out any fill or stroke colors by default<commit_after>const fs = require('fs-extra');
const globby = require('globby');
const uppercamelcase = require('uppercamelcase');
const path = require('path');
const cheerio = require('cheerio');
const prettier = require('prettier');
const SVGO = require('svgo');

const svgo = new SVGO({
  plugins: [
    {
      removeViewBox: false,
    },
    {
      removeXMLNS: true,
    },
    {
      cleanupAttrs: true,
    },
    {
      removeDoctype: true,
    },
    {
      removeXMLProcInst: true,
    },
    {
      removeComments: true,
    },
    {
      removeMetadata: true,
    },
    {
      removeTitle: true,
    },
    {
      removeDesc: true,
    },
    {
      removeUselessDefs: true,
    },
    {
      removeEditorsNSData: true,
    },
    {
      removeEmptyAttrs: true,
    },
    {
      removeHiddenElems: true,
    },
    {
      removeEmptyText: true,
    },
    {
      removeEmptyContainers: true,
    },
    {
      removeViewBox: false,
    },
    {
      cleanupEnableBackground: true,
    },
    {
      convertStyleToAttrs: true,
    },
    {
      convertColors: true,
    },
    {
      convertPathData: true,
    },
    {
      convertTransform: true,
    },
    {
      removeUnknownsAndDefaults: true,
    },
    {
      removeNonInheritableGroupAttrs: true,
    },
    {
      removeUselessStrokeAndFill: true,
    },
    {
      removeUnusedNS: true,
    },
    {
      cleanupIDs: true,
    },
    {
      cleanupNumericValues: true,
    },
    {
      moveElemsAttrsToGroup: true,
    },
    {
      moveGroupAttrsToElems: true,
    },
    {
      collapseGroups: true,
    },
    {
      removeRasterImages: false,
    },
    {
      mergePaths: true,
    },
    {
      convertShapeToPath: true,
    },
    {
      sortAttrs: true,
    },
    {
      removeDimensions: true,
    },
  ],
});

const rootDir = path.join(__dirname);
const buildDir = path.join(rootDir, 'src/icons');

/**
 * Transpile Icons
 * @param {array} icons
 * @returns {Promise<[{ id, fileName, location, icon}]>} - Returns an array of objects about each icon with props: id, fileName, location, & icon
 */
async function transpileIcons(icons) {
  await fs.mkdirp(buildDir);

  return Promise.all(
    icons.map(async icon => {
      // icons.forEach(async (i) => {
      const svg = await fs.readFile(icon, 'utf-8');
      const id = path
        .basename(icon, '.svg')
        .replace(/ /g, '-')
        .replace('.colored', '-colored');

      const $ = cheerio.load(svg, {
        xmlMode: true,
      });

      const fileName = path
        .basename(icon)
        .replace(/ /g, '-')
        .replace('.colored', '-colored')
        .replace('.svg', '.js');
      const location = path.join(buildDir, fileName);

      $('*').each((index, el) => {
        Object.keys(el.attribs).forEach(x => {
          if (x === 'stroke') {
            $(el).attr(x, 'currentColor');
          }

          if (x === 'fill') {
            $(el).attr(x, 'currentColor');
          }
        });

        if (el.name === 'svg') {
          $(el).attr('otherProps', '...');
        }

        // Remove artboard rectangle that sometimes pops up in exported SVGs
        if ($(el).attr('id') === 'Rectangle-6') {
          $(el).remove();
        }

        $(el).removeAttr('xmlns:xlink');

        if ($(el).attr('xlink:href')) {
          const xlinkHrefVal = $(el).attr('xlink:href');
          $(el).removeAttr('xlink:href');
          $(el).attr('xlinkHref', xlinkHrefVal);
        }

        if ($(el).attr('xlink:href')) {
          const xlinkHrefVal = $(el).attr('xlink:href');
          $(el).removeAttr('xlink:href');
          $(el).attr('xlinkHref', xlinkHrefVal);
        }
      });

      const result = await svgo.optimize(svg);
      const optimizedSVG = result.data;

      const element = `
  import { h } from '@bolt/core';

  export const ${uppercamelcase(
    id,
  )} = ({ bgColor, fgColor, size, ...otherProps }) => {
      return (
        ${
          fileName.includes('-color')
            ? $(optimizedSVG)
                .toString()
                .replace('viewBox', '{...otherProps} viewBox') // tack on extra props next to viewBox attribute
                .replace('d="M0 0h24v24H0z"', '')
                .replace(/width=".*?"/, 'width={size}')
                .replace(/height=".*?"/, 'height={size}')
                .replace('otherProps="..."', '{...otherProps}')
            : $(optimizedSVG)
                .toString()
                .replace('fill="#FFF"', 'fill={fgColor}')
                .replace('stroke="#FFF"', 'stroke={fgColor}')
                .replace(new RegExp(/ stroke=".*?"/, 'g'), ' stroke={bgColor}')
                .replace(
                  new RegExp(/ fill="(?!#fff|none).*?"/, 'g'),
                  ' fill={bgColor}',
                )
                .replace('viewBox', '{...otherProps} viewBox') // tack on extra props next to viewBox attribute
                .replace('d="M0 0h24v24H0z"', '')
                .replace(/width=".*?"/, 'width={size}')
                .replace(/height=".*?"/, 'height={size}')
                .replace('otherProps="..."', '{...otherProps}')
        }
      )
};
`;

      const component = prettier.format(element, {
        singleQuote: true,
        trailingComma: 'es5',
        bracketSpacing: true,
        parser: 'flow',
      });

      await fs.outputFile(location, element, 'utf-8');
      // Later when we call `const icons = await transpileIcons(iconPaths);` - `icons` will be an array of these objects:
      return {
        location,
        id,
        icon,
        fileName,
      };
    }),
  );
}

function alphabetizeIconList(a, b) {
  if (a.id < b.id) return -1;
  if (a.id > b.id) return 1;
  return 0;
}

async function build() {
  try {
    const iconPaths = await globby(path.join(rootDir, 'src/svgs/**/*.svg'));
    await fs.remove(path.join(rootDir, 'src/icons')); // Clean folder
    await fs.outputFile(path.join(rootDir, 'src', 'index.js'), '', 'utf-8');
    const icons = await transpileIcons(iconPaths);
    const allExports = icons
      .sort(alphabetizeIconList) // we alphabetize the list so multiple compiles on same set doesn't result in a change that git notices
      .map(icon => `export * from './icons/${icon.id}';`); // building up `export` lines
    allExports.push(''); // Adding empty item to end of array so file has empty line at bottom to conform to `.editorconfig`
    await fs.outputFile(
      path.join(rootDir, 'src', 'index.js'),
      allExports.join('\n'),
      'utf-8',
    );
    console.log(`Built ${iconPaths.length} icons.`);
  } catch (error) {
    console.error(error);
    console.error(
      'Error trying to run "npm run build" for "@bolt/components-icons".',
    );
    process.exitCode = 1;
  }
}

build();

{'fraction_non_alphanumeric': 0.1735210783892848, 'fraction_numerical': 0.0035202197990898943, 'mean_word_length': 2.116938720899117, 'pattern_counts': {'":': 0, '<': 7, '<?xml version=': 0, '>': 19, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 4}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1206373', 'n_tokens_mistral': 3785, 'n_tokens_neox': 3678, 'n_words': 825}