diff --git a/__fixtures__/simple-existing/File..js b/__fixtures__/simple-existing/File..js new file mode 100644 index 00000000..7158f5b0 --- /dev/null +++ b/__fixtures__/simple-existing/File..js @@ -0,0 +1,11 @@ +import * as React from 'react' + +function SvgFile(props) { + return ( + + + + ) +} + +export default SvgFile diff --git a/__fixtures__/simple-existing/index..js b/__fixtures__/simple-existing/index..js new file mode 100644 index 00000000..e2a362ff --- /dev/null +++ b/__fixtures__/simple-existing/index..js @@ -0,0 +1 @@ +export { default as File. } from './File.' \ No newline at end of file diff --git a/packages/babel-plugin-transform-svg-component/src/__snapshots__/index.test.js.snap b/packages/babel-plugin-transform-svg-component/src/__snapshots__/index.test.js.snap new file mode 100644 index 00000000..7ed69aaa --- /dev/null +++ b/packages/babel-plugin-transform-svg-component/src/__snapshots__/index.test.js.snap @@ -0,0 +1,280 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` 1`] = ` +"import * as React from 'react'; + +const MyComponent = () =>
{}
; + +export default MyComponent;" +`; + +exports[` 2`] = ` +"import * as React from 'react'; + +const MyComponent = () =>
{}
; + +export default MyComponent;" +`; + +exports[`plugin javascript custom templates support basic template 1`] = ` +"import * as React from 'react'; + +const MyComponent = () => ; + +export default MyComponent;" +`; + +exports[`plugin javascript custom templates supports TypeScript template 1`] = ` +"import * as React from 'react'; + +const MyComponent = (props: React.SVGProps) => ; + +export default MyComponent;" +`; + +exports[`plugin javascript custom templates supports template that does not return an array 1`] = `";"`; + +exports[`plugin javascript transforms whole program 1`] = ` +"import * as React from \\"react\\"; + +function SvgComponent() { + return ; +} + +export default SvgComponent;" +`; + +exports[`plugin javascript with "expandProps" add props 1`] = ` +"import * as React from \\"react\\"; + +function SvgComponent(props) { + return ; +} + +export default SvgComponent;" +`; + +exports[`plugin javascript with "memo" option wrap component in "React.memo" 1`] = ` +"import * as React from \\"react\\"; + +function SvgComponent() { + return ; +} + +const MemoSvgComponent = React.memo(SvgComponent); +export default MemoSvgComponent;" +`; + +exports[`plugin javascript with "native" option adds import from "react-native-svg" 1`] = ` +"import * as React from \\"react\\"; +import Svg from \\"react-native-svg\\"; + +function SvgComponent() { + return ; +} + +export default SvgComponent;" +`; + +exports[`plugin javascript with "native.expo" option adds import from "react-native-svg" & from "expo" 1`] = ` +"import * as React from \\"react\\"; +import \\"expo\\"; + +function SvgComponent() { + return ; +} + +export default SvgComponent;" +`; + +exports[`plugin javascript with "ref" and "expandProps" option expands props 1`] = ` +"import * as React from \\"react\\"; + +function SvgComponent({ + svgRef, + ...props +}) { + return ; +} + +const ForwardRef = React.forwardRef((props, ref) => ); +export default ForwardRef;" +`; + +exports[`plugin javascript with "ref" option adds ForwardRef component 1`] = ` +"import * as React from \\"react\\"; + +function SvgComponent({ + svgRef +}) { + return ; +} + +const ForwardRef = React.forwardRef((props, ref) => ); +export default ForwardRef;" +`; + +exports[`plugin javascript with "titleProp" adds "titleProp" and "titleId" prop 1`] = ` +"import * as React from \\"react\\"; + +function SvgComponent({ + title, + titleId +}) { + return ; +} + +export default SvgComponent;" +`; + +exports[`plugin javascript with both "memo" and "ref" option wrap component in "React.memo" and "React.forwardRef" 1`] = ` +"import * as React from \\"react\\"; + +function SvgComponent({ + svgRef +}) { + return ; +} + +const MemoSvgComponent = React.memo(SvgComponent); +const ForwardRef = React.forwardRef((props, ref) => ); +export default ForwardRef;" +`; + +exports[`plugin typescript custom templates support basic template 1`] = ` +"import * as React from 'react'; + +const MyComponent = () => ; + +export default MyComponent;" +`; + +exports[`plugin typescript custom templates supports TypeScript template 1`] = ` +"import * as React from 'react'; + +const MyComponent = (props: React.SVGProps) => ; + +export default MyComponent;" +`; + +exports[`plugin typescript custom templates supports template that does not return an array 1`] = `";"`; + +exports[`plugin typescript transforms whole program 1`] = ` +"import * as React from \\"react\\"; + +function SvgComponent() { + return ; +} + +export default SvgComponent;" +`; + +exports[`plugin typescript with "expandProps" add props 1`] = ` +"import * as React from \\"react\\"; + +function SvgComponent(props: React.SVGProps) { + return ; +} + +export default SvgComponent;" +`; + +exports[`plugin typescript with "memo" option wrap component in "React.memo" 1`] = ` +"import * as React from \\"react\\"; + +function SvgComponent() { + return ; +} + +const MemoSvgComponent = React.memo(SvgComponent); +export default MemoSvgComponent;" +`; + +exports[`plugin typescript with "native" option adds import from "react-native-svg" 1`] = ` +"import * as React from \\"react\\"; +import Svg from \\"react-native-svg\\"; + +function SvgComponent() { + return ; +} + +export default SvgComponent;" +`; + +exports[`plugin typescript with "native.expo" option adds import from "react-native-svg" & from "expo" 1`] = ` +"import * as React from \\"react\\"; +import \\"expo\\"; + +function SvgComponent() { + return ; +} + +export default SvgComponent;" +`; + +exports[`plugin typescript with "ref" and "expandProps" option expands props 1`] = ` +"import * as React from \\"react\\"; +interface SVGRProps { + svgRef?: React.Ref +} + +function SvgComponent({ + svgRef, + ...props +}: React.SVGProps & SVGRProps) { + return ; +} + +const ForwardRef = React.forwardRef((props, ref) => ); +export default ForwardRef;" +`; + +exports[`plugin typescript with "ref" option adds ForwardRef component 1`] = ` +"import * as React from \\"react\\"; +interface SVGRProps { + svgRef?: React.Ref +} + +function SvgComponent({ + svgRef +}: SVGRProps) { + return ; +} + +const ForwardRef = React.forwardRef((props, ref) => ); +export default ForwardRef;" +`; + +exports[`plugin typescript with "titleProp" adds "titleProp" and "titleId" prop 1`] = ` +"import * as React from \\"react\\"; +interface SVGRProps { + title?: String, + titleId?: String, +} + +function SvgComponent({ + title, + titleId +}: SVGRProps) { + return ; +} + +export default SvgComponent;" +`; + +exports[`plugin typescript with both "memo" and "ref" option wrap component in "React.memo" and "React.forwardRef" 1`] = ` +"import * as React from \\"react\\"; +interface SVGRProps { + svgRef?: React.Ref +} + +function SvgComponent({ + svgRef +}: SVGRProps) { + return ; +} + +const MemoSvgComponent = React.memo(SvgComponent); +const ForwardRef = React.forwardRef((props, ref) => ); +export default ForwardRef;" +`; diff --git a/packages/babel-plugin-transform-svg-component/src/index.js b/packages/babel-plugin-transform-svg-component/src/index.js index 408b9cc3..7603a6ac 100644 --- a/packages/babel-plugin-transform-svg-component/src/index.js +++ b/packages/babel-plugin-transform-svg-component/src/index.js @@ -1,16 +1,24 @@ -import { getProps, getImport, getExport } from './util' +import { getProps, getImport, getExport, getInterface } from './util' function defaultTemplate( { template }, opts, - { imports, componentName, props, jsx, exports }, + { imports, interfaces, componentName, props, jsx, exports }, ) { - return template.ast`${imports} + const plugins = ['jsx'] + if (opts.typescript) { + plugins.push('typescript') + } + const typeScriptTpl = template.smart({ plugins }) + return typeScriptTpl.ast`${imports} + +${interfaces} + function ${componentName}(${props}) { return ${jsx}; } ${exports} -` + ` } const plugin = (api, opts) => ({ @@ -20,6 +28,7 @@ const plugin = (api, opts) => ({ const template = opts.template || defaultTemplate const body = template(api, opts, { componentName: t.identifier(opts.state.componentName), + interfaces: getInterface(api, opts), props: getProps(api, opts), imports: getImport(api, opts), exports: getExport(api, opts), diff --git a/packages/babel-plugin-transform-svg-component/src/index.test.js b/packages/babel-plugin-transform-svg-component/src/index.test.js index e9f126a2..311007eb 100644 --- a/packages/babel-plugin-transform-svg-component/src/index.test.js +++ b/packages/babel-plugin-transform-svg-component/src/index.test.js @@ -1,9 +1,12 @@ import { transform } from '@babel/core' import plugin from '.' -const testPlugin = (code, options) => { +const testPlugin = language => (code, options) => { const result = transform(code, { - plugins: ['@babel/plugin-syntax-jsx', [plugin, options]], + plugins: [ + '@babel/plugin-syntax-jsx', + [plugin, { ...options, typescript: language === 'typescript' }], + ], configFile: false, }) @@ -11,159 +14,150 @@ const testPlugin = (code, options) => { } describe('plugin', () => { - it('should transform whole program', () => { - const { code } = testPlugin('
', { - state: { componentName: 'SvgComponent' }, + describe.each(['javascript', 'typescript'])('%s', language => { + it('transforms whole program', () => { + const { code } = testPlugin(language)('', { + state: { componentName: 'SvgComponent' }, + }) + expect(code).toMatchSnapshot() }) - expect(code).toMatchInlineSnapshot(` - "import * as React from \\"react\\"; - function SvgComponent() { - return
; - } - - export default SvgComponent;" - `) - }) - - it('should add import for react-native-svg', () => { - const { code } = testPlugin('
', { - state: { componentName: 'SvgComponent' }, - native: true, + describe('with "native" option', () => { + it('adds import from "react-native-svg"', () => { + const { code } = testPlugin(language)('', { + state: { componentName: 'SvgComponent' }, + native: true, + }) + expect(code).toMatchSnapshot() + }) }) - expect(code).toMatchInlineSnapshot(` - "import * as React from \\"react\\"; - import Svg from \\"react-native-svg\\"; - - function SvgComponent() { - return
; - } - - export default SvgComponent;" - `) - }) - it('should import for expo', () => { - const { code } = testPlugin('
', { - state: { componentName: 'SvgComponent' }, - native: { expo: true }, + describe('with "native.expo" option', () => { + it('adds import from "react-native-svg" & from "expo"', () => { + const { code } = testPlugin(language)('', { + state: { componentName: 'SvgComponent' }, + native: { expo: true }, + }) + expect(code).toMatchSnapshot() + }) }) - expect(code).toMatchInlineSnapshot(` - "import * as React from \\"react\\"; - import \\"expo\\"; - function SvgComponent() { - return
; - } - - export default SvgComponent;" - `) - }) - - it('should support custom template', () => { - const { code } = testPlugin('
', { - template: ( - { template }, - opts, - { jsx }, - ) => template.ast`import * as React from 'react'; - const MyComponent = () => ${jsx} - export default MyComponent -`, - state: { componentName: 'SvgComponent' }, + describe('with "ref" option', () => { + it('adds ForwardRef component', () => { + const { code } = testPlugin(language)('', { + state: { componentName: 'SvgComponent' }, + ref: true, + }) + expect(code).toMatchSnapshot() + }) }) - expect(code).toMatchInlineSnapshot(` - "import * as React from 'react'; - - const MyComponent = () =>
; - - export default MyComponent;" - `) - }) - it('should support custom typescript template', () => { - const { code } = testPlugin('
', { - template: ({ template }, opts, { jsx }) => { - const typescriptTemplate = template.smart({ plugins: ['typescript'] }) - return typescriptTemplate.ast` - import * as React from 'react'; - const MyComponent = (props: React.SVGProps) => ${jsx}; - export default MyComponent; - ` - }, - state: { componentName: 'SvgComponent' }, + describe('with "titleProp"', () => { + it('adds "titleProp" and "titleId" prop', () => { + const { code } = testPlugin(language)('', { + state: { componentName: 'SvgComponent' }, + titleProp: true, + }) + expect(code).toMatchSnapshot() + }) }) - expect(code).toMatchInlineSnapshot(` - "import * as React from 'react'; - const MyComponent = (props: React.SVGProps) =>
; - - export default MyComponent;" - `) - }) - - it('should handle template that does not return an array', () => { - const { code } = testPlugin('
', { - template: ({ template }, opts, { jsx }) => template.ast`${jsx}`, - state: { componentName: 'SvgComponent' }, + describe('with "expandProps"', () => { + it('add props', () => { + const { code } = testPlugin(language)('', { + state: { componentName: 'SvgComponent' }, + expandProps: true, + }) + expect(code).toMatchSnapshot() + }) }) - expect(code).toMatchInlineSnapshot(`"
;"`) - }) - it('should work with ref', () => { - const { code } = testPlugin('
', { - state: { componentName: 'SvgComponent' }, - ref: true, + describe('with "ref" and "expandProps" option', () => { + it('expands props', () => { + const { code } = testPlugin(language)('', { + state: { componentName: 'SvgComponent' }, + expandProps: true, + ref: true, + }) + expect(code).toMatchSnapshot() + }) }) - expect(code).toMatchInlineSnapshot(` - "import * as React from \\"react\\"; - - function SvgComponent({ - svgRef - }) { - return
; - } - - const ForwardRef = React.forwardRef((props, ref) => ); - export default ForwardRef;" - `) - }) - it('should work with memo', () => { - const { code } = testPlugin('
', { - state: { componentName: 'SvgComponent' }, - memo: true, + describe('with "memo" option', () => { + it('wrap component in "React.memo"', () => { + const { code } = testPlugin(language)('', { + state: { componentName: 'SvgComponent' }, + memo: true, + }) + expect(code).toMatchSnapshot() + }) }) - expect(code).toMatchInlineSnapshot(` - "import * as React from \\"react\\"; - - function SvgComponent() { - return
; - } - - const MemoSvgComponent = React.memo(SvgComponent); - export default MemoSvgComponent;" - `) - }) - it('should work with memo + ref', () => { - const { code } = testPlugin('
', { - state: { componentName: 'SvgComponent' }, - memo: true, - ref: true, + describe('with both "memo" and "ref" option', () => { + it('wrap component in "React.memo" and "React.forwardRef"', () => { + const { code } = testPlugin(language)('', { + state: { componentName: 'SvgComponent' }, + memo: true, + ref: true, + }) + expect(code).toMatchSnapshot() + }) }) - expect(code).toMatchInlineSnapshot(` - "import * as React from \\"react\\"; - - function SvgComponent({ - svgRef - }) { - return
; - } - const MemoSvgComponent = React.memo(SvgComponent); - const ForwardRef = React.forwardRef((props, ref) => ); - export default ForwardRef;" - `) + describe('custom templates', () => { + it('support basic template', () => { + const { code } = testPlugin(language)('', { + template: ( + { template }, + opts, + { jsx }, + ) => template.ast`import * as React from 'react'; + const MyComponent = () => ${jsx} + export default MyComponent + `, + state: { componentName: 'SvgComponent' }, + }) + expect(code).toMatchSnapshot() + }) + + describe('it supports JSX template', () => { + const { code } = testPlugin(language)('', { + template: ({ template }, opts, { jsx }) => { + const jsxTemplate = template.smart({ plugins: ['jsx'] }) + return jsxTemplate.ast`import * as React from 'react'; + const MyComponent = () =>
{${jsx}}
+ export default MyComponent + ` + }, + state: { componentName: 'SvgComponent' }, + }) + expect(code).toMatchSnapshot() + }) + + it('supports TypeScript template', () => { + const { code } = testPlugin(language)('', { + template: ({ template }, opts, { jsx }) => { + const typescriptTemplate = template.smart({ + plugins: ['typescript'], + }) + return typescriptTemplate.ast` + import * as React from 'react'; + const MyComponent = (props: React.SVGProps) => ${jsx}; + export default MyComponent; + ` + }, + state: { componentName: 'SvgComponent' }, + }) + expect(code).toMatchSnapshot() + }) + + it('supports template that does not return an array', () => { + const { code } = testPlugin(language)('', { + template: ({ template }, opts, { jsx }) => template.ast`${jsx}`, + state: { componentName: 'SvgComponent' }, + }) + expect(code).toMatchSnapshot() + }) + }) }) }) diff --git a/packages/babel-plugin-transform-svg-component/src/util.js b/packages/babel-plugin-transform-svg-component/src/util.js index 6a69a556..375bc333 100644 --- a/packages/babel-plugin-transform-svg-component/src/util.js +++ b/packages/babel-plugin-transform-svg-component/src/util.js @@ -1,3 +1,74 @@ +function typeAnnotation(typeAnnotation) { + return { + type: 'TypeAnnotation', + typeAnnotation, + } +} + +function genericTypeAnnotation(id, typeParameters = null) { + return { type: 'GenericTypeAnnotation', id, typeParameters } +} + +function typeParameters(params) { + return { + type: 'TypeParameterInstantiation', + params, + } +} + +function qualifiedTypeIdentifier(qualification, id) { + return { type: 'QualifiedTypeIdentifier', qualification, id } +} + +function intersectionTypeAnnotation(types) { + return { type: 'IntersectionTypeAnnotation', types } +} + +function interfaceDeclaration(id, body) { + return { + type: 'InterfaceDeclaration', + id, + typeParameters: null, + extends: [], + implements: [], + mixins: [], + body, + } +} + +function objectTypeAnnotation(properties) { + return { + type: 'ObjectTypeAnnotation', + properties, + } +} + +function objectTypeProperty(key, value, optional = false) { + return { + type: 'ObjectTypeProperty', + key, + static: false, + proto: false, + kind: 'init', + method: false, + value, + variance: null, + optional, + } +} + +function addTypeAnotation(obj, typeAnnotation, opts) { + if (!opts.typescript) return obj + return { ...obj, typeAnnotation } +} + +function getSvgPropsTypeAnnotation(t) { + return genericTypeAnnotation( + qualifiedTypeIdentifier(t.identifier('React'), t.identifier('SVGProps')), + typeParameters([genericTypeAnnotation(t.identifier('SVGSVGElement'))]), + ) +} + export const getProps = ({ types: t }, opts) => { const props = [] @@ -41,10 +112,57 @@ export const getProps = ({ types: t }, opts) => { } if (props.length === 1 && opts.expandProps) { - return t.identifier('props') + return addTypeAnotation( + t.identifier('props'), + typeAnnotation(getSvgPropsTypeAnnotation(t)), + opts, + ) } - return t.objectPattern(props) + return addTypeAnotation( + t.objectPattern(props), + typeAnnotation( + opts.expandProps + ? intersectionTypeAnnotation([ + getSvgPropsTypeAnnotation(t), + genericTypeAnnotation(t.identifier('SVGRProps')), + ]) + : genericTypeAnnotation(t.identifier('SVGRProps')), + ), + opts, + ) +} + +export const getInterface = ({ types: t }, opts) => { + if (!opts.typescript) return null + const properties = [] + if (opts.ref) { + properties.push( + objectTypeProperty( + t.identifier('svgRef'), + genericTypeAnnotation( + qualifiedTypeIdentifier(t.identifier('React'), t.identifier('Ref')), + typeParameters([ + genericTypeAnnotation(t.identifier('SVGSVGElement')), + ]), + ), + true, + ), + ) + } + if (opts.titleProp) { + properties.push( + objectTypeProperty(t.identifier('title'), t.identifier('String'), true), + ) + properties.push( + objectTypeProperty(t.identifier('titleId'), t.identifier('String'), true), + ) + } + if (properties.length === 0) return null + return interfaceDeclaration( + t.identifier('SVGRProps'), + objectTypeAnnotation(properties), + ) } export const getImport = ({ types: t }, opts) => { diff --git a/packages/cli/src/__snapshots__/index.test.js.snap b/packages/cli/src/__snapshots__/index.test.js.snap index 12e44574..ef5408c7 100644 --- a/packages/cli/src/__snapshots__/index.test.js.snap +++ b/packages/cli/src/__snapshots__/index.test.js.snap @@ -404,25 +404,85 @@ export default SvgFile " `; +exports[`cli should support various args: --typescript --ref 1`] = ` +"import * as React from 'react' +interface SVGRProps { + svgRef?: React.Ref; +} + +function SvgFile({ + svgRef, + ...props +}: React.SVGProps & SVGRProps) { + return ( + + + + ) +} + +const ForwardRef = React.forwardRef((props, ref) => ( + +)) +export default ForwardRef + +" +`; + +exports[`cli should support various args: --typescript 1`] = ` +"import * as React from 'react' + +function SvgFile(props: React.SVGProps) { + return ( + + + + ) +} + +export default SvgFile + +" +`; + exports[`cli should suppress output when transforming a directory with a --silent option 1`] = `""`; exports[`cli should transform a whole directory and output relative destination paths 1`] = ` -" -__fixtures__/cased/PascalCase.svg -> __fixtures_build__/whole/cased/PascalCase.js -__fixtures__/cased/camelCase.svg -> __fixtures_build__/whole/cased/CamelCase.js -__fixtures__/cased/kebab-case.svg -> __fixtures_build__/whole/cased/KebabCase.js -__fixtures__/cased/multiple---dashes.svg -> __fixtures_build__/whole/cased/MultipleDashes.js -__fixtures__/complex/skype.svg -> __fixtures_build__/whole/complex/Skype.js -__fixtures__/complex/telegram.svg -> __fixtures_build__/whole/complex/Telegram.js -__fixtures__/nesting/a/c/three.svg -> __fixtures_build__/whole/nesting/a/c/Three.js -__fixtures__/nesting/a/two.svg -> __fixtures_build__/whole/nesting/a/Two.js -__fixtures__/nesting/one.svg -> __fixtures_build__/whole/nesting/One.js -__fixtures__/numeric/2.file.svg -> __fixtures_build__/whole/numeric/2File.js -__fixtures__/numeric/file.svg -> __fixtures_build__/whole/numeric/File.js -__fixtures__/simple/file.svg -> __fixtures_build__/whole/simple/File.js -__fixtures__/withPrettierRc/file.svg -> __fixtures_build__/whole/withPrettierRc/File.js -__fixtures__/withSvgoYml/file.svg -> __fixtures_build__/whole/withSvgoYml/File.js -__fixtures__/withSvgrRc/file.svg -> __fixtures_build__/whole/withSvgrRc/File.js" +" +__fixtures__/cased/camelCase.svg -> __fixtures_build__/whole/cased/CamelCase.js +__fixtures__/cased/kebab-case.svg -> __fixtures_build__/whole/cased/KebabCase.js +__fixtures__/cased/multiple---dashes.svg -> __fixtures_build__/whole/cased/MultipleDashes.js +__fixtures__/complex/skype.svg -> __fixtures_build__/whole/complex/Skype.js +__fixtures__/complex/telegram.svg -> __fixtures_build__/whole/complex/Telegram.js +__fixtures__/nesting/a/c/three.svg -> __fixtures_build__/whole/nesting/a/c/Three.js +__fixtures__/nesting/a/two.svg -> __fixtures_build__/whole/nesting/a/Two.js +__fixtures__/nesting/one.svg -> __fixtures_build__/whole/nesting/One.js +__fixtures__/numeric/2.file.svg -> __fixtures_build__/whole/numeric/2File.js +__fixtures__/numeric/file.svg -> __fixtures_build__/whole/numeric/File.js +__fixtures__/simple/file.svg -> __fixtures_build__/whole/simple/File.js +__fixtures__/withPrettierRc/file.svg -> __fixtures_build__/whole/withPrettierRc/File.js +__fixtures__/withSvgoYml/file.svg -> __fixtures_build__/whole/withSvgoYml/File.js +__fixtures__/withSvgrRc/file.svg -> __fixtures_build__/whole/withSvgrRc/File.js +__fixtures__/cased/PascalCase.svg -> __fixtures_build__/whole/cased/PascalCase.js" +`; + +exports[`cli should transform a whole directory with --typescript 1`] = ` +" +__fixtures__/cased/camelCase.svg -> __fixtures_build__/whole/cased/CamelCase.tsx +__fixtures__/cased/kebab-case.svg -> __fixtures_build__/whole/cased/KebabCase.tsx +__fixtures__/cased/multiple---dashes.svg -> __fixtures_build__/whole/cased/MultipleDashes.tsx +__fixtures__/complex/skype.svg -> __fixtures_build__/whole/complex/Skype.tsx +__fixtures__/complex/telegram.svg -> __fixtures_build__/whole/complex/Telegram.tsx +__fixtures__/nesting/a/c/three.svg -> __fixtures_build__/whole/nesting/a/c/Three.tsx +__fixtures__/nesting/a/two.svg -> __fixtures_build__/whole/nesting/a/Two.tsx +__fixtures__/nesting/one.svg -> __fixtures_build__/whole/nesting/One.tsx +__fixtures__/numeric/2.file.svg -> __fixtures_build__/whole/numeric/2File.tsx +__fixtures__/numeric/file.svg -> __fixtures_build__/whole/numeric/File.tsx +__fixtures__/simple/file.svg -> __fixtures_build__/whole/simple/File.tsx +__fixtures__/withPrettierRc/file.svg -> __fixtures_build__/whole/withPrettierRc/File.tsx +__fixtures__/withSvgoYml/file.svg -> __fixtures_build__/whole/withSvgoYml/File.tsx +__fixtures__/withSvgrRc/file.svg -> __fixtures_build__/whole/withSvgrRc/File.tsx +__fixtures__/cased/PascalCase.svg -> __fixtures_build__/whole/cased/PascalCase.tsx" `; exports[`cli should work with a simple file 1`] = ` diff --git a/packages/cli/src/dirCommand.js b/packages/cli/src/dirCommand.js index 2219ec4f..de2d631e 100644 --- a/packages/cli/src/dirCommand.js +++ b/packages/cli/src/dirCommand.js @@ -44,14 +44,19 @@ function defaultIndexTemplate(files) { return exportEntries.join('\n') } +function getDefaultExtension(options) { + return options.typescript ? 'tsx' : 'js' +} + export default async function dirCommand( program, filenames, - { ext = 'js', filenameCase = CASE.PASCAL, ...options }, + { ext, filenameCase = CASE.PASCAL, ...options }, ) { async function write(src, dest) { if (!isCompilable(src)) return null + ext = ext || getDefaultExtension(options) dest = rename(dest, ext, filenameCase) const code = await convertFile(src, options) const cwdRelative = path.relative(process.cwd(), dest) diff --git a/packages/cli/src/index.js b/packages/cli/src/index.js index ffda4b9c..9d8b53b0 100644 --- a/packages/cli/src/index.js +++ b/packages/cli/src/index.js @@ -55,6 +55,7 @@ program 'specify filename case ("pascal", "kebab", "camel") (default: "pascal")', ) .option('--icon', 'use "1em" as width and height') + .option('--typescript', 'transform svg into typescript') .option('--native', 'add react-native support with react-native-svg') .option('--memo', 'add React.memo into the result component') .option('--ref', 'forward ref to SVG root element') diff --git a/packages/cli/src/index.test.js b/packages/cli/src/index.test.js index d75fe367..466c6a3a 100644 --- a/packages/cli/src/index.test.js +++ b/packages/cli/src/index.test.js @@ -64,6 +64,17 @@ describe('cli', () => { expect(sorted).toMatchSnapshot() }, 10000) + it('should transform a whole directory with --typescript', async () => { + const result = await cli( + '--typescript --out-dir __fixtures_build__/whole __fixtures__', + ) + const sorted = result + .split(/\n/) + .sort() + .join('\n') + expect(sorted).toMatchSnapshot() + }, 10000) + it('should suppress output when transforming a directory with a --silent option', async () => { const result = await cli( '--silent --out-dir __fixtures_build__/whole __fixtures__', @@ -118,6 +129,8 @@ describe('cli', () => { ['--no-svgo'], ['--no-prettier'], ['--title-prop'], + ['--typescript'], + ['--typescript --ref'], ])( 'should support various args', async args => { diff --git a/packages/core/src/config.js b/packages/core/src/config.js index 0c4b45ed..58429389 100644 --- a/packages/core/src/config.js +++ b/packages/core/src/config.js @@ -6,6 +6,7 @@ export const DEFAULT_CONFIG = { expandProps: 'end', icon: false, native: false, + typescript: false, prettier: true, prettierConfig: null, memo: false, diff --git a/website/src/pages/docs/custom-templates.mdx b/website/src/pages/docs/custom-templates.mdx new file mode 100644 index 00000000..20d43098 --- /dev/null +++ b/website/src/pages/docs/custom-templates.mdx @@ -0,0 +1,93 @@ +--- +menu: Advanced +title: Custom Templates +order: 6 +--- + +# Custom Templates + +Custom templates give you the opportunity to personalize the final generated component by SVGR. In most of case you don't need it, only advanced use-cases require templates. + +## Create a custom template + +A custom template takes place in a file that exports a "template function". + +This function is called in a babel plugin: `babel-plugin-transform-svg-component` and must returns a Babel AST. If you are not familiar with all Babel stuff, you should read [this guide](https://github.com/jamiebuilds/babel-handbook). + +- `api`: The API object returned by Babel +- `opts`: Options passed to `babel-plugin-transform-svg-component` +- `astParts`: All pre-compiled parts by SVGR + - `componentName`: The component name + - `props`: The properties + - `interfaces`: All necessary interfaces (typescript) + - `imports`: All necessary imports + - `exports`: The export of the component + - `jsx`: The JSX part of the component + +The following template is the default template used by SVGR. It is a good idea to start with it: + +```js +function defaultTemplate( + { template }, + opts, + { imports, componentName, props, jsx, exports }, +) { + return template.ast`${imports} +function ${componentName}(${props}) { + return ${jsx}; +} +${exports} +` +} + +module.exports = defaultTemplate +``` + +As you can see, we use [the `template.ast` helper of Babel](https://babeljs.io/docs/en/babel-template#template). This function have to return an AST. + +Let's try something very simple. You want to add some PropTypes to your component: + +```js +function propTypesTemplate( + { template }, + opts, + { imports, componentName, props, jsx, exports }, +) { + return template.ast`${imports} +import PropTypes from 'prop-types'; + +function ${componentName}(${props}) { + return ${jsx}; +} + +${componentName}.propTypes = { + title: PropTypes.string, +}; + +${exports} +` +} + +module.exports = propTypesTemplate +``` + +As you can see it is very natural, we just add code and use AST parts in the template. + +## Use template with CLI + +You can use this template in the CLI, along the `--ext` argument: + +```sh +$ npx @svgr/cli --template my/template.js --ext .ts my-icon.svg +``` + +## Use template in config + +Specify `.svgrrc.js`: + +```js +// .svgrrc.js +module.exports = { + template: require('./my-template'), +} +``` diff --git a/website/src/pages/docs/options.mdx b/website/src/pages/docs/options.mdx index 1b7755c6..1f76dff2 100644 --- a/website/src/pages/docs/options.mdx +++ b/website/src/pages/docs/options.mdx @@ -53,6 +53,14 @@ Override using the API with `native: { expo: true }` to template SVG nodes with | ------- | ------------ | -------------------------------------------- | | `false` | `--native` | `native: ` or `native: { expo: true }` | +## TypeScript + +Generates `.tsx` files with [TypeScript](https://www.typescriptlang.org/) typings. + +| Default | CLI Override | API Override | +| ------- | -------------- | -------------------- | +| `false` | `--typescript` | `typescript: ` | + ## Dimensions Remove width and height from root SVG tag. diff --git a/website/src/pages/docs/typescript.mdx b/website/src/pages/docs/typescript.mdx deleted file mode 100644 index c8ba4e93..00000000 --- a/website/src/pages/docs/typescript.mdx +++ /dev/null @@ -1,57 +0,0 @@ ---- -menu: Advanced -title: TypeScript -order: 5 ---- - -# TypeScript - -Every languages supported by Babel are also supported by SVGR including TypeScript and Flow. - -## Create a custom template - -To target TypeScript, you need to create a custom template: - -```js -function template( - { template }, - opts, - { imports, componentName, props, jsx, exports } -) { - const typeScriptTpl = template.smart({ plugins: ['typescript'] }) - return typeScriptTpl.ast` - import * as React from 'react'; - const ${componentName} = (props: React.SVGProps) => ${jsx}; - export default ${componentName}; - ` -} - -module.exports = template -``` - -You can use this template in the CLI, along the `--ext` argument: - -```sh -$ npx @svgr/cli --template path/to/template.js --ext .ts my-icon.svg -``` - -Or specify it in your `.svgrrc.js`: - -```js -module.exports = { - template( - { template }, - opts, - { imports, componentName, props, jsx, exports } - ) { - const typeScriptTpl = template.smart({ plugins: ['typescript'] }) - return typeScriptTpl.ast` - import * as React from 'react'; - const ${componentName} = (props: React.SVGProps) => ${jsx}; - export default ${componentName}; - ` - }, -} -``` - -> To target flow, follow the same steps and replace `plugins: ['typescript']` by `plugins: ['flow']`