code:
'use strict';

const { pathElems, referencesProps } = require('./_collections.js');

exports.name = 'moveGroupAttrsToElems';

exports.type = 'perItem';

exports.active = true;

exports.description = 'moves some group attributes to the content elements';

const pathElemsWithGroupsAndText = [...pathElems, 'g', 'text'];

/**
 * Move group attrs to the content elements.
 *
 * @example
 * <g transform="scale(2)">
 *     <path transform="rotate(45)" d="M0,0 L10,20"/>
 *     <path transform="translate(10, 20)" d="M0,10 L20,30"/>
 * </g>
 *                          ⬇
 * <g>
 *     <path transform="scale(2) rotate(45)" d="M0,0 L10,20"/>
 *     <path transform="scale(2) translate(10, 20)" d="M0,10 L20,30"/>
 * </g>
 *
 * @param {Object} item current iteration item
 * @return {Boolean} if false, item will be filtered out
 *
 * @author Kir Belevich
 */
exports.fn = function (item) {
  // move group transform attr to content's pathElems
  if (
    item.type === 'element' &&
    item.name === 'g' &&
    item.children.length !== 0 &&
    item.attributes.transform != null &&
    Object.entries(item.attributes).some(
      ([name, value]) =>
        referencesProps.includes(name) && value.includes('url(')
    ) === false &&
    item.children.every(
      (inner) =>
        pathElemsWithGroupsAndText.includes(inner.name) &&
        inner.attributes.id == null
    )
  ) {
    for (const inner of item.children) {
      const value = item.attributes.transform;
      if (inner.attributes.transform != null) {
        inner.attributes.transform = value + ' ' + inner.attributes.transform;
      } else {
        inner.attributes.transform = value;
      }
    }

    delete item.attributes.transform;
  }
};


code:
'use strict';

// builtin presets
exports['preset-default'] = require('./preset-default.js');

// builtin plugins
exports.addAttributesToSVGElement = require('./addAttributesToSVGElement.js');
exports.addClassesToSVGElement = require('./addClassesToSVGElement.js');
exports.cleanupAttrs = require('./cleanupAttrs.js');
exports.cleanupEnableBackground = require('./cleanupEnableBackground.js');
exports.cleanupIDs = require('./cleanupIDs.js');
exports.cleanupListOfValues = require('./cleanupListOfValues.js');
exports.cleanupNumericValues = require('./cleanupNumericValues.js');
exports.collapseGroups = require('./collapseGroups.js');
exports.convertColors = require('./convertColors.js');
exports.convertEllipseToCircle = require('./convertEllipseToCircle.js');
exports.convertPathData = require('./convertPathData.js');
exports.convertShapeToPath = require('./convertShapeToPath.js');
exports.convertStyleToAttrs = require('./convertStyleToAttrs.js');
exports.convertTransform = require('./convertTransform.js');
exports.mergeStyles = require('./mergeStyles.js');
exports.inlineStyles = require('./inlineStyles.js');
exports.mergePaths = require('./mergePaths.js');
exports.minifyStyles = require('./minifyStyles.js');
exports.moveElemsAttrsToGroup = require('./moveElemsAttrsToGroup.js');
exports.moveGroupAttrsToElems = require('./moveGroupAttrsToElems.js');
exports.prefixIds = require('./prefixIds.js');
exports.removeAttributesBySelector = require('./removeAttributesBySelector.js');
exports.removeAttrs = require('./removeAttrs.js');
exports.removeComments = require('./removeComments.js');
exports.removeDesc = require('./removeDesc.js');
exports.removeDimensions = require('./removeDimensions.js');
exports.removeDoctype = require('./removeDoctype.js');
exports.removeEditorsNSData = require('./removeEditorsNSData.js');
exports.removeElementsByAttr = require('./removeElementsByAttr.js');
exports.removeEmptyAttrs = require('./removeEmptyAttrs.js');
exports.removeEmptyContainers = require('./removeEmptyContainers.js');
exports.removeEmptyText = require('./removeEmptyText.js');
exports.removeHiddenElems = require('./removeHiddenElems.js');
exports.removeMetadata = require('./removeMetadata.js');
exports.removeNonInheritableGroupAttrs = require('./removeNonInheritableGroupAttrs.js');
exports.removeOffCanvasPaths = require('./removeOffCanvasPaths.js');
exports.removeRasterImages = require('./removeRasterImages.js');
exports.removeScriptElement = require('./removeScriptElement.js');
exports.removeStyleElement = require('./removeStyleElement.js');
exports.removeTitle = require('./removeTitle.js');
exports.removeUnknownsAndDefaults = require('./removeUnknownsAndDefaults.js');
exports.removeUnusedNS = require('./removeUnusedNS.js');
exports.removeUselessDefs = require('./removeUselessDefs.js');
exports.removeUselessStrokeAndFill = require('./removeUselessStrokeAndFill.js');
exports.removeViewBox = require('./removeViewBox.js');
exports.removeXMLNS = require('./removeXMLNS.js');
exports.removeXMLProcInst = require('./removeXMLProcInst.js');
exports.reusePaths = require('./reusePaths.js');
exports.sortAttrs = require('./sortAttrs.js');
exports.sortDefsChildren = require('./sortDefsChildren.js');

license: mit


code:
'use strict';

const csstree = require('css-tree');
const { referencesProps } = require('./_collections.js');

/**
 * @typedef {import('../lib/types').XastElement} XastElement
 * @typedef {import('../lib/types').PluginInfo} PluginInfo
 */

exports.type = 'visitor';
exports.name = 'prefixIds';
exports.active = false;
exports.description = 'prefix IDs';

/**
 * extract basename from path
 * @type {(path: string) => string}
 */
const getBasename = (path) => {
  // extract everything after latest slash or backslash
  const matched = path.match(/[/\\]?([^/\\]+)$/);
  if (matched) {
    return matched[1];
  }
  return '';
};

/**
 * escapes a string for being used as ID
 * @type {(string: string) => string}
 */
const escapeIdentifierName = (str) => {
  return str.replace(/[. ]/g, '_');
};

/**
 * @type {(string: string) => string}
 */
const unquote = (string) => {
  if (
    (string.startsWith('"') && string.endsWith('"')) ||
    (string.startsWith("'") && string.endsWith("'"))
  ) {
    return string.slice(1, -1);
  }
  return string;
};

/**
 * prefix an ID
 * @type {(prefix: string, name: string) => string}
 */
const prefixId = (prefix, value) => {
  if (value.startsWith(prefix)) {
    return value;
  }
  return prefix + value;
};

/**
 * prefix an #ID
 * @type {(prefix: string, name: string) => string | null}
 */
const prefixReference = (prefix, value) => {
  if (value.startsWith('#')) {
    return '#' + prefixId(prefix, value.slice(1));
  }
  return null;
};

/**
 * Prefixes identifiers
 *
 * @author strarsis <strarsis@gmail.com>
 *
 * @type {import('../lib/types').Plugin<{
 *   prefix?: boolean | string | ((node: XastElement, info: PluginInfo) => string),
 *   delim?: string,
 *   prefixIds?: boolean,
 *   prefixClassNames?: boolean,
 * }>}
 */
exports.fn = (_root, params, info) => {
  const { delim = '__', prefixIds = true, prefixClassNames = true } = params;

  return {
    element: {
      enter: (node) => {
        /**
         * prefix, from file name or option
         * @type {string}
         */
        let prefix = 'prefix' + delim;
        if (typeof params.prefix === 'function') {
          prefix = params.prefix(node, info) + delim;
        } else if (typeof params.prefix === 'string') {
          prefix = params.prefix + delim;
        } else if (params.prefix === false) {
          prefix = '';
        } else if (info.path != null && info.path.length > 0) {
          prefix = escapeIdentifierName(getBasename(info.path)) + delim;
        }

        // prefix id/class selectors and url() references in styles
        if (node.name === 'style') {
          // skip empty <style/> elements
          if (node.children.length === 0) {
            return;
          }

          // parse styles
          let cssText = '';
          if (
            node.children[0].type === 'text' ||
            node.children[0].type === 'cdata'
          ) {
            cssText = node.children[0].value;
          }
          /**
           * @type {null | csstree.CssNode}
           */
          let cssAst = null;
          try {
            cssAst = csstree.parse(cssText, {
              parseValue: true,
              parseCustomProperty: false,
            });
          } catch {
            return;
          }

          csstree.walk(cssAst, (node) => {
            // #ID, .class selectors
            if (
              (prefixIds && node.type === 'IdSelector') ||
              (prefixClassNames && node.type === 'ClassSelector')
            ) {
              node.name = prefixId(prefix, node.name);
              return;
            }
            // url(...) references
            if (
              node.type === 'Url' &&
              node.value.value &&
              node.value.value.length > 0
            ) {
              const prefixed = prefixReference(
                prefix,
                unquote(node.value.value)
              );
              if (prefixed != null) {
                node.value.value = prefixed;
              }
            }
          });

          // update styles
          if (
            node.children[0].type === 'text' ||
            node.children[0].type === 'cdata'
          ) {
            node.children[0].value = csstree.generate(cssAst);
          }
          return;
        }

        // prefix an ID attribute value
        if (
          prefixIds &&
          node.attributes.id != null &&
          node.attributes.id.length !== 0
        ) {
          node.attributes.id = prefixId(prefix, node.attributes.id);
        }

        // prefix a class attribute value
        if (
          prefixClassNames &&
          node.attributes.class != null &&
          node.attributes.class.length !== 0
        ) {
          node.attributes.class = node.attributes.class
            .split(/\s+/)
            .map((name) => prefixId(prefix, name))
            .join(' ');
        }

        // prefix a href attribute value
        // xlink:href is deprecated, must be still supported
        for (const name of ['href', 'xlink:href']) {
          if (
            node.attributes[name] != null &&
            node.attributes[name].length !== 0
          ) {
            const prefixed = prefixReference(prefix, node.attributes[name]);
            if (prefixed != null) {
              node.attributes[name] = prefixed;
            }
          }
        }

        // prefix an URL attribute value
        for (const name of referencesProps) {
          if (
            node.attributes[name] != null &&
            node.attributes[name].length !== 0
          ) {
            node.attributes[name] = node.attributes[name].replace(
              /url\((.*?)\)/gi,
              (match, url) => {
                const prefixed = prefixReference(prefix, url);
                if (prefixed == null) {
                  return match;
                }
                return `url(${prefixed})`;
              }
            );
          }
        }

        // prefix begin/end attribute value
        for (const name of ['begin', 'end']) {
          if (
            node.attributes[name] != null &&
            node.attributes[name].length !== 0
          ) {
            const parts = node.attributes[name].split(/\s*;\s+/).map((val) => {
              if (val.endsWith('.end') || val.endsWith('.start')) {
                const [id, postfix] = val.split('.');
                return `${prefixId(prefix, id)}.${postfix}`;
              }
              return val;
            });
            node.attributes[name] = parts.join('; ');
          }
        }
      },
    },
  };
};


code:
'use strict';

const { createPreset } = require('../lib/svgo/plugins.js');

const removeDoctype = require('./removeDoctype.js');
const removeXMLProcInst = require('./removeXMLProcInst.js');
const removeComments = require('./removeComments.js');
const removeMetadata = require('./removeMetadata.js');
const removeEditorsNSData = require('./removeEditorsNSData.js');
const cleanupAttrs = require('./cleanupAttrs.js');
const mergeStyles = require('./mergeStyles.js');
const inlineStyles = require('./inlineStyles.js');
const minifyStyles = require('./minifyStyles.js');
const cleanupIDs = require('./cleanupIDs.js');
const removeUselessDefs = require('./removeUselessDefs.js');
const cleanupNumericValues = require('./cleanupNumericValues.js');
const convertColors = require('./convertColors.js');
const removeUnknownsAndDefaults = require('./removeUnknownsAndDefaults.js');
const removeNonInheritableGroupAttrs = require('./removeNonInheritableGroupAttrs.js');
const removeUselessStrokeAndFill = require('./removeUselessStrokeAndFill.js');
const removeViewBox = require('./removeViewBox.js');
const cleanupEnableBackground = require('./cleanupEnableBackground.js');
const removeHiddenElems = require('./removeHiddenElems.js');
const removeEmptyText = require('./removeEmptyText.js');
const convertShapeToPath = require('./convertShapeToPath.js');
const convertEllipseToCircle = require('./convertEllipseToCircle.js');
const moveElemsAttrsToGroup = require('./moveElemsAttrsToGroup.js');
const moveGroupAttrsToElems = require('./moveGroupAttrsToElems.js');
const collapseGroups = require('./collapseGroups.js');
const convertPathData = require('./convertPathData.js');
const convertTransform = require('./convertTransform.js');
const removeEmptyAttrs = require('./removeEmptyAttrs.js');
const removeEmptyContainers = require('./removeEmptyContainers.js');
const mergePaths = require('./mergePaths.js');
const removeUnusedNS = require('./removeUnusedNS.js');
const sortDefsChildren = require('./sortDefsChildren.js');
const removeTitle = require('./removeTitle.js');
const removeDesc = require('./removeDesc.js');

const presetDefault = createPreset({
  name: 'presetDefault',
  plugins: [
    removeDoctype,
    removeXMLProcInst,
    removeComments,
    removeMetadata,
    removeEditorsNSData,
    cleanupAttrs,
    mergeStyles,
    inlineStyles,
    minifyStyles,
    cleanupIDs,
    removeUselessDefs,
    cleanupNumericValues,
    convertColors,
    removeUnknownsAndDefaults,
    removeNonInheritableGroupAttrs,
    removeUselessStrokeAndFill,
    removeViewBox,
    cleanupEnableBackground,
    removeHiddenElems,
    removeEmptyText,
    convertShapeToPath,
    convertEllipseToCircle,
    moveElemsAttrsToGroup,
    moveGroupAttrsToElems,
    collapseGroups,
    convertPathData,
    convertTransform,
    removeEmptyAttrs,
    removeEmptyContainers,
    mergePaths,
    removeUnusedNS,
    sortDefsChildren,
    removeTitle,
    removeDesc,
  ],
});

module.exports = presetDefault;


code:
'use strict';

const { querySelectorAll } = require('../lib/xast.js');

exports.name = 'removeAttributesBySelector';
exports.type = 'visitor';
exports.active = false;
exports.description =
  'removes attributes of elements that match a css selector';

/**
 * Removes attributes of elements that match a css selector.
 *
 * @example
 *
A selector removing a single attribute

 * plugins: [
 *   {
 *     name: "removeAttributesBySelector",
 *     params: {
 *       selector: "[fill='#00ff00']"
 *       attributes: "fill"
 *     }
 *   }
 * ]
 *
 * <rect x="0" y="0" width="100" height="100" fill="#00ff00" stroke="#00ff00"/>
 *   ↓
 * <rect x="0" y="0" width="100" height="100" stroke="#00ff00"/>
 *
 *
A selector removing multiple attributes

 * plugins: [
 *   {
 *     name: "removeAttributesBySelector",
 *     params: {
 *       selector: "[fill='#00ff00']",
 *       attributes: [
 *         "fill",
 *         "stroke"
 *       ]
 *     }
 *   }
 * ]
 *
 * <rect x="0" y="0" width="100" height="100" fill="#00ff00" stroke="#00ff00"/>
 *   ↓
 * <rect x="0" y="0" width="100" height="100"/>
 *
 *
Multiple selectors removing attributes

 * plugins: [
 *   {
 *     name: "removeAttributesBySelector",
 *     params: {
 *       selectors: [
 *         {
 *           selector: "[fill='#00ff00']",
 *           attributes: "fill"
 *         },
 *         {
 *           selector: "#remove",
 *           attributes: [
 *             "stroke",
 *             "id"
 *           ]
 *         }
 *       ]
 *     }
 *   }
 * ]
 *
 * <rect x="0" y="0" width="100" height="100" fill="#00ff00" stroke="#00ff00"/>
 *   ↓
 * <rect x="0" y="0" width="100" height="100"/>
 *
 * @link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|MDN CSS Selectors
 *
 * @author Bradley Mease
 *
 * @type {import('../lib/types').Plugin<any>}
 */
exports.fn = (root, params) => {
  const selectors = Array.isArray(params.selectors)
    ? params.selectors
    : [params];
  for (const { selector, attributes } of selectors) {
    const nodes = querySelectorAll(root, selector);
    for (const node of nodes) {
      if (node.type === 'element') {
        if (Array.isArray(attributes)) {
          for (const name of attributes) {
            delete node.attributes[name];
          }
        } else {
          delete node.attributes[attributes];
        }
      }
    }
  }
  return {};
};


code:
'use strict';

exports.name = 'removeAttrs';
exports.type = 'visitor';
exports.active = false;
exports.description = 'removes specified attributes';

const DEFAULT_SEPARATOR = ':';
const ENOATTRS = `Warning: The plugin "removeAttrs" requires the "attrs" parameter.
It should have a pattern to remove, otherwise the plugin is a noop.
Config example:

plugins: [
  {
    name: "removeAttrs",
    params: {
      attrs: "(fill|stroke)"
    }
  }
]
`;

/**
 * Remove attributes
 *
 * @example elemSeparator
 *   format: string
 *
 * @example preserveCurrentColor
 *   format: boolean
 *
 * @example attrs:
 *
 *   format: [ element* : attribute* : value* ]
 *
 *   element   : regexp (wrapped into ^...$), single * or omitted > all elements (must be present when value is used)
 *   attribute : regexp (wrapped into ^...$)
 *   value     : regexp (wrapped into ^...$), single * or omitted > all values
 *
 *   examples:
 *
 *     > basic: remove fill attribute
 *     ---
 *     removeAttrs:
 *       attrs: 'fill'
 *
 *     > remove fill attribute on path element
 *     ---
 *       attrs: 'path:fill'
 *
 *     > remove fill attribute on path element where value is none
 *     ---
 *       attrs: 'path:fill:none'
 *
 *
 *     > remove all fill and stroke attribute
 *     ---
 *       attrs:
 *         - 'fill'
 *         - 'stroke'
 *
 *     [is same as]
 *
 *       attrs: '(fill|stroke)'
 *
 *     [is same as]
 *
 *       attrs: '*:(fill|stroke)'
 *
 *     [is same as]
 *
 *       attrs: '.*:(fill|stroke)'
 *
 *     [is same as]
 *
 *       attrs: '.*:(fill|stroke):.*'
 *
 *
 *     > remove all stroke related attributes
 *     ----
 *     attrs: 'stroke.*'
 *
 *
 * @author Benny Schudel
 *
 * @type {import('../lib/types').Plugin<{
 *   elemSeparator?: string,
 *   preserveCurrentColor?: boolean,
 *   attrs: string | Array<string>
 * }>}
 */
exports.fn = (root, params) => {
  if (typeof params.attrs == 'undefined') {
    console.warn(ENOATTRS);
    return null;
  }

  const elemSeparator =
    typeof params.elemSeparator == 'string'
      ? params.elemSeparator
      : DEFAULT_SEPARATOR;
  const preserveCurrentColor =
    typeof params.preserveCurrentColor == 'boolean'
      ? params.preserveCurrentColor
      : false;
  const attrs = Array.isArray(params.attrs) ? params.attrs : [params.attrs];

  return {
    element: {
      enter: (node) => {
        for (let pattern of attrs) {
          // if no element separators (:), assume it's attribute name, and apply to all elements *regardless of value*
          if (pattern.includes(elemSeparator) === false) {
            pattern = ['.*', elemSeparator, pattern, elemSeparator, '.*'].join(
              ''
            );
            // if only 1 separator, assume it's element and attribute name, and apply regardless of attribute value
          } else if (pattern.split(elemSeparator).length < 3) {
            pattern = [pattern, elemSeparator, '.*'].join('');
          }

          // create regexps for element, attribute name, and attribute value
          const list = pattern.split(elemSeparator).map((value) => {
            // adjust single * to match anything
            if (value === '*') {
              value = '.*';
            }
            return new RegExp(['^', value, '$'].join(''), 'i');
          });

          // matches element
          if (list[0].test(node.name)) {
            // loop attributes
            for (const [name, value] of Object.entries(node.attributes)) {
              const isFillCurrentColor =
                preserveCurrentColor &&
                name == 'fill' &&
                value == 'currentColor';
              const isStrokeCurrentColor =
                preserveCurrentColor &&
                name == 'stroke' &&
                value == 'currentColor';
              if (
                !isFillCurrentColor &&
                !isStrokeCurrentColor &&
                // matches attribute name
                list[1].test(name) &&
                // matches attribute value
                list[2].test(value)
              ) {
                delete node.attributes[name];
              }
            }
          }
        }
      },
    },
  };
};


code:
'use strict';

const { detachNodeFromParent } = require('../lib/xast.js');

exports.name = 'removeComments';
exports.type = 'visitor';
exports.active = true;
exports.description = 'removes comments';

/**
 * Remove comments.
 *
 * @example
 * <!-- Generator: Adobe Illustrator 15.0.0, SVG Export
 * Plug-In . SVG Version: 6.00 Build 0)  -->
 *
 * @author Kir Belevich
 *
 * @type {import('../lib/types').Plugin<void>}
 */
exports.fn = () => {
  return {
    comment: {
      enter: (node, parentNode) => {
        if (node.value.charAt(0) !== '!') {
          detachNodeFromParent(node, parentNode);
        }
      },
    },
  };
};


code:
'use strict';

const { detachNodeFromParent } = require('../lib/xast.js');

exports.name = 'removeDesc';
exports.type = 'visitor';
exports.active = true;
exports.description = 'removes <desc>';

const standardDescs = /^(Created with|Created using)/;

/**
 * Removes <desc>.
 * Removes only standard editors content or empty elements 'cause it can be used for accessibility.
 * Enable parameter 'removeAny' to remove any description.
 *
 * https://developer.mozilla.org/en-US/docs/Web/SVG/Element/desc
 *
 * @author Daniel Wabyick
 *
 * @type {import('../lib/types').Plugin<{ removeAny?: boolean }>}
 */
exports.fn = (root, params) => {
  const { removeAny = true } = params;
  return {
    element: {
      enter: (node, parentNode) => {
        if (node.name === 'desc') {
          if (
            removeAny ||
            node.children.length === 0 ||
            (node.children[0].type === 'text' &&
              standardDescs.test(node.children[0].value))
          ) {
            detachNodeFromParent(node, parentNode);
          }
        }
      },
    },
  };
};


code:
'use strict';

exports.name = 'removeDimensions';

exports.type = 'perItem';

exports.active = false;

exports.description =
  'removes width and height in presence of viewBox (opposite to removeViewBox, disable it first)';

/**
 * Remove width/height attributes and add the viewBox attribute if it's missing
 *
 * @example
 * <svg width="100" height="50" />
 *   ↓
 * <svg viewBox="0 0 100 50" />
 *
 * @param {Object} item current iteration item
 * @return {Boolean} if true, with and height will be filtered out
 *
 * @author Benny Schudel
 */
exports.fn = function (item) {
  if (item.type === 'element' && item.name === 'svg') {
    if (item.attributes.viewBox != null) {
      delete item.attributes.width;
      delete item.attributes.height;
    } else if (
      item.attributes.width != null &&
      item.attributes.height != null &&
      Number.isNaN(Number(item.attributes.width)) === false &&
      Number.isNaN(Number(item.attributes.height)) === false
    ) {
      const width = Number(item.attributes.width);
      const height = Number(item.attributes.height);
      item.attributes.viewBox = `0 0 ${width} ${height}`;
      delete item.attributes.width;
      delete item.attributes.height;
    }
  }
};


code:
'use strict';

const { detachNodeFromParent } = require('../lib/xast.js');

exports.name = 'removeDoctype';
exports.type = 'visitor';
exports.active = true;
exports.description = 'removes doctype declaration';

/**
 * Remove DOCTYPE declaration.
 *
 * "Unfortunately the SVG DTDs are a source of so many
 * issues that the SVG WG has decided not to write one
 * for the upcoming SVG 1.2 standard. In fact SVG WG
 * members are even telling people not to use a DOCTYPE
 * declaration in SVG 1.0 and 1.1 documents"
 * https://jwatt.org/svg/authoring/#doctype-declaration
 *
 * @example
 * <!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
 * q"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
 *
 * @example
 * <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 * "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd" [
 *     <!-- an internal subset can be embedded here -->
 * ]>
 *
 * @author Kir Belevich
 *
 * @type {import('../lib/types').Plugin<void>}
 */
exports.fn = () => {
  return {
    doctype: {
      enter: (node, parentNode) => {
        detachNodeFromParent(node, parentNode);
      },
    },
  };
};


code:
'use strict';

const { detachNodeFromParent } = require('../lib/xast.js');
const { editorNamespaces } = require('./_collections.js');

exports.type = 'visitor';
exports.name = 'removeEditorsNSData';
exports.active = true;
exports.description = 'removes editors namespaces, elements and attributes';

/**
 * Remove editors namespaces, elements and attributes.
 *
 * @example
 * <svg xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd">
 * <sodipodi:namedview/>
 * <path sodipodi:nodetypes="cccc"/>
 *
 * @author Kir Belevich
 *
 * @type {import('../lib/types').Plugin<{
 *   additionalNamespaces?: Array<string>
 * }>}
 */
exports.fn = (_root, params) => {
  let namespaces = editorNamespaces;
  if (Array.isArray(params.additionalNamespaces)) {
    namespaces = [...editorNamespaces, ...params.additionalNamespaces];
  }
  /**
   * @type {Array<string>}
   */
  const prefixes = [];
  return {
    element: {
      enter: (node, parentNode) => {
        // collect namespace aliases from svg element
        if (node.name === 'svg') {
          for (const [name, value] of Object.entries(node.attributes)) {
            if (name.startsWith('xmlns:') && namespaces.includes(value)) {
              prefixes.push(name.slice('xmlns:'.length));
              // <svg xmlns:sodipodi="">
              delete node.attributes[name];
            }
          }
        }
        // remove editor attributes, for example
        // <* sodipodi:*="">
        for (const name of Object.keys(node.attributes)) {
          if (name.includes(':')) {
            const [prefix] = name.split(':');
            if (prefixes.includes(prefix)) {
              delete node.attributes[name];
            }
          }
        }
        // remove editor elements, for example
        // <sodipodi:*>
        if (node.name.includes(':')) {
          const [prefix] = node.name.split(':');
          if (prefixes.includes(prefix)) {
            detachNodeFromParent(node, parentNode);
          }
        }
      },
    },
  };
};


code:
'use strict';

const { detachNodeFromParent } = require('../lib/xast.js');

exports.name = 'removeElementsByAttr';
exports.type = 'visitor';
exports.active = false;
exports.description =
  'removes arbitrary elements by ID or className (disabled by default)';

/**
 * Remove arbitrary SVG elements by ID or className.
 *
 * @example id
 *     > single: remove element with ID of `elementID`
 *     ---
 *     removeElementsByAttr:
 *       id: 'elementID'
 *
 *     > list: remove multiple elements by ID
 *     ---
 *     removeElementsByAttr:
 *       id:
 *         - 'elementID'
 *         - 'anotherID'
 *
 * @example class
 *     > single: remove all elements with class of `elementClass`
 *     ---
 *     removeElementsByAttr:
 *       class: 'elementClass'
 *
 *     > list: remove all elements with class of `elementClass` or `anotherClass`
 *     ---
 *     removeElementsByAttr:
 *       class:
 *         - 'elementClass'
 *         - 'anotherClass'
 *
 * @author Eli Dupuis (@elidupuis)
 *
 * @type {import('../lib/types').Plugin<{
 *   id?: string | Array<string>,
 *   class?: string | Array<string>
 * }>}
 */
exports.fn = (root, params) => {
  const ids =
    params.id == null ? [] : Array.isArray(params.id) ? params.id : [params.id];
  const classes =
    params.class == null
      ? []
      : Array.isArray(params.class)
      ? params.class
      : [params.class];
  return {
    element: {
      enter: (node, parentNode) => {
        // remove element if it's `id` matches configured `id` params
        if (node.attributes.id != null && ids.length !== 0) {
          if (ids.includes(node.attributes.id)) {
            detachNodeFromParent(node, parentNode);
          }
        }
        // remove element if it's `class` contains any of the configured `class` params
        if (node.attributes.class && classes.length !== 0) {
          const classList = node.attributes.class.split(' ');
          for (const item of classes) {
            if (classList.includes(item)) {
              detachNodeFromParent(node, parentNode);
              break;
            }
          }
        }
      },
    },
  };
};


code:
'use strict';

const { attrsGroups } = require('./_collections.js');

exports.type = 'visitor';
exports.name = 'removeEmptyAttrs';
exports.active = true;
exports.description = 'removes empty attributes';

/**
 * Remove attributes with empty values.
 *
 * @author Kir Belevich
 *
 * @type {import('../lib/types').Plugin<void>}
 */
exports.fn = () => {
  return {
    element: {
      enter: (node) => {
        for (const [name, value] of Object.entries(node.attributes)) {
          if (
            value === '' &&
            // empty conditional processing attributes prevents elements from rendering
            attrsGroups.conditionalProcessing.includes(name) === false
          ) {
            delete node.attributes[name];
          }
        }
      },
    },
  };
};


code:
'use strict';

const { detachNodeFromParent } = require('../lib/xast.js');
const { elemsGroups } = require('./_collections.js');

exports.type = 'visitor';
exports.name = 'removeEmptyContainers';
exports.active = true;
exports.description = 'removes empty container elements';

/**
 * Remove empty containers.
 *
 * @see https://www.w3.org/TR/SVG11/intro.html#TermContainerElement
 *
 * @example
 * <defs/>
 *
 * @example
 * <g><marker><a/></marker></g>
 *
 * @author Kir Belevich
 *
 * @type {import('../lib/types').Plugin<void>}
 */
exports.fn = () => {
  return {
    element: {
      exit: (node, parentNode) => {
        // remove only empty non-svg containers
        if (
          node.name === 'svg' ||
          elemsGroups.container.includes(node.name) === false ||
          node.children.length !== 0
        ) {
          return;
        }
        // empty patterns may contain reusable configuration
        if (
          node.name === 'pattern' &&
          Object.keys(node.attributes).length !== 0
        ) {
          return;
        }
        // The <g> may not have content, but the filter may cause a rectangle
        // to be created and filled with pattern.
        if (node.name === 'g' && node.attributes.filter != null) {
          return;
        }
        // empty <mask> hides masked element
        if (node.name === 'mask' && node.attributes.id != null) {
          return;
        }
        detachNodeFromParent(node, parentNode);
      },
    },
  };
};


code:
'use strict';

const { detachNodeFromParent } = require('../lib/xast.js');

exports.name = 'removeEmptyText';
exports.type = 'visitor';
exports.active = true;
exports.description = 'removes empty <text> elements';

/**
 * Remove empty Text elements.
 *
 * @see https://www.w3.org/TR/SVG11/text.html
 *
 * @example
 * Remove empty text element:
 * <text/>
 *
 * Remove empty tspan element:
 * <tspan/>
 *
 * Remove tref with empty xlink:href attribute:
 * <tref xlink:href=""/>
 *
 * @author Kir Belevich
 *
 * @type {import('../lib/types').Plugin<{
 *   text?: boolean,
 *   tspan?: boolean,
 *   tref?: boolean
 * }>}
 */
exports.fn = (root, params) => {
  const { text = true, tspan = true, tref = true } = params;
  return {
    element: {
      enter: (node, parentNode) => {
        // Remove empty text element
        if (text && node.name === 'text' && node.children.length === 0) {
          detachNodeFromParent(node, parentNode);
        }
        // Remove empty tspan element
        if (tspan && node.name === 'tspan' && node.children.length === 0) {
          detachNodeFromParent(node, parentNode);
        }
        // Remove tref with empty xlink:href attribute
        if (
          tref &&
          node.name === 'tref' &&
          node.attributes['xlink:href'] == null
        ) {
          detachNodeFromParent(node, parentNode);
        }
      },
    },
  };
};


code:
'use strict';

const {
  querySelector,
  closestByName,
  detachNodeFromParent,
} = require('../lib/xast.js');
const { collectStylesheet, computeStyle } = require('../lib/style.js');
const { parsePathData } = require('../lib/path.js');

exports.name = 'removeHiddenElems';
exports.type = 'visitor';
exports.active = true;
exports.description =
  'removes hidden elements (zero sized, with absent attributes)';

/**
 * Remove hidden elements with disabled rendering:
 * - display="none"
 * - opacity="0"
 * - circle with zero radius
 * - ellipse with zero x-axis or y-axis radius
 * - rectangle with zero width or height
 * - pattern with zero width or height
 * - image with zero width or height
 * - path with empty data
 * - polyline with empty points
 * - polygon with empty points
 *
 * @author Kir Belevich
 *
 * @type {import('../lib/types').Plugin<{
 *   isHidden: boolean,
 *   displayNone: boolean,
 *   opacity0: boolean,
 *   circleR0: boolean,
 *   ellipseRX0: boolean,
 *   ellipseRY0: boolean,
 *   rectWidth0: boolean,
 *   rectHeight0: boolean,
 *   patternWidth0: boolean,
 *   patternHeight0: boolean,
 *   imageWidth0: boolean,
 *   imageHeight0: boolean,
 *   pathEmptyD: boolean,
 *   polylineEmptyPoints: boolean,
 *   polygonEmptyPoints: boolean,
 * }>}
 */
exports.fn = (root, params) => {
  const {
    isHidden = true,
    displayNone = true,
    opacity0 = true,
    circleR0 = true,
    ellipseRX0 = true,
    ellipseRY0 = true,
    rectWidth0 = true,
    rectHeight0 = true,
    patternWidth0 = true,
    patternHeight0 = true,
    imageWidth0 = true,
    imageHeight0 = true,
    pathEmptyD = true,
    polylineEmptyPoints = true,
    polygonEmptyPoints = true,
  } = params;
  const stylesheet = collectStylesheet(root);

  return {
    element: {
      enter: (node, parentNode) => {
        // Removes hidden elements
        // https://www.w3schools.com/cssref/pr_class_visibility.asp
        const computedStyle = computeStyle(stylesheet, node);
        if (
          isHidden &&
          computedStyle.visibility &&
          computedStyle.visibility.type === 'static' &&
          computedStyle.visibility.value === 'hidden' &&
          // keep if any descendant enables visibility
          querySelector(node, '[visibility=visible]') == null
        ) {
          detachNodeFromParent(node, parentNode);
          return;
        }

        // display="none"
        //
        // https://www.w3.org/TR/SVG11/painting.html#DisplayProperty
        // "A value of display: none indicates that the given element
        // and its children shall not be rendered directly"
        if (
          displayNone &&
          computedStyle.display &&
          computedStyle.display.type === 'static' &&
          computedStyle.display.value === 'none' &&
          // markers with display: none still rendered
          node.name !== 'marker'
        ) {
          detachNodeFromParent(node, parentNode);
          return;
        }

        // opacity="0"
        //
        // https://www.w3.org/TR/SVG11/masking.html#ObjectAndGroupOpacityProperties
        if (
          opacity0 &&
          computedStyle.opacity &&
          computedStyle.opacity.type === 'static' &&
          computedStyle.opacity.value === '0' &&
          // transparent element inside clipPath still affect clipped elements
          closestByName(node, 'clipPath') == null
        ) {
          detachNodeFromParent(node, parentNode);
          return;
        }

        // Circles with zero radius
        //
        // https://www.w3.org/TR/SVG11/shapes.html#CircleElementRAttribute
        // "A value of zero disables rendering of the element"
        //
        // <circle r="0">
        if (
          circleR0 &&
          node.name === 'circle' &&
          node.children.length === 0 &&
          node.attributes.r === '0'
        ) {
          detachNodeFromParent(node, parentNode);
          return;
        }

        // Ellipse with zero x-axis radius
        //
        // https://www.w3.org/TR/SVG11/shapes.html#EllipseElementRXAttribute
        // "A value of zero disables rendering of the element"
        //
        // <ellipse rx="0">
        if (
          ellipseRX0 &&
          node.name === 'ellipse' &&
          node.children.length === 0 &&
          node.attributes.rx === '0'
        ) {
          detachNodeFromParent(node, parentNode);
          return;
        }

        // Ellipse with zero y-axis radius
        //
        // https://www.w3.org/TR/SVG11/shapes.html#EllipseElementRYAttribute
        // "A value of zero disables rendering of the element"
        //
        // <ellipse ry="0">
        if (
          ellipseRY0 &&
          node.name === 'ellipse' &&
          node.children.length === 0 &&
          node.attributes.ry === '0'
        ) {
          detachNodeFromParent(node, parentNode);
          return;
        }

        // Rectangle with zero width
        //
        // https://www.w3.org/TR/SVG11/shapes.html#RectElementWidthAttribute
        // "A value of zero disables rendering of the element"
        //
        // <rect width="0">
        if (
          rectWidth0 &&
          node.name === 'rect' &&
          node.children.length === 0 &&
          node.attributes.width === '0'
        ) {
          detachNodeFromParent(node, parentNode);
          return;
        }

        // Rectangle with zero height
        //
        // https://www.w3.org/TR/SVG11/shapes.html#RectElementHeightAttribute
        // "A value of zero disables rendering of the element"
        //
        // <rect height="0">
        if (
          rectHeight0 &&
          rectWidth0 &&
          node.name === 'rect' &&
          node.children.length === 0 &&
          node.attributes.height === '0'
        ) {
          detachNodeFromParent(node, parentNode);
          return;
        }

        // Pattern with zero width
        //
        // https://www.w3.org/TR/SVG11/pservers.html#PatternElementWidthAttribute
        // "A value of zero disables rendering of the element (i.e., no paint is applied)"
        //
        // <pattern width="0">
        if (
          patternWidth0 &&
          node.name === 'pattern' &&
          node.attributes.width === '0'
        ) {
          detachNodeFromParent(node, parentNode);
          return;
        }

        // Pattern with zero height
        //
        // https://www.w3.org/TR/SVG11/pservers.html#PatternElementHeightAttribute
        // "A value of zero disables rendering of the element (i.e., no paint is applied)"
        //
        // <pattern height="0">
        if (
          patternHeight0 &&
          node.name === 'pattern' &&
          node.attributes.height === '0'
        ) {
          detachNodeFromParent(node, parentNode);
          return;
        }

        // Image with zero width
        //
        // https://www.w3.org/TR/SVG11/struct.html#ImageElementWidthAttribute
        // "A value of zero disables rendering of the element"
        //
        // <image width="0">
        if (
          imageWidth0 &&
          node.name === 'image' &&
          node.attributes.width === '0'
        ) {
          detachNodeFromParent(node, parentNode);
          return;
        }

        // Image with zero height
        //
        // https://www.w3.org/TR/SVG11/struct.html#ImageElementHeightAttribute
        // "A value of zero disables rendering of the element"
        //
        // <image height="0">
        if (
          imageHeight0 &&
          node.name === 'image' &&
          node.attributes.height === '0'
        ) {
          detachNodeFromParent(node, parentNode);
          return;
        }

        // Path with empty data
        //
        // https://www.w3.org/TR/SVG11/paths.html#DAttribute
        //
        // <path d=""/>
        if (pathEmptyD && node.name === 'path') {
          if (node.attributes.d == null) {
            detachNodeFromParent(node, parentNode);
            return;
          }
          const pathData = parsePathData(node.attributes.d);
          if (pathData.length === 0) {
            detachNodeFromParent(node, parentNode);
            return;
          }
          // keep single point paths for markers
          if (
            pathData.length === 1 &&
            computedStyle['marker-start'] == null &&
            computedStyle['marker-end'] == null
          ) {
            detachNodeFromParent(node, parentNode);
            return;
          }
          return;
        }

        // Polyline with empty points
        //
        // https://www.w3.org/TR/SVG11/shapes.html#PolylineElementPointsAttribute
        //
        // <polyline points="">
        if (
          polylineEmptyPoints &&
          node.name === 'polyline' &&
          node.attributes.points == null
        ) {
          detachNodeFromParent(node, parentNode);
          return;
        }

        // Polygon with empty points
        //
        // https://www.w3.org/TR/SVG11/shapes.html#PolygonElementPointsAttribute
        //
        // <polygon points="">
        if (
          polygonEmptyPoints &&
          node.name === 'polygon' &&
          node.attributes.points == null
        ) {
          detachNodeFromParent(node, parentNode);
          return;
        }
      },
    },
  };
};


code:
'use strict';

const { detachNodeFromParent } = require('../lib/xast.js');

exports.name = 'removeMetadata';
exports.type = 'visitor';
exports.active = true;
exports.description = 'removes <metadata>';

/**
 * Remove <metadata>.
 *
 * https://www.w3.org/TR/SVG11/metadata.html
 *
 * @author Kir Belevich
 *
 * @type {import('../lib/types').Plugin<void>}
 */
exports.fn = () => {
  return {
    element: {
      enter: (node, parentNode) => {
        if (node.name === 'metadata') {
          detachNodeFromParent(node, parentNode);
        }
      },
    },
  };
};


code:
'use strict';

exports.name = 'removeNonInheritableGroupAttrs';

exports.type = 'perItem';

exports.active = true;

exports.description =
  'removes non-inheritable group’s presentational attributes';

const {
  inheritableAttrs,
  attrsGroups,
  presentationNonInheritableGroupAttrs,
} = require('./_collections');

/**
 * Remove non-inheritable group's "presentation" attributes.
 *
 * @param {Object} item current iteration item
 * @return {Boolean} if false, item will be filtered out
 *
 * @author Kir Belevich
 */
exports.fn = function (item) {
  if (item.type === 'element' && item.name === 'g') {
    for (const name of Object.keys(item.attributes)) {
      if (
        attrsGroups.presentation.includes(name) === true &&
        inheritableAttrs.includes(name) === false &&
        presentationNonInheritableGroupAttrs.includes(name) === false
      ) {
        delete item.attributes[name];
      }
    }
  }
};


code:
'use strict';

/**
 * @typedef {import('../lib/types').PathDataItem} PathDataItem
 */

const { visitSkip, detachNodeFromParent } = require('../lib/xast.js');
const { parsePathData } = require('../lib/path.js');
const { intersects } = require('./_path.js');

exports.type = 'visitor';
exports.name = 'removeOffCanvasPaths';
exports.active = false;
exports.description =
  'removes elements that are drawn outside of the viewbox (disabled by default)';

/**
 * Remove elements that are drawn outside of the viewbox.
 *
 * @author JoshyPHP
 *
 * @type {import('../lib/types').Plugin<void>}
 */
exports.fn = () => {
  /**
   * @type {null | {
   *   top: number,
   *   right: number,
   *   bottom: number,
   *   left: number,
   *   width: number,
   *   height: number
   * }}
   */
  let viewBoxData = null;

  return {
    element: {
      enter: (node, parentNode) => {
        if (node.name === 'svg' && parentNode.type === 'root') {
          let viewBox = '';
          // find viewbox
          if (node.attributes.viewBox != null) {
            // remove commas and plus signs, normalize and trim whitespace
            viewBox = node.attributes.viewBox;
          } else if (
            node.attributes.height != null &&
            node.attributes.width != null
          ) {
            viewBox = `0 0 ${node.attributes.width} ${node.attributes.height}`;
          }

          // parse viewbox
          // remove commas and plus signs, normalize and trim whitespace
          viewBox = viewBox
            .replace(/[,+]|px/g, ' ')
            .replace(/\s+/g, ' ')
            .replace(/^\s*|\s*$/g, '');
          // ensure that the dimensions are 4 values separated by space
          const m =
            /^(-?\d*\.?\d+) (-?\d*\.?\d+) (\d*\.?\d+) (\d*\.?\d+)$/.exec(
              viewBox
            );
          if (m == null) {
            return;
          }
          const left = Number.parseFloat(m[1]);
          const top = Number.parseFloat(m[2]);
          const width = Number.parseFloat(m[3]);
          const height = Number.parseFloat(m[4]);

          // store the viewBox boundaries
          viewBoxData = {
            left,
            top,
            right: left + width,
            bottom: top + height,
            width,
            height,
          };
        }

        // consider that any item with a transform attribute is visible
        if (node.attributes.transform != null) {
          return visitSkip;
        }

        if (
          node.name === 'path' &&
          node.attributes.d != null &&
          viewBoxData != null
        ) {
          const pathData = parsePathData(node.attributes.d);

          // consider that a M command within the viewBox is visible
          let visible = false;
          for (const pathDataItem of pathData) {
            if (pathDataItem.command === 'M') {
              const [x, y] = pathDataItem.args;
              if (
                x >= viewBoxData.left &&
                x <= viewBoxData.right &&
                y >= viewBoxData.top &&
                y <= viewBoxData.bottom
              ) {
                visible = true;
              }
            }
          }
          if (visible) {
            return;
          }

          if (pathData.length === 2) {
            // close the path too short for intersects()
            pathData.push({ command: 'z', args: [] });
          }

          const { left, top, width, height } = viewBoxData;
          /**
           * @type {Array<PathDataItem>}
           */
          const viewBoxPathData = [
            { command: 'M', args: [left, top] },
            { command: 'h', args: [width] },
            { command: 'v', args: [height] },
            { command: 'H', args: [left] },
            { command: 'z', args: [] },
          ];

          if (intersects(viewBoxPathData, pathData) === false) {
            detachNodeFromParent(node, parentNode);
          }
        }
      },
    },
  };
};


code:
'use strict';

const { detachNodeFromParent } = require('../lib/xast.js');

exports.name = 'removeRasterImages';
exports.type = 'visitor';
exports.active = false;
exports.description = 'removes raster images (disabled by default)';

/**
 * Remove raster images references in <image>.
 *
 * @see https://bugs.webkit.org/show_bug.cgi?id=63548
 *
 * @author Kir Belevich
 *
 * @type {import('../lib/types').Plugin<void>}
 */
exports.fn = () => {
  return {
    element: {
      enter: (node, parentNode) => {
        if (
          node.name === 'image' &&
          node.attributes['xlink:href'] != null &&
          /(\.|image\/)(jpg|png|gif)/.test(node.attributes['xlink:href'])
        ) {
          detachNodeFromParent(node, parentNode);
        }
      },
    },
  };
};


code:
'use strict';

const { detachNodeFromParent } = require('../lib/xast.js');

exports.name = 'removeScriptElement';
exports.type = 'visitor';
exports.active = false;
exports.description = 'removes <script> elements (disabled by default)';

/**
 * Remove <script>.
 *
 * https://www.w3.org/TR/SVG11/script.html
 *
 * @author Patrick Klingemann
 *
 * @type {import('../lib/types').Plugin<void>}
 */
exports.fn = () => {
  return {
    element: {
      enter: (node, parentNode) => {
        if (node.name === 'script') {
          detachNodeFromParent(node, parentNode);
        }
      },
    },
  };
};


code:
'use strict';

const { detachNodeFromParent } = require('../lib/xast.js');

exports.name = 'removeStyleElement';
exports.type = 'visitor';
exports.active = false;
exports.description = 'removes <style> element (disabled by default)';

/**
 * Remove <style>.
 *
 * https://www.w3.org/TR/SVG11/styling.html#StyleElement
 *
 * @author Betsy Dupuis
 *
 * @type {import('../lib/types').Plugin<void>}
 */
exports.fn = () => {
  return {
    element: {
      enter: (node, parentNode) => {
        if (node.name === 'style') {
          detachNodeFromParent(node, parentNode);
        }
      },
    },
  };
};


code:
'use strict';

const { detachNodeFromParent } = require('../lib/xast.js');

exports.name = 'removeTitle';
exports.type = 'visitor';
exports.active = true;
exports.description = 'removes <title>';

/**
 * Remove <title>.
 *
 * https://developer.mozilla.org/en-US/docs/Web/SVG/Element/title
 *
 * @author Igor Kalashnikov
 *
 * @type {import('../lib/types').Plugin<void>}
 */
exports.fn = () => {
  return {
    element: {
      enter: (node, parentNode) => {
        if (node.name === 'title') {
          detachNodeFromParent(node, parentNode);
        }
      },
    },
  };
};


code:
'use strict';

const { visitSkip, detachNodeFromParent } = require('../lib/xast.js');
const { collectStylesheet, computeStyle } = require('../lib/style.js');
const {
  elems,
  attrsGroups,
  elemsGroups,
  attrsGroupsDefaults,
  presentationNonInheritableGroupAttrs,
} = require('./_collections');

exports.type = 'visitor';
exports.name = 'removeUnknownsAndDefaults';
exports.active = true;
exports.description =
  'removes unknown elements content and attributes, removes attrs with default values';

// resolve all groups references

/**
 * @type {Map<string, Set<string>>}
 */
const allowedChildrenPerElement = new Map();
/**
 * @type {Map<string, Set<string>>}
 */
const allowedAttributesPerElement = new Map();
/**
 * @type {Map<string, Map<string, string>>}
 */
const attributesDefaultsPerElement = new Map();

for (const [name, config] of Object.entries(elems)) {
  /**
   * @type {Set<string>}
   */
  const allowedChildren = new Set();
  if (config.content) {
    for (const elementName of config.content) {
      allowedChildren.add(elementName);
    }
  }
  if (config.contentGroups) {
    for (const contentGroupName of config.contentGroups) {
      const elemsGroup = elemsGroups[contentGroupName];
      if (elemsGroup) {
        for (const elementName of elemsGroup) {
          allowedChildren.add(elementName);
        }
      }
    }
  }
  /**
   * @type {Set<string>}
   */
  const allowedAttributes = new Set();
  if (config.attrs) {
    for (const attrName of config.attrs) {
      allowedAttributes.add(attrName);
    }
  }
  /**
   * @type {Map<string, string>}
   */
  const attributesDefaults = new Map();
  if (config.defaults) {
    for (const [attrName, defaultValue] of Object.entries(config.defaults)) {
      attributesDefaults.set(attrName, defaultValue);
    }
  }
  for (const attrsGroupName of config.attrsGroups) {
    const attrsGroup = attrsGroups[attrsGroupName];
    if (attrsGroup) {
      for (const attrName of attrsGroup) {
        allowedAttributes.add(attrName);
      }
    }
    const groupDefaults = attrsGroupsDefaults[attrsGroupName];
    if (groupDefaults) {
      for (const [attrName, defaultValue] of Object.entries(groupDefaults)) {
        attributesDefaults.set(attrName, defaultValue);
      }
    }
  }
  allowedChildrenPerElement.set(name, allowedChildren);
  allowedAttributesPerElement.set(name, allowedAttributes);
  attributesDefaultsPerElement.set(name, attributesDefaults);
}

/**
 * Remove unknown elements content and attributes,
 * remove attributes with default values.
 *
 * @author Kir Belevich
 *
 * @type {import('../lib/types').Plugin<{
 *   unknownContent?: boolean,
 *   unknownAttrs?: boolean,
 *   defaultAttrs?: boolean,
 *   uselessOverrides?: boolean,
 *   keepDataAttrs?: boolean,
 *   keepAriaAttrs?: boolean,
 *   keepRoleAttr?: boolean,
 * }>}
 */
exports.fn = (root, params) => {
  const {
    unknownContent = true,
    unknownAttrs = true,
    defaultAttrs = true,
    uselessOverrides = true,
    keepDataAttrs = true,
    keepAriaAttrs = true,
    keepRoleAttr = false,
  } = params;
  const stylesheet = collectStylesheet(root);

  return {
    element: {
      enter: (node, parentNode) => {
        // skip namespaced elements
        if (node.name.includes(':')) {
          return;
        }
        // skip visiting foreignObject subtree
        if (node.name === 'foreignObject') {
          return visitSkip;
        }

        // remove unknown element's content
        if (unknownContent && parentNode.type === 'element') {
          const allowedChildren = allowedChildrenPerElement.get(
            parentNode.name
          );
          if (allowedChildren == null || allowedChildren.size === 0) {
            // remove unknown elements
            if (allowedChildrenPerElement.get(node.name) == null) {
              detachNodeFromParent(node, parentNode);
              return;
            }
          } else {
            // remove not allowed children
            if (allowedChildren.has(node.name) === false) {
              detachNodeFromParent(node, parentNode);
              return;
            }
          }
        }

        const allowedAttributes = allowedAttributesPerElement.get(node.name);
        const attributesDefaults = attributesDefaultsPerElement.get(node.name);
        const computedParentStyle =
          parentNode.type === 'element'
            ? computeStyle(stylesheet, parentNode)
            : null;

        // remove element's unknown attrs and attrs with default values
        for (const [name, value] of Object.entries(node.attributes)) {
          if (keepDataAttrs && name.startsWith('data-')) {
            continue;
          }
          if (keepAriaAttrs && name.startsWith('aria-')) {
            continue;
          }
          if (keepRoleAttr && name === 'role') {
            continue;
          }
          // skip xmlns attribute
          if (name === 'xmlns') {
            continue;
          }
          // skip namespaced attributes except xml:* and xlink:*
          if (name.includes(':')) {
            const [prefix] = name.split(':');
            if (prefix !== 'xml' && prefix !== 'xlink') {
              continue;
            }
          }

          if (
            unknownAttrs &&
            allowedAttributes &&
            allowedAttributes.has(name) === false
          ) {
            delete node.attributes[name];
          }
          if (
            defaultAttrs &&
            node.attributes.id == null &&
            attributesDefaults &&
            attributesDefaults.get(name) === value
          ) {
            // keep defaults if parent has own or inherited style
            if (
              computedParentStyle == null ||
              computedParentStyle[name] == null
            ) {
              delete node.attributes[name];
            }
          }
          if (uselessOverrides && node.attributes.id == null) {
            const style =
              computedParentStyle == null ? null : computedParentStyle[name];
            if (
              presentationNonInheritableGroupAttrs.includes(name) === false &&
              style != null &&
              style.type === 'static' &&
              style.value === value
            ) {
              delete node.attributes[name];
            }
          }
        }
      },
    },
  };
};


code:
'use strict';

exports.type = 'visitor';
exports.name = 'removeUnusedNS';
exports.active = true;
exports.description = 'removes unused namespaces declaration';

/**
 * Remove unused namespaces declaration from svg element
 * which are not used in elements or attributes
 *
 * @author Kir Belevich
 *
 * @type {import('../lib/types').Plugin<void>}
 */
exports.fn = () => {
  /**
   * @type {Set<string>}
   */
  const unusedNamespaces = new Set();
  return {
    element: {
      enter: (node, parentNode) => {
        // collect all namespaces from svg element
        // (such as xmlns:xlink="http://www.w3.org/1999/xlink")
        if (node.name === 'svg' && parentNode.type === 'root') {
          for (const name of Object.keys(node.attributes)) {
            if (name.startsWith('xmlns:')) {
              const local = name.slice('xmlns:'.length);
              unusedNamespaces.add(local);
            }
          }
        }
        if (unusedNamespaces.size !== 0) {
          // preserve namespace used in nested elements names
          if (node.name.includes(':')) {
            const [ns] = node.name.split(':');
            if (unusedNamespaces.has(ns)) {
              unusedNamespaces.delete(ns);
            }
          }
          // preserve namespace used in nested elements attributes
          for (const name of Object.keys(node.attributes)) {
            if (name.includes(':')) {
              const [ns] = name.split(':');
              unusedNamespaces.delete(ns);
            }
          }
        }
      },
      exit: (node, parentNode) => {
        // remove unused namespace attributes from svg element
        if (node.name === 'svg' && parentNode.type === 'root') {
          for (const name of unusedNamespaces) {
            delete node.attributes[`xmlns:${name}`];
          }
        }
      },
    },
  };
};


code:
'use strict';

/**
 * @typedef {import('../lib/types').XastElement} XastElement
 */

const { detachNodeFromParent } = require('../lib/xast.js');
const { elemsGroups } = require('./_collections.js');

exports.type = 'visitor';
exports.name = 'removeUselessDefs';
exports.active = true;
exports.description = 'removes elements in <defs> without id';

/**
 * Removes content of defs and properties that aren't rendered directly without ids.
 *
 * @author Lev Solntsev
 *
 * @type {import('../lib/types').Plugin<void>}
 */
exports.fn = () => {
  return {
    element: {
      enter: (node, parentNode) => {
        if (node.name === 'defs') {
          /**
           * @type {Array<XastElement>}
           */
          const usefulNodes = [];
          collectUsefulNodes(node, usefulNodes);
          if (usefulNodes.length === 0) {
            detachNodeFromParent(node, parentNode);
          }
          // TODO remove in SVGO 3
          for (const usefulNode of usefulNodes) {
            // @ts-ignore parentNode is legacy
            usefulNode.parentNode = node;
          }
          node.children = usefulNodes;
        } else if (
          elemsGroups.nonRendering.includes(node.name) &&
          node.attributes.id == null
        ) {
          detachNodeFromParent(node, parentNode);
        }
      },
    },
  };
};

/**
 * @type {(node: XastElement, usefulNodes: Array<XastElement>) => void}
 */
const collectUsefulNodes = (node, usefulNodes) => {
  for (const child of node.children) {
    if (child.type === 'element') {
      if (child.attributes.id != null || child.name === 'style') {
        usefulNodes.push(child);
      } else {
        collectUsefulNodes(child, usefulNodes);
      }
    }
  }
};


code:
'use strict';

const { visit, visitSkip, detachNodeFromParent } = require('../lib/xast.js');
const { collectStylesheet, computeStyle } = require('../lib/style.js');
const { elemsGroups } = require('./_collections.js');

exports.type = 'visitor';
exports.name = 'removeUselessStrokeAndFill';
exports.active = true;
exports.description = 'removes useless stroke and fill attributes';

/**
 * Remove useless stroke and fill attrs.
 *
 * @author Kir Belevich
 *
 * @type {import('../lib/types').Plugin<{
 *  stroke?: boolean,
 *  fill?: boolean,
 *  removeNone?: boolean
 * }>}
 */
exports.fn = (root, params) => {
  const {
    stroke: removeStroke = true,
    fill: removeFill = true,
    removeNone = false,
  } = params;

  // style and script elements deoptimise this plugin
  let hasStyleOrScript = false;
  visit(root, {
    element: {
      enter: (node) => {
        if (node.name === 'style' || node.name === 'script') {
          hasStyleOrScript = true;
        }
      },
    },
  });
  if (hasStyleOrScript) {
    return null;
  }

  const stylesheet = collectStylesheet(root);

  return {
    element: {
      enter: (node, parentNode) => {
        // id attribute deoptimise the whole subtree
        if (node.attributes.id != null) {
          return visitSkip;
        }
        if (elemsGroups.shape.includes(node.name) == false) {
          return;
        }
        const computedStyle = computeStyle(stylesheet, node);
        const stroke = computedStyle.stroke;
        const strokeOpacity = computedStyle['stroke-opacity'];
        const strokeWidth = computedStyle['stroke-width'];
        const markerEnd = computedStyle['marker-end'];
        const fill = computedStyle.fill;
        const fillOpacity = computedStyle['fill-opacity'];
        const computedParentStyle =
          parentNode.type === 'element'
            ? computeStyle(stylesheet, parentNode)
            : null;
        const parentStroke =
          computedParentStyle == null ? null : computedParentStyle.stroke;

        // remove stroke*
        if (removeStroke) {
          if (
            stroke == null ||
            (stroke.type === 'static' && stroke.value == 'none') ||
            (strokeOpacity != null &&
              strokeOpacity.type === 'static' &&
              strokeOpacity.value === '0') ||
            (strokeWidth != null &&
              strokeWidth.type === 'static' &&
              strokeWidth.value === '0')
          ) {
            // stroke-width may affect the size of marker-end
            // marker is not visible when stroke-width is 0
            if (
              (strokeWidth != null &&
                strokeWidth.type === 'static' &&
                strokeWidth.value === '0') ||
              markerEnd == null
            ) {
              for (const name of Object.keys(node.attributes)) {
                if (name.startsWith('stroke')) {
                  delete node.attributes[name];
                }
              }
              // set explicit none to not inherit from parent
              if (
                parentStroke != null &&
                parentStroke.type === 'static' &&
                parentStroke.value !== 'none'
              ) {
                node.attributes.stroke = 'none';
              }
            }
          }
        }

        // remove fill*
        if (removeFill) {
          if (
            (fill != null && fill.type === 'static' && fill.value === 'none') ||
            (fillOpacity != null &&
              fillOpacity.type === 'static' &&
              fillOpacity.value === '0')
          ) {
            for (const name of Object.keys(node.attributes)) {
              if (name.startsWith('fill-')) {
                delete node.attributes[name];
              }
            }
            if (
              fill == null ||
              (fill.type === 'static' && fill.value !== 'none')
            ) {
              node.attributes.fill = 'none';
            }
          }
        }

        if (removeNone) {
          if (
            (stroke == null || node.attributes.stroke === 'none') &&
            ((fill != null &&
              fill.type === 'static' &&
              fill.value === 'none') ||
              node.attributes.fill === 'none')
          ) {
            detachNodeFromParent(node, parentNode);
          }
        }
      },
    },
  };
};


code:
'use strict';

exports.type = 'visitor';
exports.name = 'removeViewBox';
exports.active = true;
exports.description = 'removes viewBox attribute when possible';

const viewBoxElems = ['svg', 'pattern', 'symbol'];

/**
 * Remove viewBox attr which coincides with a width/height box.
 *
 * @see https://www.w3.org/TR/SVG11/coords.html#ViewBoxAttribute
 *
 * @example
 * <svg width="100" height="50" viewBox="0 0 100 50">
 *             ⬇
 * <svg width="100" height="50">
 *
 * @author Kir Belevich
 *
 * @type {import('../lib/types').Plugin<void>}
 */
exports.fn = () => {
  return {
    element: {
      enter: (node, parentNode) => {
        if (
          viewBoxElems.includes(node.name) &&
          node.attributes.viewBox != null &&
          node.attributes.width != null &&
          node.attributes.height != null
        ) {
          // TODO remove width/height for such case instead
          if (node.name === 'svg' && parentNode.type !== 'root') {
            return;
          }
          const nums = node.attributes.viewBox.split(/[ ,]+/g);
          if (
            nums[0] === '0' &&
            nums[1] === '0' &&
            node.attributes.width.replace(/px$/, '') === nums[2] && // could use parseFloat too
            node.attributes.height.replace(/px$/, '') === nums[3]
          ) {
            delete node.attributes.viewBox;
          }
        }
      },
    },
  };
};


code:
'use strict';

exports.name = 'removeXMLNS';

exports.type = 'perItem';

exports.active = false;

exports.description =
  'removes xmlns attribute (for inline svg, disabled by default)';

/**
 * Remove the xmlns attribute when present.
 *
 * @example
 * <svg viewBox="0 0 100 50" xmlns="http://www.w3.org/2000/svg">
 *   ↓
 * <svg viewBox="0 0 100 50">
 *
 * @param {Object} item current iteration item
 * @return {Boolean} if true, xmlns will be filtered out
 *
 * @author Ricardo Tomasi
 */
exports.fn = function (item) {
  if (item.type === 'element' && item.name === 'svg') {
    delete item.attributes.xmlns;
    delete item.attributes['xmlns:xlink'];
  }
};


code:
'use strict';

const { detachNodeFromParent } = require('../lib/xast.js');

exports.name = 'removeXMLProcInst';
exports.type = 'visitor';
exports.active = true;
exports.description = 'removes XML processing instructions';

/**
 * Remove XML Processing Instruction.
 *
 * @example
 * <?xml version="1.0" encoding="utf-8"?>
 *
 * @author Kir Belevich
 *
 * @type {import('../lib/types').Plugin<void>}
 */
exports.fn = () => {
  return {
    instruction: {
      enter: (node, parentNode) => {
        if (node.name === 'xml') {
          detachNodeFromParent(node, parentNode);
        }
      },
    },
  };
};


code:
'use strict';

/**
 * @typedef {import('../lib/types').XastElement} XastElement
 * @typedef {import('../lib/types').XastParent} XastParent
 * @typedef {import('../lib/types').XastNode} XastNode
 */

const JSAPI = require('../lib/svgo/jsAPI.js');

exports.type = 'visitor';
exports.name = 'reusePaths';
exports.active = false;
exports.description =
  'Finds <path> elements with the same d, fill, and ' +
  'stroke, and converts them to <use> elements ' +
  'referencing a single <path> def.';

/**
 * Finds <path> elements with the same d, fill, and stroke, and converts them to
 * <use> elements referencing a single <path> def.
 *
 * @author Jacob Howcroft
 *
 * @type {import('../lib/types').Plugin<void>}
 */
exports.fn = () => {
  /**
   * @type {Map<string, Array<XastElement>>}
   */
  const paths = new Map();

  return {
    element: {
      enter: (node) => {
        if (node.name === 'path' && node.attributes.d != null) {
          const d = node.attributes.d;
          const fill = node.attributes.fill || '';
          const stroke = node.attributes.stroke || '';
          const key = d + ';s:' + stroke + ';f:' + fill;
          let list = paths.get(key);
          if (list == null) {
            list = [];
            paths.set(key, list);
          }
          list.push(node);
        }
      },

      exit: (node, parentNode) => {
        if (node.name === 'svg' && parentNode.type === 'root') {
          /**
           * @type {XastElement}
           */
          const rawDefs = {
            type: 'element',
            name: 'defs',
            attributes: {},
            children: [],
          };
          /**
           * @type {XastElement}
           */
          const defsTag = new JSAPI(rawDefs, node);
          let index = 0;
          for (const list of paths.values()) {
            if (list.length > 1) {
              // add reusable path to defs
              /**
               * @type {XastElement}
               */
              const rawPath = {
                type: 'element',
                name: 'path',
                attributes: { ...list[0].attributes },
                children: [],
              };
              delete rawPath.attributes.transform;
              let id;
              if (rawPath.attributes.id == null) {
                id = 'reuse-' + index;
                index += 1;
                rawPath.attributes.id = id;
              } else {
                id = rawPath.attributes.id;
                delete list[0].attributes.id;
              }
              /**
               * @type {XastElement}
               */
              const reusablePath = new JSAPI(rawPath, defsTag);
              defsTag.children.push(reusablePath);
              // convert paths to <use>
              for (const pathNode of list) {
                pathNode.name = 'use';
                pathNode.attributes['xlink:href'] = '#' + id;
                delete pathNode.attributes.d;
                delete pathNode.attributes.stroke;
                delete pathNode.attributes.fill;
              }
            }
          }
          if (defsTag.children.length !== 0) {
            if (node.attributes['xmlns:xlink'] == null) {
              node.attributes['xmlns:xlink'] = 'http://www.w3.org/1999/xlink';
            }
            node.children.unshift(defsTag);
          }
        }
      },
    },
  };
};


code:
'use strict';

exports.type = 'visitor';
exports.name = 'sortAttrs';
exports.active = false;
exports.description = 'Sort element attributes for better compression';

/**
 * Sort element attributes for better compression
 *
 * @author Nikolay Frantsev
 *
 * @type {import('../lib/types').Plugin<{
 *   order?: Array<string>
 *   xmlnsOrder?: 'front' | 'alphabetical'
 * }>}
 */
exports.fn = (_root, params) => {
  const {
    order = [
      'id',
      'width',
      'height',
      'x',
      'x1',
      'x2',
      'y',
      'y1',
      'y2',
      'cx',
      'cy',
      'r',
      'fill',
      'stroke',
      'marker',
      'd',
      'points',
    ],
    xmlnsOrder = 'front',
  } = params;

  /**
   * @type {(name: string) => number}
   */
  const getNsPriority = (name) => {
    if (xmlnsOrder === 'front') {
      // put xmlns first
      if (name === 'xmlns') {
        return 3;
      }
      // xmlns:* attributes second
      if (name.startsWith('xmlns:')) {
        return 2;
      }
    }
    // other namespaces after and sort them alphabetically
    if (name.includes(':')) {
      return 1;
    }
    // other attributes
    return 0;
  };

  /**
   * @type {(a: [string, string], b: [string, string]) => number}
   */
  const compareAttrs = ([aName], [bName]) => {
    // sort namespaces
    const aPriority = getNsPriority(aName);
    const bPriority = getNsPriority(bName);
    const priorityNs = bPriority - aPriority;
    if (priorityNs !== 0) {
      return priorityNs;
    }
    // extract the first part from attributes
    // for example "fill" from "fill" and "fill-opacity"
    const [aPart] = aName.split('-');
    const [bPart] = bName.split('-');
    // rely on alphabetical sort when the first part is the same
    if (aPart !== bPart) {
      const aInOrderFlag = order.includes(aPart) ? 1 : 0;
      const bInOrderFlag = order.includes(bPart) ? 1 : 0;
      // sort by position in order param
      if (aInOrderFlag === 1 && bInOrderFlag === 1) {
        return order.indexOf(aPart) - order.indexOf(bPart);
      }
      // put attributes from order param before others
      const priorityOrder = bInOrderFlag - aInOrderFlag;
      if (priorityOrder !== 0) {
        return priorityOrder;
      }
    }
    // sort alphabetically
    return aName < bName ? -1 : 1;
  };

  return {
    element: {
      enter: (node) => {
        const attrs = Object.entries(node.attributes);
        attrs.sort(compareAttrs);
        /**
         * @type {Record<string, string>}
         */
        const sortedAttributes = {};
        for (const [name, value] of attrs) {
          sortedAttributes[name] = value;
        }
        node.attributes = sortedAttributes;
      },
    },
  };
};


code:
'use strict';

exports.type = 'visitor';
exports.name = 'sortDefsChildren';
exports.active = true;
exports.description = 'Sorts children of <defs> to improve compression';

/**
 * Sorts children of defs in order to improve compression.
 * Sorted first by frequency then by element name length then by element name (to ensure grouping).
 *
 * @author David Leston
 *
 * @type {import('../lib/types').Plugin<void>}
 */
exports.fn = () => {
  return {
    element: {
      enter: (node) => {
        if (node.name === 'defs') {
          /**
           * @type {Map<string, number>}
           */
          const frequencies = new Map();
          for (const child of node.children) {
            if (child.type === 'element') {
              const frequency = frequencies.get(child.name);
              if (frequency == null) {
                frequencies.set(child.name, 1);
              } else {
                frequencies.set(child.name, frequency + 1);
              }
            }
          }
          node.children.sort((a, b) => {
            if (a.type !== 'element' || b.type !== 'element') {
              return 0;
            }
            const aFrequency = frequencies.get(a.name);
            const bFrequency = frequencies.get(b.name);
            if (aFrequency != null && bFrequency != null) {
              const frequencyComparison = bFrequency - aFrequency;
              if (frequencyComparison !== 0) {
                return frequencyComparison;
              }
            }
            const lengthComparison = b.name.length - a.name.length;
            if (lengthComparison !== 0) {
              return lengthComparison;
            }
            if (a.name !== b.name) {
              return a.name > b.name ? -1 : 1;
            }
            return 0;
          });
        }
      },
    },
  };
};


code:
# synckit

[![GitHub Actions](https://github.com/un-ts/synckit/workflows/CI/badge.svg)](https://github.com/un-ts/synckit/actions/workflows/ci.yml)
[![Codecov](https://img.shields.io/codecov/c/github/un-ts/synckit.svg)](https://codecov.io/gh/un-ts/synckit)
[![type-coverage](https://img.shields.io/badge/dynamic/json.svg?label=type-coverage&prefix=%E2%89%A5&suffix=%&query=$.typeCoverage.atLeast&uri=https%3A%2F%2Fraw.githubusercontent.com%2Fun-ts%2Fsynckit%2Fmain%2Fpackage.json)](https://github.com/plantain-00/type-coverage)
[![npm](https://img.shields.io/npm/v/synckit.svg)](https://www.npmjs.com/package/synckit)
[![GitHub Release](https://img.shields.io/github/release/un-ts/synckit)](https://github.com/un-ts/synckit/releases)

[![Conventional Commits](https://img.shields.io/badge/conventional%20commits-1.0.0-yellow.svg)](https://conventionalcommits.org)
[![Renovate enabled](https://img.shields.io/badge/renovate-enabled-brightgreen.svg)](https://renovatebot.com)
[![JavaScript Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://standardjs.com)
[![Code Style: Prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier)

Perform async work synchronously in Node.js using `worker_threads` with first-class TypeScript support.

## TOC <!-- omit in toc -->

- [Usage](#usage)
  - [Install](#install)
  - [API](#api)
  - [Types](#types)
  - [Options](#options)
  - [Envs](#envs)
  - [TypeScript](#typescript)
    - [`ts-node`](#ts-node)
    - [`esbuild-register`](#esbuild-register)
    - [`esbuild-runner`](#esbuild-runner)
    - [`swc`](#swc)
    - [`tsx`](#tsx)
- [Benchmark](#benchmark)
- [Sponsors](#sponsors)
- [Backers](#backers)
- [Changelog](#changelog)
- [License](#license)

## Usage

### Install

```sh
# yarn
yarn add synckit

# npm
npm i synckit
```

### API

```js
// runner.js
import { createSyncFn } from 'synckit'

// the worker path must be absolute
const syncFn = createSyncFn(require.resolve('./worker'), {
  tsRunner: 'tsx', // optional, can be `'ts-node' | 'esbuild-register' | 'esbuild-runner' | 'tsx'`
})

// do whatever you want, you will get the result synchronously!
const result = syncFn(...args)
```

```js
// worker.js
import { runAsWorker } from 'synckit'

runAsWorker(async (...args) => {
  // do expensive work
  return result
})
```

You must make sure, the `result` is serializable by [`Structured Clone Algorithm`](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm)

### Types

````ts
export interface GlobalShim {
  moduleName: string
  /**
   * `undefined` means side effect only
   */
  globalName?: string
  /**
   * 1. `undefined` or empty string means `default`, for example:
   * ```js
   * import globalName from 'module-name'
   * ```
   *
   * 2. `null` means namespaced, for example:
   * ```js
   * import * as globalName from 'module-name'
   * ```
   *
   */
  named?: string | null
  /**
   * If not `false`, the shim will only be applied when the original `globalName` unavailable,
   * for example you may only want polyfill `globalThis.fetch` when it's unavailable natively:
   * ```js
   * import fetch from 'node-fetch'
   *
   * if (!globalThis.fetch) {
   *   globalThis.fetch = fetch
   * }
   * ```
   */
  conditional?: boolean
}
````

### Options

1. `bufferSize` same as env `SYNCKIT_BUFFER_SIZE`
2. `timeout` same as env `SYNCKIT_TIMEOUT`
3. `execArgv` same as env `SYNCKIT_EXEC_ARGV`
4. `tsRunner` same as env `SYNCKIT_TS_RUNNER`
5. `transferList`: Please refer Node.js [`worker_threads`](https://nodejs.org/api/worker_threads.html#:~:text=Default%3A%20true.-,transferList,-%3CObject%5B%5D%3E%20If) documentation
6. `globalShims`: Similar like env `SYNCKIT_GLOBAL_SHIMS` but much more flexible which can be a `GlobalShim` `Array`, see `GlobalShim`'s [definition](#types) for more details

### Envs

1. `SYNCKIT_BUFFER_SIZE`: `bufferSize` to create `SharedArrayBuffer` for `worker_threads` (default as `1024`)
2. `SYNCKIT_TIMEOUT`: `timeout` for performing the async job (no default)
3. `SYNCKIT_EXEC_ARGV`: List of node CLI options passed to the worker, split with comma `,`. (default as `[]`), see also [`node` docs](https://nodejs.org/api/worker_threads.html)
4. `SYNCKIT_TS_RUNNER`: Which TypeScript runner to be used, it could be very useful for development, could be `'ts-node' | 'esbuild-register' | 'esbuild-runner' | 'swc' | 'tsx'`, `'ts-node'` is used by default, make sure you have installed them already
5. `SYNCKIT_GLOBAL_SHIMS`: Whether to enable the default `DEFAULT_GLOBAL_SHIMS_PRESET` as `globalShims`

### TypeScript

#### `ts-node`

If you want to use `ts-node` for worker file (a `.ts` file), it is supported out of box!

If you want to use a custom tsconfig as project instead of default `tsconfig.json`, use `TS_NODE_PROJECT` env. Please view [ts-node](https://github.com/TypeStrong/ts-node#tsconfig) for more details.

If you want to integrate with [tsconfig-paths](https://www.npmjs.com/package/tsconfig-paths), please view [ts-node](https://github.com/TypeStrong/ts-node#paths-and-baseurl) for more details.

#### `esbuild-register`

Please view [`esbuild-register`][] for its document

#### `esbuild-runner`

Please view [`esbuild-runner`][] for its document

#### `swc`

Please view [`@swc-node/register`][] for its document

#### `tsx`

Please view [`tsx`][] for its document

## Benchmark

It is about 20x faster than [`sync-threads`](https://github.com/lambci/sync-threads) but 3x slower than native for reading the file content itself 1000 times during runtime, and 18x faster than `sync-threads` but 4x slower than native for total time.

And it's almost same as [`deasync`](https://github.com/abbr/deasync) but requires no native bindings or `node-gyp`.

See [benchmark.cjs](./benchmarks/benchmark.cjs.txt) and [benchmark.esm](./benchmarks/benchmark.esm.txt) for more details.

You can try it with running `yarn benchmark` by yourself. [Here](./benchmarks/benchmark.js) is the benchmark source code.

## Sponsors

| 1stG                                                                                | RxTS                                                                                | UnTS                                                                                |
| ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| [![1stG Open Collective backers and sponsors](https://opencollective.com/1stG/organizations.svg)](https://opencollective.com/1stG) | [![RxTS Open Collective backers and sponsors](https://opencollective.com/rxts/organizations.svg)](https://opencollective.com/rxts) | [![UnTS Open Collective backers and sponsors](https://opencollective.com/unts/organizations.svg)](https://opencollective.com/unts) |

## Backers

| 1stG                                                                                | RxTS                                                                                | UnTS                                                                                |
| -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
| [![1stG Open Collective backers and sponsors](https://opencollective.com/1stG/individuals.svg)](https://opencollective.com/1stG) | [![RxTS Open Collective backers and sponsors](https://opencollective.com/rxts/individuals.svg)](https://opencollective.com/rxts) | [![UnTS Open Collective backers and sponsors](https://opencollective.com/unts/individuals.svg)](https://opencollective.com/unts) |

## Changelog

Detailed changes for each release are documented in [CHANGELOG.md](./CHANGELOG.md).

## License

[MIT][] © [JounQin][]@[1stG.me][]

[`esbuild-register`]: https://github.com/egoist/esbuild-register
[`esbuild-runner`]: https://github.com/folke/esbuild-runner
[`@swc-node/register`]: https://github.com/swc-project/swc-node/tree/master/packages/register
[`tsx`]: https://github.com/esbuild-kit/tsx
[1stg.me]: https://www.1stg.me
[jounqin]: https://GitHub.com/JounQin
[mit]: http://opensource.org/licenses/MIT


code:
'use strict';

var node_crypto = require('node:crypto');
var fs = require('node:fs');
var node_module = require('node:module');
var path = require('node:path');
var node_url = require('node:url');
var node_worker_threads = require('node:worker_threads');
var core = require('@pkgr/core');

var __async = (__this, __arguments, generator) => {
  return new Promise((resolve, reject) => {
    var fulfilled = (value) => {
      try {
        step(generator.next(value));
      } catch (e) {
        reject(e);
      }
    };
    var rejected = (value) => {
      try {
        step(generator.throw(value));
      } catch (e) {
        reject(e);
      }
    };
    var step = (x) => x.done ? resolve(x.value) : Promise.resolve(x.value).then(fulfilled, rejected);
    step((generator = generator.apply(__this, __arguments)).next());
  });
};
const import_meta = {};
const TsRunner = {
  // https://github.com/TypeStrong/ts-node
  TsNode: "ts-node",
  // https://github.com/egoist/esbuild-register
  EsbuildRegister: "esbuild-register",
  // https://github.com/folke/esbuild-runner
  EsbuildRunner: "esbuild-runner",
  // https://github.com/swc-project/swc-node/tree/master/packages/register
  SWC: "swc",
  // https://github.com/esbuild-kit/tsx
  TSX: "tsx"
};
const {
  SYNCKIT_BUFFER_SIZE,
  SYNCKIT_TIMEOUT,
  SYNCKIT_EXEC_ARGV,
  SYNCKIT_TS_RUNNER,
  SYNCKIT_GLOBAL_SHIMS,
  NODE_OPTIONS
} = process.env;
const DEFAULT_BUFFER_SIZE = SYNCKIT_BUFFER_SIZE ? +SYNCKIT_BUFFER_SIZE : void 0;
const DEFAULT_TIMEOUT = SYNCKIT_TIMEOUT ? +SYNCKIT_TIMEOUT : void 0;
const DEFAULT_WORKER_BUFFER_SIZE = DEFAULT_BUFFER_SIZE || 1024;
const DEFAULT_EXEC_ARGV = (SYNCKIT_EXEC_ARGV == null ? void 0 : SYNCKIT_EXEC_ARGV.split(",")) || [];
const DEFAULT_TS_RUNNER = SYNCKIT_TS_RUNNER;
const DEFAULT_GLOBAL_SHIMS = ["1", "true"].includes(
  SYNCKIT_GLOBAL_SHIMS
);
const DEFAULT_GLOBAL_SHIMS_PRESET = [
  {
    moduleName: "node-fetch",
    globalName: "fetch"
  },
  {
    moduleName: "node:perf_hooks",
    globalName: "performance",
    named: "performance"
  }
];
const MTS_SUPPORTED_NODE_VERSION = 16;
const syncFnCache = /* @__PURE__ */ new Map();
function extractProperties(object) {
  if (object && typeof object === "object") {
    const properties = {};
    for (const key in object) {
      properties[key] = object[key];
    }
    return properties;
  }
}
function createSyncFn(workerPath, bufferSizeOrOptions, timeout) {
  if (!path.isAbsolute(workerPath)) {
    throw new Error("`workerPath` must be absolute");
  }
  const cachedSyncFn = syncFnCache.get(workerPath);
  if (cachedSyncFn) {
    return cachedSyncFn;
  }
  const syncFn = startWorkerThread(
    workerPath,
    /* istanbul ignore next */
    typeof bufferSizeOrOptions === "number" ? { bufferSize: bufferSizeOrOptions, timeout } : bufferSizeOrOptions
  );
  syncFnCache.set(workerPath, syncFn);
  return syncFn;
}
const cjsRequire = typeof require === "undefined" ? node_module.createRequire(import_meta.url) : (
  /* istanbul ignore next */
  require
);
const dataUrl = (code) => new URL(`data:text/javascript,${encodeURIComponent(code)}`);
const isFile = (path2) => {
  var _a;
  try {
    return !!((_a = fs.statSync(path2, { throwIfNoEntry: false })) == null ? void 0 : _a.isFile());
  } catch (e) {
    return false;
  }
};
const setupTsRunner = (workerPath, { execArgv, tsRunner }) => {
  let ext = path.extname(workerPath);
  if (!/[/\\]node_modules[/\\]/.test(workerPath) && (!ext || /^\.[cm]?js$/.test(ext))) {
    const workPathWithoutExt = ext ? workerPath.slice(0, -ext.length) : workerPath;
    let extensions;
    switch (ext) {
      case ".cjs": {
        extensions = [".cts", ".cjs"];
        break;
      }
      case ".mjs": {
        extensions = [".mts", ".mjs"];
        break;
      }
      default: {
        extensions = [".ts", ".js"];
        break;
      }
    }
    const found = core.tryExtensions(workPathWithoutExt, extensions);
    let differentExt;
    if (found && (!ext || (differentExt = found !== workPathWithoutExt))) {
      workerPath = found;
      if (differentExt) {
        ext = path.extname(workerPath);
      }
    }
  }
  const isTs = /\.[cm]?ts$/.test(workerPath);
  let jsUseEsm = workerPath.endsWith(".mjs");
  let tsUseEsm = workerPath.endsWith(".mts");
  if (isTs) {
    if (!tsUseEsm) {
      const pkg = core.findUp(workerPath);
      if (pkg) {
        tsUseEsm = cjsRequire(pkg).type === "module";
      }
    }
    if (tsRunner == null && core.isPkgAvailable(TsRunner.TsNode)) {
      tsRunner = TsRunner.TsNode;
    }
    switch (tsRunner) {
      case TsRunner.TsNode: {
        if (tsUseEsm) {
          if (!execArgv.includes("--loader")) {
            execArgv = ["--loader", `${TsRunner.TsNode}/esm`, ...execArgv];
          }
        } else if (!execArgv.includes("-r")) {
          execArgv = ["-r", `${TsRunner.TsNode}/register`, ...execArgv];
        }
        break;
      }
      case TsRunner.EsbuildRegister: {
        if (!execArgv.includes("-r")) {
          execArgv = ["-r", TsRunner.EsbuildRegister, ...execArgv];
        }
        break;
      }
      case TsRunner.EsbuildRunner: {
        if (!execArgv.includes("-r")) {
          execArgv = ["-r", `${TsRunner.EsbuildRunner}/register`, ...execArgv];
        }
        break;
      }
      case TsRunner.SWC: {
        if (!execArgv.includes("-r")) {
          execArgv = ["-r", `@${TsRunner.SWC}-node/register`, ...execArgv];
        }
        break;
      }
      case TsRunner.TSX: {
        if (!execArgv.includes("--loader")) {
          execArgv = ["--loader", TsRunner.TSX, ...execArgv];
        }
        break;
      }
      default: {
        throw new Error(`Unknown ts runner: ${String(tsRunner)}`);
      }
    }
  } else if (!jsUseEsm) {
    const pkg = core.findUp(workerPath);
    if (pkg) {
      jsUseEsm = cjsRequire(pkg).type === "module";
    }
  }
  if (process.versions.pnp) {
    const nodeOptions = NODE_OPTIONS == null ? void 0 : NODE_OPTIONS.split(/\s+/);
    let pnpApiPath;
    try {
      pnpApiPath = cjsRequire.resolve("pnpapi");
    } catch (e) {
    }
    if (pnpApiPath && !(nodeOptions == null ? void 0 : nodeOptions.some(
      (option, index) => ["-r", "--require"].includes(option) && pnpApiPath === cjsRequire.resolve(nodeOptions[index + 1])
    )) && !execArgv.includes(pnpApiPath)) {
      execArgv = ["-r", pnpApiPath, ...execArgv];
      const pnpLoaderPath = path.resolve(pnpApiPath, "../.pnp.loader.mjs");
      if (isFile(pnpLoaderPath)) {
        const experimentalLoader = node_url.pathToFileURL(pnpLoaderPath).toString();
        execArgv = ["--experimental-loader", experimentalLoader, ...execArgv];
      }
    }
  }
  return {
    ext,
    isTs,
    jsUseEsm,
    tsRunner,
    tsUseEsm,
    workerPath,
    execArgv
  };
};
const md5Hash = (text) => node_crypto.createHash("md5").update(text).digest("hex");
const encodeImportModule = (moduleNameOrGlobalShim, type = "import") => {
  const { moduleName, globalName, named, conditional } = typeof moduleNameOrGlobalShim === "string" ? { moduleName: moduleNameOrGlobalShim } : moduleNameOrGlobalShim;
  const importStatement = type === "import" ? `import${globalName ? " " + (named === null ? "* as " + globalName : (named == null ? void 0 : named.trim()) ? `{${named}}` : globalName) + " from" : ""} '${path.isAbsolute(moduleName) ? String(node_url.pathToFileURL(moduleName)) : moduleName}'` : `${globalName ? "const " + ((named == null ? void 0 : named.trim()) ? `{${named}}` : globalName) + "=" : ""}require('${moduleName.replace(/\\/g, "\\\\")}')`;
  if (!globalName) {
    return importStatement;
  }
  const overrideStatement = `globalThis.${globalName}=${(named == null ? void 0 : named.trim()) ? named : globalName}`;
  return importStatement + (conditional === false ? `;${overrideStatement}` : `;if(!globalThis.${globalName})${overrideStatement}`);
};
const _generateGlobals = (globalShims, type) => globalShims.reduce(
  (acc, shim) => `${acc}${acc ? ";" : ""}${encodeImportModule(shim, type)}`,
  ""
);
const globalsCache = /* @__PURE__ */ new Map();
let tmpdir;
const _dirname = typeof __dirname === "undefined" ? path.dirname(node_url.fileURLToPath(import_meta.url)) : (
  /* istanbul ignore next */
  __dirname
);
const generateGlobals = (workerPath, globalShims, type = "import") => {
  const cached = globalsCache.get(workerPath);
  if (cached) {
    const [content2, filepath2] = cached;
    if (type === "require" && !filepath2 || type === "import" && filepath2 && isFile(filepath2)) {
      return content2;
    }
  }
  const globals = _generateGlobals(globalShims, type);
  let content = globals;
  let filepath;
  if (type === "import") {
    if (!tmpdir) {
      tmpdir = path.resolve(core.findUp(_dirname), "../node_modules/.synckit");
    }
    fs.mkdirSync(tmpdir, { recursive: true });
    filepath = path.resolve(tmpdir, md5Hash(workerPath) + ".mjs");
    content = encodeImportModule(filepath);
    fs.writeFileSync(filepath, globals);
  }
  globalsCache.set(workerPath, [content, filepath]);
  return content;
};
function startWorkerThread(workerPath, {
  bufferSize = DEFAULT_WORKER_BUFFER_SIZE,
  timeout = DEFAULT_TIMEOUT,
  execArgv = DEFAULT_EXEC_ARGV,
  tsRunner = DEFAULT_TS_RUNNER,
  transferList = [],
  globalShims = DEFAULT_GLOBAL_SHIMS
} = {}) {
  const { port1: mainPort, port2: workerPort } = new node_worker_threads.MessageChannel();
  const {
    isTs,
    ext,
    jsUseEsm,
    tsUseEsm,
    tsRunner: finalTsRunner,
    workerPath: finalWorkerPath,
    execArgv: finalExecArgv
  } = setupTsRunner(workerPath, { execArgv, tsRunner });
  const workerPathUrl = node_url.pathToFileURL(finalWorkerPath);
  if (/\.[cm]ts$/.test(finalWorkerPath)) {
    const isTsxSupported = !tsUseEsm || Number.parseFloat(process.versions.node) >= MTS_SUPPORTED_NODE_VERSION;
    if (!finalTsRunner) {
      throw new Error("No ts runner specified, ts worker path is not supported");
    } else if ([
      // https://github.com/egoist/esbuild-register/issues/79
      TsRunner.EsbuildRegister,
      // https://github.com/folke/esbuild-runner/issues/67
      TsRunner.EsbuildRunner,
      // https://github.com/swc-project/swc-node/issues/667
      TsRunner.SWC,
      .../* istanbul ignore next */
      isTsxSupported ? [] : [TsRunner.TSX]
    ].includes(finalTsRunner)) {
      throw new Error(
        `${finalTsRunner} is not supported for ${ext} files yet` + /* istanbul ignore next */
        (isTsxSupported ? ", you can try [tsx](https://github.com/esbuild-kit/tsx) instead" : "")
      );
    }
  }
  const finalGlobalShims = (globalShims === true ? DEFAULT_GLOBAL_SHIMS_PRESET : Array.isArray(globalShims) ? globalShims : []).filter(({ moduleName }) => core.isPkgAvailable(moduleName));
  const useGlobals = finalGlobalShims.length > 0;
  const useEval = isTs ? !tsUseEsm : !jsUseEsm && useGlobals;
  const worker = new node_worker_threads.Worker(
    jsUseEsm && useGlobals || tsUseEsm && finalTsRunner === TsRunner.TsNode ? dataUrl(
      `${generateGlobals(
        finalWorkerPath,
        finalGlobalShims
      )};import '${String(workerPathUrl)}'`
    ) : useEval ? `${generateGlobals(
      finalWorkerPath,
      finalGlobalShims,
      "require"
    )};${encodeImportModule(finalWorkerPath, "require")}` : workerPathUrl,
    {
      eval: useEval,
      workerData: { workerPort },
      transferList: [workerPort, ...transferList],
      execArgv: finalExecArgv
    }
  );
  let nextID = 0;
  const syncFn = (...args) => {
    const id = nextID++;
    const sharedBuffer = new SharedArrayBuffer(bufferSize);
    const sharedBufferView = new Int32Array(sharedBuffer);
    const msg = { sharedBuffer, id, args };
    worker.postMessage(msg);
    const status = Atomics.wait(sharedBufferView, 0, 0, timeout);
    if (!["ok", "not-equal"].includes(status)) {
      throw new Error("Internal error: Atomics.wait() failed: " + status);
    }
    const {
      id: id2,
      result,
      error,
      properties
    } = node_worker_threads.receiveMessageOnPort(mainPort).message;
    if (id !== id2) {
      throw new Error(`Internal error: Expected id ${id} but got id ${id2}`);
    }
    if (error) {
      throw Object.assign(error, properties);
    }
    return result;
  };
  worker.unref();
  return syncFn;
}
function runAsWorker(fn) {
  if (!node_worker_threads.workerData) {
    return;
  }
  const { workerPort } = node_worker_threads.workerData;
  node_worker_threads.parentPort.on(
    "message",
    ({ sharedBuffer, id, args }) => {
      (() => __async(this, null, function* () {
        const sharedBufferView = new Int32Array(sharedBuffer);
        let msg;
        try {
          msg = { id, result: yield fn(...args) };
        } catch (error) {
          msg = { id, error, properties: extractProperties(error) };
        }
        workerPort.postMessage(msg);
        Atomics.add(sharedBufferView, 0, 1);
        Atomics.notify(sharedBufferView, 0);
      }))();
    }
  );
}

exports.DEFAULT_BUFFER_SIZE = DEFAULT_BUFFER_SIZE;
exports.DEFAULT_EXEC_ARGV = DEFAULT_EXEC_ARGV;
exports.DEFAULT_GLOBAL_SHIMS = DEFAULT_GLOBAL_SHIMS;
exports.DEFAULT_GLOBAL_SHIMS_PRESET = DEFAULT_GLOBAL_SHIMS_PRESET;
exports.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
exports.DEFAULT_TS_RUNNER = DEFAULT_TS_RUNNER;
exports.DEFAULT_WORKER_BUFFER_SIZE = DEFAULT_WORKER_BUFFER_SIZE;
exports.MTS_SUPPORTED_NODE_VERSION = MTS_SUPPORTED_NODE_VERSION;
exports.TsRunner = TsRunner;
exports._generateGlobals = _generateGlobals;
exports.createSyncFn = createSyncFn;
exports.encodeImportModule = encodeImportModule;
exports.extractProperties = extractProperties;
exports.generateGlobals = generateGlobals;
exports.isFile = isFile;
exports.runAsWorker = runAsWorker;


code:
/// <reference types="node" />
import { type TransferListItem } from 'node:worker_threads';
import type { AnyAsyncFn, GlobalShim, Syncify, ValueOf } from './types.js';
export * from './types.js';
export declare const TsRunner: {
    readonly TsNode: "ts-node";
    readonly EsbuildRegister: "esbuild-register";
    readonly EsbuildRunner: "esbuild-runner";
    readonly SWC: "swc";
    readonly TSX: "tsx";
};
export type TsRunner = ValueOf<typeof TsRunner>;
export declare const DEFAULT_BUFFER_SIZE: number | undefined;
export declare const DEFAULT_TIMEOUT: number | undefined;
export declare const DEFAULT_WORKER_BUFFER_SIZE: number;
export declare const DEFAULT_EXEC_ARGV: string[];
export declare const DEFAULT_TS_RUNNER: TsRunner | undefined;
export declare const DEFAULT_GLOBAL_SHIMS: boolean;
export declare const DEFAULT_GLOBAL_SHIMS_PRESET: GlobalShim[];
export declare const MTS_SUPPORTED_NODE_VERSION = 16;
export interface SynckitOptions {
    bufferSize?: number;
    timeout?: number;
    execArgv?: string[];
    tsRunner?: TsRunner;
    transferList?: TransferListItem[];
    globalShims?: GlobalShim[] | boolean;
}
export declare function extractProperties<T extends object>(object: T): T;
export declare function extractProperties<T>(object?: T): T | undefined;
export declare function createSyncFn<T extends AnyAsyncFn>(workerPath: string, bufferSize?: number, timeout?: number): Syncify<T>;
export declare function createSyncFn<T extends AnyAsyncFn>(workerPath: string, options?: SynckitOptions): Syncify<T>;
export declare const isFile: (path: string) => boolean;
export declare const encodeImportModule: (moduleNameOrGlobalShim: GlobalShim | string, type?: 'import' | 'require') => string;
export declare const generateGlobals: (workerPath: string, globalShims: GlobalShim[], type?: 'import' | 'require') => string;
export declare function runAsWorker<R = unknown, T extends AnyAsyncFn<R> = AnyAsyncFn<R>>(fn: T): void;