Repository: smooth-code/svgr
Pull request #172 (2018-09-16 13:40:26)

Svg props

TrySound: Do you know you could push commits to my branch directly?
neoziro: @TrySound I didn't know! sorry
neoziro: @TrySound OK for you? I will merge it.
TrySound: Sure, looks good to me. Waiting for release :)

Patch:
diff --git a/README.md b/README.md
index 485d7a2..12cc729 100644
--- a/README.md
+++ b/README.md
@@ -80,7 +80,7 @@ powerful and configurable HTML transpiler. It uses AST (like
 ## Command line usage
 
 ```
-Usage: svgr [options] <file>
+Usage: svgr [options] <file|directory>
 
 Options:
 
@@ -94,7 +94,8 @@ Options:
   --ref                              add svgRef prop to svg
   --no-dimensions                    remove width and height from root SVG tag
   --no-expand-props                  disable props expanding
-  --svg-attributes <property=value>  add some attributes to the svg
+  --svg-attributes <property=value>  add attributes to the svg element (deprecated)
+  --svg-props <property=value>       add props to the svg element
   --replace-attr-values <old=new>    replace an attribute value
   --template <file>                  specify a custom template to use
   --title-prop                       create a title element linked with props
@@ -104,8 +105,8 @@ Options:
   --no-svgo                          disable SVGO
   -h, --help                         output usage information
 
-Examples:
-  svgr --replace-attr-values "#fff=currentColor" icon.svg
+  Examples:
+    svgr --replace-attr-values "#fff=currentColor" icon.svg
 ```
 
 ### Recipes
@@ -344,13 +345,15 @@ change an icon color to "currentColor" in order to inherit from text color.
 | ------- | --------------------------------- | ------------------------------------ |
 | `[]`    | `--replace-attr-values <old=new>` | `replaceAttrValues: { old: 'new' }>` |
 
-### SVG attributes
+### SVG props
 
-Add attributes to the root SVG tag.
+Add props to the root SVG tag.
 
-| Default | CLI Override                    | API Override                        |
-| ------- | ------------------------------- | ----------------------------------- |
-| `[]`    | `--svg-attributes <name=value>` | `svgAttributes: { name: 'value' }>` |
+| Default | CLI Override               | API Override                   |
+| ------- | -------------------------- | ------------------------------ |
+| `[]`    | `--svg-props <name=value>` | `svgProps: { name: 'value' }>` |
+
+> You can specify dynamic property using curly braces: `{ focusable: "{true}" }` or `--svg-props focusable={true}`. It is particulary useful with a custom template.
 
 ### Template
 
diff --git a/packages/cli/README.md b/packages/cli/README.md
index 4654173..95d8461 100644
--- a/packages/cli/README.md
+++ b/packages/cli/README.md
@@ -13,29 +13,30 @@ npm install @svgr/cli
 ## Usage
 
 ```
-  Usage: index [options] <file>
-
-  Options:
-
-    -V, --version                      output the version number
-    --config <file>                    specify the path of the svgr config
-    -d, --out-dir <dirname>            output files into a directory
-    --ext <ext>                        specify a custom file extension (default: "js")
-    --filename-case <case>             specify filename case (pascal, kebab, camel) (default: "pascal")
-    --icon                             use "1em" as width and height
-    --native                           add react-native support with react-native-svg
-    --ref                              add svgRef prop to svg
-    --no-dimensions                    remove width and height from root SVG tag
-    --no-expand-props                  disable props expanding
-    --svg-attributes <property=value>  add some attributes to the svg
-    --replace-attr-values <old=new>    replace an attribute value
-    --template <file>                  specify a custom template to use
-    --title-prop                       create a title element linked with props
-    --prettier-config <fileOrJson>     Prettier config
-    --no-prettier                      disable Prettier
-    --svgo-config <fileOrJson>         SVGO config
-    --no-svgo                          disable SVGO
-    -h, --help                         output usage information
+Usage: svgr [options] <file|directory>
+
+Options:
+
+  -V, --version                      output the version number
+  --config <file>                    specify the path of the svgr config
+  -d, --out-dir <dirname>            output files into a directory
+  --ext <ext>                        specify a custom file extension (default: "js")
+  --filename-case <case>             specify filename case (pascal, kebab, camel) (default: "pascal")
+  --icon                             use "1em" as width and height
+  --native                           add react-native support with react-native-svg
+  --ref                              add svgRef prop to svg
+  --no-dimensions                    remove width and height from root SVG tag
+  --no-expand-props                  disable props expanding
+  --svg-attributes <property=value>  add attributes to the svg element (deprecated)
+  --svg-props <property=value>       add props to the svg element
+  --replace-attr-values <old=new>    replace an attribute value
+  --template <file>                  specify a custom template to use
+  --title-prop                       create a title element linked with props
+  --prettier-config <fileOrJson>     Prettier config
+  --no-prettier                      disable Prettier
+  --svgo-config <fileOrJson>         SVGO config
+  --no-svgo                          disable SVGO
+  -h, --help                         output usage information
 
   Examples:
     svgr --replace-attr-values "#fff=currentColor" icon.svg
diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js
index 66503db..72bf472 100644
--- a/packages/cli/src/index.js
+++ b/packages/cli/src/index.js
@@ -3,6 +3,7 @@ import program from 'commander'
 import path from 'path'
 import glob from 'glob'
 import fs from 'fs'
+import chalk from 'chalk'
 import pkg from '../package.json'
 import fileCommand from './fileCommand'
 import dirCommand from './dirCommand'
@@ -51,7 +52,12 @@ program
   .option('--no-expand-props', 'disable props expanding')
   .option(
     '--svg-attributes <property=value>',
-    'add some attributes to the svg',
+    'add attributes to the svg element (deprecated)',
+    parseObject,
+  )
+  .option(
+    '--svg-props <property=value>',
+    'add props to the svg element',
     parseObject,
   )
   .option(
@@ -123,6 +129,15 @@ async function run() {
     }
   }
 
+  // TODO remove in v3
+  if (program.outDir && program.svgAttributes) {
+    console.log(
+      chalk.yellow(
+        '--svg-attributes option is deprecated an will be removed in v3, please use --svg-props instead',
+      ),
+    )
+  }
+
   const command = program.outDir ? dirCommand : fileCommand
   await command(program, filenames, config)
 }
diff --git a/packages/core/src/config.js b/packages/core/src/config.js
index 381ca32..34da6a4 100644
--- a/packages/core/src/config.js
+++ b/packages/core/src/config.js
@@ -11,6 +11,7 @@ export const DEFAULT_CONFIG = {
   ref: false,
   replaceAttrValues: null,
   svgAttributes: null,
+  svgProps: null,
   svgo: true,
   svgoConfig: null,
   template: null,
diff --git a/packages/core/src/h2x/svgAttributes.js b/packages/core/src/h2x/svgAttributes.js
index 7ae948f..11d6d24 100644
--- a/packages/core/src/h2x/svgAttributes.js
+++ b/packages/core/src/h2x/svgAttributes.js
@@ -16,7 +16,7 @@ const areAttrsAlreadyInjected = (node, attributes = {}) => {
   }, true)
 }
 
-const svgAttribute = (attributes = {}) => () => {
+const svgAttributes = (attributes = {}) => () => {
   const keys = Object.keys(attributes)
 
   return {
@@ -27,10 +27,10 @@ const svgAttribute = (attributes = {}) => () => {
           if (areAttrsAlreadyInjected(path.node, attributes)) return
 
           const parseAttributes = keys.reduce((accumulation, key) => {
-            const props = new JSXAttribute()
-            props.name = key
-            props.value = attributes[key]
-            return [...accumulation, props]
+            const prop = new JSXAttribute()
+            prop.name = key
+            prop.value = attributes[key]
+            return [...accumulation, prop]
           }, [])
 
           const mergeAttributes = path.node.attributes.reduce(
@@ -50,4 +50,4 @@ const svgAttribute = (attributes = {}) => () => {
   }
 }
 
-export default svgAttribute
+export default svgAttributes
diff --git a/packages/core/src/h2x/svgProps.js b/packages/core/src/h2x/svgProps.js
new file mode 100644
index 0000000..e544665
--- /dev/null
+++ b/packages/core/src/h2x/svgProps.js
@@ -0,0 +1,69 @@
+import { JSXAttribute } from 'h2x-plugin-jsx'
+
+const compareAttrsName = attr => otherAttr => attr.name === otherAttr.name
+const compareAttrsValue = attr => otherAttr => attr.value === otherAttr.value
+
+const compareAttrs = attr => otherAttr =>
+  compareAttrsName(attr)(otherAttr) && compareAttrsValue(attr)(otherAttr)
+
+const areAttrsAlreadyInjected = (node, attributes = {}) => {
+  const nodeAttrs = node.attributes
+
+  return Object.keys(attributes).reduce((accumulation, key) => {
+    if (nodeAttrs.some(compareAttrs({ name: key, value: attributes[key] })))
+      return accumulation
+    return false
+  }, true)
+}
+
+const svgProps = (props = {}) => () => {
+  const interpolated = new Set()
+  const keys = Object.keys(props)
+  const attributes = keys.reduce((acc, prop) => {
+    const value = props[prop]
+    if (
+      typeof value === 'string' &&
+      value.startsWith('{') &&
+      value.endsWith('}')
+    ) {
+      acc[prop] = value.slice(1, -1)
+      interpolated.add(prop)
+    } else {
+      acc[prop] = value
+    }
+    return acc
+  }, {})
+
+  return {
+    visitor: {
+      JSXElement: {
+        enter(path) {
+          if (path.node.name !== 'svg') return
+          if (areAttrsAlreadyInjected(path.node, attributes)) return
+
+          const parseAttributes = keys.reduce((accumulation, key) => {
+            const prop = new JSXAttribute()
+            prop.name = key
+            prop.value = attributes[key]
+            prop.literal = interpolated.has(key)
+            return [...accumulation, prop]
+          }, [])
+
+          const mergeAttributes = path.node.attributes.reduce(
+            (accumulation, value) => {
+              if (accumulation.some(compareAttrsName(value)))
+                return accumulation
+              return [...accumulation, value]
+            },
+            parseAttributes,
+          )
+
+          path.node.attributes = mergeAttributes
+          path.replace(path.node)
+        },
+      },
+    },
+  }
+}
+
+export default svgProps
diff --git a/packages/core/src/index.js b/packages/core/src/index.js
index 2aa2e77..87248ab 100644
--- a/packages/core/src/index.js
+++ b/packages/core/src/index.js
@@ -12,6 +12,7 @@ export { default as removeStyle } from './h2x/removeStyle'
 export { default as replaceAttrValues } from './h2x/replaceAttrValues'
 export { default as stripAttribute } from './h2x/stripAttribute'
 export { default as svgAttributes } from './h2x/svgAttributes'
+export { default as svgProps } from './h2x/svgProps'
 export { default as svgRef } from './h2x/svgRef'
 export { default as titleProp } from './h2x/titleProp'
 export { default as toReactNative } from './h2x/toReactNative'
diff --git a/packages/core/src/plugins/h2x.js b/packages/core/src/plugins/h2x.js
index 1a9f618..c2663f8 100644
--- a/packages/core/src/plugins/h2x.js
+++ b/packages/core/src/plugins/h2x.js
@@ -9,6 +9,7 @@ import {
   replaceAttrValues,
   stripAttribute,
   svgAttributes,
+  svgProps,
   svgRef,
   titleProp,
   toReactNative,
@@ -27,6 +28,7 @@ function configToPlugins(config) {
   if (config.icon) plugins.push(emSize())
   if (config.ref) plugins.push(svgRef())
   if (config.svgAttributes) plugins.push(svgAttributes(config.svgAttributes))
+  if (config.svgProps) plugins.push(svgProps(config.svgProps))
   // TODO remove boolean value in the next major release
   if (config.expandProps)
     plugins.push(


Test patch:
diff --git a/packages/cli/src/__snapshots__/index.test.js.snap b/packages/cli/src/__snapshots__/index.test.js.snap
index ec3f570..77f5422 100644
--- a/packages/cli/src/__snapshots__/index.test.js.snap
+++ b/packages/cli/src/__snapshots__/index.test.js.snap
@@ -292,6 +292,20 @@ export default File
 "
 `;
 
+exports[`cli should support various args: --svg-props "hidden={true}" 1`] = `
+"import React from 'react'
+
+const File = props => (
+  <svg hidden={true} width={48} height={1} {...props}>
+    <path d=\\"M0 0h48v1H0z\\" fill=\\"#063855\\" fillRule=\\"evenodd\\" />
+  </svg>
+)
+
+export default File
+
+"
+`;
+
 exports[`cli should support various args: --title-prop 1`] = `
 "import React from 'react'
 
diff --git a/packages/cli/src/index.test.js b/packages/cli/src/index.test.js
index 77932e5..90afd34 100644
--- a/packages/cli/src/index.test.js
+++ b/packages/cli/src/index.test.js
@@ -127,6 +127,7 @@ describe('cli', () => {
     ['--ref'],
     ['--replace-attr-values "#063855=currentColor"'],
     ['--svg-attributes "focusable=false"'],
+    [`--svg-props "hidden={true}"`],
     ['--no-svgo'],
     ['--no-prettier'],
     ['--title-prop'],
diff --git a/packages/core/src/h2x/svgProps.test.js b/packages/core/src/h2x/svgProps.test.js
new file mode 100644
index 0000000..64110d7
--- /dev/null
+++ b/packages/core/src/h2x/svgProps.test.js
@@ -0,0 +1,126 @@
+import jsx from 'h2x-plugin-jsx'
+import { transform } from 'h2x-core'
+import svgProps from './svgProps'
+
+function transformSvgProps(code, props) {
+  return transform(code, { plugins: [jsx, svgProps(props)] })
+}
+
+describe('svgProps', () => {
+  it('should add one prop to svg', () => {
+    expect(
+      transformSvgProps(
+        ` <svg>
+            <g stroke="#063855" stroke-width="2">
+              <path d="M51,37 L37,51"></path>
+            </g>
+          </svg>
+        `,
+        { focusable: false },
+      ),
+    ).toMatchInlineSnapshot(`
+"<svg focusable=\\"false\\">
+  <g stroke=\\"#063855\\" strokeWidth={2}>
+    <path d=\\"M51,37 L37,51\\" />
+  </g>
+</svg>
+"
+`)
+  })
+
+  it('should add multiple props to svg', () => {
+    expect(
+      transformSvgProps(
+        ` <svg>
+            <g stroke="#063855" stroke-width="2">
+              <path d="M51,37 L37,51"></path>
+            </g>
+          </svg>
+        `,
+        {
+          focusable: false,
+          hidden: 'hidden',
+        },
+      ),
+    ).toMatchInlineSnapshot(`
+"<svg focusable=\\"false\\" hidden=\\"hidden\\">
+  <g stroke=\\"#063855\\" strokeWidth={2}>
+    <path d=\\"M51,37 L37,51\\" />
+  </g>
+</svg>
+"
+`)
+  })
+
+  it('should update old prop to the new prop', () => {
+    expect(
+      transformSvgProps(
+        ` <svg focusable="false">
+            <g stroke="#063855" stroke-width="2">
+              <path d="M51,37 L37,51"></path>
+            </g>
+          </svg>
+        `,
+        { focusable: true },
+      ),
+    ).toMatchInlineSnapshot(`
+"<svg focusable=\\"true\\">
+  <g stroke=\\"#063855\\" strokeWidth={2}>
+    <path d=\\"M51,37 L37,51\\" />
+  </g>
+</svg>
+"
+`)
+  })
+
+  it('should add a new prop', () => {
+    expect(
+      transformSvgProps(
+        ` <svg focusable="true" color="#fff">
+            <g stroke="#063855" stroke-width="2">
+              <path d="M51,37 L37,51"></path>
+            </g>
+          </svg>
+        `,
+        { focusable: true, hidden: true },
+      ),
+    ).toMatchInlineSnapshot(`
+"<svg focusable=\\"true\\" hidden=\\"true\\" color=\\"#fff\\">
+  <g stroke=\\"#063855\\" strokeWidth={2}>
+    <path d=\\"M51,37 L37,51\\" />
+  </g>
+</svg>
+"
+`)
+  })
+
+  it('should interpolate prop with curly brackets', () => {
+    expect(
+      transformSvgProps(
+        ` <svg focusable="true" color="#fff">
+            <g stroke="#063855" stroke-width="2">
+              <path d="M51,37 L37,51"></path>
+            </g>
+          </svg>
+        `,
+        {
+          // literal
+          focusable: `{true}`,
+          // variables
+          hidden: `{hidden}`,
+          // expression
+          fill: `{fill == null ? 'currentColor' : fill}`,
+          // string
+          id: 'foo',
+        },
+      ),
+    ).toMatchInlineSnapshot(`
+"<svg focusable={true} hidden={hidden} fill={fill == null ? 'currentColor' : fill} id=\\"foo\\" color=\\"#fff\\">
+  <g stroke=\\"#063855\\" strokeWidth={2}>
+    <path d=\\"M51,37 L37,51\\" />
+  </g>
+</svg>
+"
+`)
+  })
+})