├── .babelrc
├── test
├── fixtures
│ ├── test-export-all-as.jsx
│ ├── test-export-default.jsx
│ ├── test-export-default-as.jsx
│ ├── test-no-svg-or-react.js
│ ├── test-no-react.jsx
│ ├── test-no-duplicate-react.jsx
│ ├── test-commented.jsx
│ ├── test-commented-6.jsx
│ ├── test-root-styled.jsx
│ ├── test-case-sensitive.jsx
│ ├── test-import.jsx
│ ├── test-import-read-file.jsx
│ ├── test-require.jsx
│ ├── test-dynamic-require.jsx
│ ├── test-multiple-svg.jsx
│ ├── close.svg
│ ├── close-a.svg
│ ├── close2.svg
│ ├── root-styled.svg
│ ├── commented-6.svg
│ └── commented.svg
└── sanity.js
├── .npmrc
├── .gitignore
├── src
├── camelize.js
├── cssToObj.js
├── fileExistsWithCaseSync.js
├── escapeBraces.js
├── optimize.js
├── transformSvg.js
└── index.js
├── .github
└── workflows
│ ├── require-allow-edits.yml
│ ├── rebase.yml
│ ├── node-pretest.yml
│ └── node.yml
├── .eslintrc
├── LICENSE
├── package.json
├── README.md
└── CHANGELOG.md
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["airbnb"]
3 | }
4 |
--------------------------------------------------------------------------------
/test/fixtures/test-export-all-as.jsx:
--------------------------------------------------------------------------------
1 | export * as foo from 'bar';
2 |
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | package-lock=false
2 | allow-same-version=true
3 | message=v%s
4 |
--------------------------------------------------------------------------------
/test/fixtures/test-export-default.jsx:
--------------------------------------------------------------------------------
1 | export { default } from "./close-a.svg";
2 |
--------------------------------------------------------------------------------
/test/fixtures/test-export-default-as.jsx:
--------------------------------------------------------------------------------
1 | export { default as IconClose } from "./close.svg";
2 |
--------------------------------------------------------------------------------
/test/fixtures/test-no-svg-or-react.js:
--------------------------------------------------------------------------------
1 | import './path/to.jpg';
2 | import './code.js';
3 |
4 | export default class Foo {
5 | }
6 |
--------------------------------------------------------------------------------
/test/fixtures/test-no-react.jsx:
--------------------------------------------------------------------------------
1 | import MySvg from './close.svg';
2 |
3 | export function MyFunctionIcon() {
4 | return MySvg;
5 | }
6 |
--------------------------------------------------------------------------------
/test/fixtures/test-no-duplicate-react.jsx:
--------------------------------------------------------------------------------
1 | import MySvg from './close.svg';
2 |
3 | export function MyFunctionIcon() {
4 | return MySvg;
5 | }
6 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # gitignore
2 | node_modules
3 | lib
4 |
5 | # Only apps should have lockfiles
6 | npm-shrinkwrap.json
7 | package-lock.json
8 | yarn.lock
9 |
10 | .npmignore
11 |
--------------------------------------------------------------------------------
/src/camelize.js:
--------------------------------------------------------------------------------
1 | export function hyphenToCamel(name) {
2 | return name.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
3 | }
4 |
5 | export function namespaceToCamel(namespace, name) {
6 | return namespace + name.charAt(0).toUpperCase() + name.slice(1);
7 | }
8 |
--------------------------------------------------------------------------------
/test/fixtures/test-commented.jsx:
--------------------------------------------------------------------------------
1 | import SVG from './commented.svg';
2 |
3 | export function MyFunctionIcon() {
4 | return ;
5 | }
6 |
7 | export class MyClassIcon extends React.Component {
8 | render() {
9 | return ;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/test/fixtures/test-commented-6.jsx:
--------------------------------------------------------------------------------
1 | import SVG from './commented-6.svg';
2 |
3 | export function MyFunctionIcon() {
4 | return ;
5 | }
6 |
7 | export class MyClassIcon extends React.Component {
8 | render() {
9 | return ;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/test/fixtures/test-root-styled.jsx:
--------------------------------------------------------------------------------
1 | import SVG from './root-styled.svg';
2 |
3 | export function MyFunctionIcon() {
4 | return ;
5 | }
6 |
7 | export class MyClassIcon extends React.Component {
8 | render() {
9 | return ;
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/.github/workflows/require-allow-edits.yml:
--------------------------------------------------------------------------------
1 | name: Require “Allow Edits”
2 |
3 | on: [pull_request_target]
4 |
5 | jobs:
6 | _:
7 | name: "Require “Allow Edits”"
8 |
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: ljharb/require-allow-edits@main
13 |
--------------------------------------------------------------------------------
/test/fixtures/test-case-sensitive.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MySvg from './Close.svg';
3 |
4 | export function MyFunctionIcon() {
5 | return ;
6 | }
7 |
8 | export class MyClassIcon extends React.Component {
9 | render() {
10 | return ;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/test/fixtures/test-import.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MySvg from './close.svg';
3 |
4 | export default function MyFunctionIcon() {
5 | return ;
6 | }
7 |
8 | export class MyClassIcon extends React.Component {
9 | render() {
10 | return ;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/test/fixtures/test-import-read-file.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MySvg from './close.svg';
3 |
4 | export default function MyFunctionIcon() {
5 | return ;
6 | }
7 |
8 | export class MyClassIcon extends React.Component {
9 | render() {
10 | return ;
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/test/fixtures/test-require.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const MySvg = require('./close.svg');
4 |
5 | export default function MyFunctionIcon() {
6 | return ;
7 | }
8 |
9 | export class MyClassIcon extends React.Component {
10 | render() {
11 | return ;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/src/cssToObj.js:
--------------------------------------------------------------------------------
1 | export default function cssToObj(css) {
2 | const o = {};
3 | css.split(';')
4 | .filter((el) => !!el)
5 | .forEach((el) => {
6 | const s = el.split(':');
7 | const key = s.shift().trim();
8 | const value = s.join(':').trim();
9 | o[key] = value;
10 | });
11 |
12 | return o;
13 | }
14 |
--------------------------------------------------------------------------------
/.github/workflows/rebase.yml:
--------------------------------------------------------------------------------
1 | name: Automatic Rebase
2 |
3 | on: [pull_request_target]
4 |
5 | jobs:
6 | _:
7 | name: "Automatic Rebase"
8 |
9 | runs-on: ubuntu-latest
10 |
11 | steps:
12 | - uses: actions/checkout@v2
13 | - uses: ljharb/rebase@master
14 | env:
15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
16 |
--------------------------------------------------------------------------------
/test/fixtures/test-dynamic-require.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const MySvg = require('./close.svg');
4 |
5 | export const foo = require(path.join(pkgBase, 'package.json')); // regression test
6 |
7 | export default function MyFunctionIcon() {
8 | return ;
9 | }
10 |
11 | export class MyClassIcon extends React.Component {
12 | render() {
13 | return ;
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/test/fixtures/test-multiple-svg.jsx:
--------------------------------------------------------------------------------
1 | import MySvg from './close.svg';
2 | import MySvg2 from './close.svg';
3 | import MySvg3 from './close2.svg';
4 |
5 | export function MyFunctionIcon() {
6 | return ;
7 | }
8 |
9 | export class MyClassIcon extends React.Component {
10 | render() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/src/fileExistsWithCaseSync.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs');
2 | const path = require('path');
3 |
4 | // Based on https://stackoverflow.com/questions/27367261/check-if-file-exists-case-sensitive
5 | export default function fileExistsWithCaseSync(filepath) {
6 | const dir = path.dirname(filepath);
7 | if (dir === '/' || dir === '.') return true;
8 | const filenames = fs.readdirSync(dir);
9 | return filenames.indexOf(path.basename(filepath)) !== -1;
10 | }
11 |
--------------------------------------------------------------------------------
/src/escapeBraces.js:
--------------------------------------------------------------------------------
1 | /* If the SVG has text that has curly braces, or
2 | if there is a
8 | // to
9 | //
10 | return { ...raw, data: raw.data.replace(/(\{|\})/g, '{`$1`}') };
11 | }
12 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "airbnb",
3 |
4 | "root": true,
5 |
6 | "ignorePatterns": [
7 | "lib/",
8 | ],
9 |
10 | "overrides": [
11 | {
12 | "files": "test/**",
13 | "rules": {
14 | "no-console": "off",
15 | },
16 | },
17 | {
18 | "files": "test/fixtures/**",
19 | "rules": {
20 | "import/extensions": "off",
21 | "import/no-unresolved": "off",
22 | },
23 | },
24 | ],
25 | }
26 |
--------------------------------------------------------------------------------
/.github/workflows/node-pretest.yml:
--------------------------------------------------------------------------------
1 | name: 'Tests: pretest/posttest'
2 |
3 | on: [pull_request, push]
4 |
5 | jobs:
6 | pretest:
7 | runs-on: ubuntu-latest
8 |
9 | steps:
10 | - uses: actions/checkout@v2
11 | - uses: ljharb/actions/node/install@main
12 | name: 'nvm install lts/* && npm install'
13 | with:
14 | node-version: 'lts/*'
15 | - run: npm run pretest
16 |
17 | posttest:
18 | runs-on: ubuntu-latest
19 |
20 | steps:
21 | - uses: actions/checkout@v2
22 | - uses: ljharb/actions/node/install@main
23 | name: 'nvm install lts/* && npm install'
24 | with:
25 | node-version: 'lts/*'
26 | - run: npm run posttest
27 |
--------------------------------------------------------------------------------
/test/fixtures/close.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/test/fixtures/close-a.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/test/fixtures/close2.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/test/fixtures/root-styled.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/test/fixtures/commented-6.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/test/fixtures/commented.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Jordan Harband
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/.github/workflows/node.yml:
--------------------------------------------------------------------------------
1 | name: 'Tests: node.js'
2 |
3 | on: [pull_request, push]
4 |
5 | jobs:
6 | matrix:
7 | runs-on: ubuntu-latest
8 | outputs:
9 | latest: ${{ steps.set-matrix.outputs.requireds }}
10 | steps:
11 | - uses: ljharb/actions/node/matrix@main
12 | id: set-matrix
13 | with:
14 | versionsAsRoot: true
15 | type: 'majors'
16 | preset: '10.13 || >=10.13'
17 |
18 | test:
19 | needs: [matrix]
20 | name: 'latest majors'
21 | runs-on: ubuntu-latest
22 |
23 | strategy:
24 | fail-fast: false
25 | matrix:
26 | node-version: ${{ fromJson(needs.matrix.outputs.latest) }}
27 |
28 | steps:
29 | - uses: actions/checkout@v3
30 | - uses: ljharb/actions/node/install@main
31 | name: 'nvm install ${{ matrix.node-version }} && npm install'
32 | with:
33 | node-version: ${{ matrix.node-version }}
34 | skip-ls-check: true
35 | - run: npm run tests-only
36 | - uses: codecov/codecov-action@v1
37 |
38 | node:
39 | name: 'node.js'
40 | needs: [test]
41 | runs-on: ubuntu-latest
42 | steps:
43 | - run: 'echo tests completed'
44 |
--------------------------------------------------------------------------------
/src/optimize.js:
--------------------------------------------------------------------------------
1 | // validates svgo opts
2 | // to contain minimal set of plugins that will strip some stuff
3 | // for the babylon JSX parser to work
4 | import * as SVGO from 'svgo';
5 | import isPlainObject from 'lodash.isplainobject';
6 |
7 | const essentialPlugins = ['removeDoctype', 'removeComments'];
8 |
9 | function isEssentialPlugin(p) {
10 | return essentialPlugins.indexOf(p) !== -1;
11 | }
12 |
13 | function validateAndFix(opts) {
14 | if (!isPlainObject(opts)) return;
15 |
16 | if (opts.full) {
17 | if (
18 | typeof opts.plugins === 'undefined'
19 | || (Array.isArray(opts.plugins) && opts.plugins.length === 0)
20 | ) {
21 | /* eslint no-param-reassign: 1 */
22 | opts.plugins = [...essentialPlugins];
23 | return;
24 | }
25 | }
26 |
27 | // opts.full is false, plugins can be empty
28 | if (typeof opts.plugins === 'undefined') return;
29 | if (Array.isArray(opts.plugins) && opts.plugins.length === 0) return;
30 |
31 | // track whether its defined in opts.plugins
32 | const state = essentialPlugins.reduce((p, c) => Object.assign(p, { [c]: false }), {});
33 |
34 | opts.plugins.forEach((p) => {
35 | if (typeof p === 'string' && isEssentialPlugin(p)) {
36 | state[p] = true;
37 | } else if (typeof p === 'object') {
38 | Object.keys(p).forEach((k) => {
39 | if (isEssentialPlugin(k)) {
40 | // make it essential
41 | if (!p[k]) p[k] = true;
42 | // and update state
43 | /* eslint no-param-reassign: 1 */
44 | state[k] = true;
45 | }
46 | });
47 | }
48 | });
49 |
50 | Object.keys(state)
51 | .filter((key) => !state[key])
52 | .forEach((key) => opts.plugins.push(key));
53 | }
54 |
55 | export default function optimize(content, opts = {}) {
56 | validateAndFix(opts);
57 |
58 | return SVGO.optimize(content, opts);
59 | }
60 |
--------------------------------------------------------------------------------
/src/transformSvg.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-param-reassign */
2 | //
3 | // These visitors normalize the SVG into something React understands:
4 | //
5 |
6 | import { namespaceToCamel, hyphenToCamel } from './camelize';
7 | import cssToObj from './cssToObj';
8 |
9 | export default (t) => ({
10 | JSXAttribute({ node }) {
11 | const { name: originalName } = node;
12 | if (t.isJSXNamespacedName(originalName)) {
13 | // converts
14 | //