From 61c30bda2cd1f5d79005f48d21f2a066d22ef912 Mon Sep 17 00:00:00 2001 From: Ben Saufley Date: Tue, 21 Nov 2023 16:17:57 -0500 Subject: [PATCH 1/5] Add transformAttributes option to @svgr/core and hast-util-to-babel-ast Allows disabling of this feature for Preact users --- packages/core/src/config.ts | 1 + .../src/__snapshots__/index.test.ts.snap | 2 ++ .../src/configuration.ts | 15 ++++++++++++ .../src/getAttributes.ts | 8 ++++--- .../hast-util-to-babel-ast/src/handlers.ts | 4 ++-- .../hast-util-to-babel-ast/src/helpers.ts | 3 ++- .../hast-util-to-babel-ast/src/index.test.ts | 24 +++++++++++++++++-- packages/hast-util-to-babel-ast/src/index.ts | 6 ++++- packages/plugin-jsx/src/index.ts | 4 +++- 9 files changed, 57 insertions(+), 10 deletions(-) create mode 100644 packages/hast-util-to-babel-ast/src/configuration.ts diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 9cbba1ef..9292f1b7 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -46,6 +46,7 @@ export interface Config { // JSX jsx?: { babelConfig?: BabelTransformOptions + transformAttributes?: boolean } } diff --git a/packages/hast-util-to-babel-ast/src/__snapshots__/index.test.ts.snap b/packages/hast-util-to-babel-ast/src/__snapshots__/index.test.ts.snap index a019a3ef..c3d01abf 100644 --- a/packages/hast-util-to-babel-ast/src/__snapshots__/index.test.ts.snap +++ b/packages/hast-util-to-babel-ast/src/__snapshots__/index.test.ts.snap @@ -3,3 +3,5 @@ exports[`hast-util-to-babel-ast should handle spaces and tab 1`] = `";"`; exports[`hast-util-to-babel-ast transforms SVG 1`] = `"{"Dismiss"}{"Created with Sketch."};"`; + +exports[`hast-util-to-babel-ast transforms SVG without transforming attributes 1`] = `"{"Dismiss"}{"Created with Sketch."};"`; diff --git a/packages/hast-util-to-babel-ast/src/configuration.ts b/packages/hast-util-to-babel-ast/src/configuration.ts new file mode 100644 index 00000000..08457e7e --- /dev/null +++ b/packages/hast-util-to-babel-ast/src/configuration.ts @@ -0,0 +1,15 @@ +export interface Configuration { + transformAttributes: boolean +} + +export const getConfig = ( + config: Partial = {}, + key: K, +): Configuration[K] => { + switch (key) { + case 'transformAttributes': + return config.transformAttributes ?? true + default: + throw new Error(`Unknown option ${key}`) + } +} diff --git a/packages/hast-util-to-babel-ast/src/getAttributes.ts b/packages/hast-util-to-babel-ast/src/getAttributes.ts index 33b0f980..c2d2b232 100644 --- a/packages/hast-util-to-babel-ast/src/getAttributes.ts +++ b/packages/hast-util-to-babel-ast/src/getAttributes.ts @@ -9,7 +9,9 @@ const convertAriaAttribute = (kebabKey: string) => { return `${aria}-${parts.join('').toLowerCase()}` } -const getKey = (key: string, node: ElementNode) => { +const getKey = (key: string, node: ElementNode, transformAttributes: boolean) => { + if (!transformAttributes) return t.jsxIdentifier(key); + const lowerCaseKey = key.toLowerCase() const mappedElementAttribute = // @ts-ignore @@ -53,7 +55,7 @@ const getValue = (key: string, value: string[] | string | number) => { return t.stringLiteral(replaceSpaces(value)) } -export const getAttributes = (node: ElementNode): t.JSXAttribute[] => { +export const getAttributes = (node: ElementNode, transformAttributes: boolean): t.JSXAttribute[] => { if (!node.properties) return [] const keys = Object.keys(node.properties) const attributes = [] @@ -62,7 +64,7 @@ export const getAttributes = (node: ElementNode): t.JSXAttribute[] => { while (++index < keys.length) { const key = keys[index] const value = node.properties[key] - const attribute = t.jsxAttribute(getKey(key, node), getValue(key, value)) + const attribute = t.jsxAttribute(getKey(key, node, transformAttributes), getValue(key, value)) attributes.push(attribute) } diff --git a/packages/hast-util-to-babel-ast/src/handlers.ts b/packages/hast-util-to-babel-ast/src/handlers.ts index 43cf7e3d..cdb56a4c 100644 --- a/packages/hast-util-to-babel-ast/src/handlers.ts +++ b/packages/hast-util-to-babel-ast/src/handlers.ts @@ -5,6 +5,7 @@ import { getAttributes } from './getAttributes' import { ELEMENT_TAG_NAME_MAPPING } from './mappings' import type { RootNode, ElementNode, TextNode } from 'svg-parser' import type { Helpers } from './helpers' +import { getConfig } from './configuration' export const root = (h: Helpers, node: RootNode): t.Program => // @ts-ignore @@ -44,7 +45,6 @@ export const element = ( parent: RootNode | ElementNode, ): t.JSXElement | t.ExpressionStatement | null => { if (!node.tagName) return null - const children = all(h, node) const selfClosing = children.length === 0 @@ -52,7 +52,7 @@ export const element = ( const openingElement = t.jsxOpeningElement( t.jsxIdentifier(name), - getAttributes(node), + getAttributes(node, getConfig(h.config, 'transformAttributes')), selfClosing, ) diff --git a/packages/hast-util-to-babel-ast/src/helpers.ts b/packages/hast-util-to-babel-ast/src/helpers.ts index f5d6015b..71124657 100644 --- a/packages/hast-util-to-babel-ast/src/helpers.ts +++ b/packages/hast-util-to-babel-ast/src/helpers.ts @@ -1,5 +1,6 @@ +import type { Configuration } from './configuration' import * as handlers from './handlers' -export const helpers = { handlers } +export const helpers = { handlers, config: {} as Partial } export type Helpers = typeof helpers diff --git a/packages/hast-util-to-babel-ast/src/index.test.ts b/packages/hast-util-to-babel-ast/src/index.test.ts index 972c7e5d..68976e2f 100644 --- a/packages/hast-util-to-babel-ast/src/index.test.ts +++ b/packages/hast-util-to-babel-ast/src/index.test.ts @@ -1,11 +1,12 @@ import { parse } from 'svg-parser' import generate from '@babel/generator' import hastToBabelAst from './index' +import type { Configuration } from './configuration' -function transform(code: string) { +function transform(code: string, config: Partial = {}) { const hastTree = parse(code) - const babelTree = hastToBabelAst(hastTree) + const babelTree = hastToBabelAst(hastTree, config) const { code: generatedCode } = generate(babelTree) @@ -32,6 +33,25 @@ describe('hast-util-to-babel-ast', () => { expect(transform(code)).toMatchSnapshot() }) + it('transforms SVG without transforming attributes', () => { + const code = ` + + + + Dismiss + Created with Sketch. + + + + + + + + +` + expect(transform(code, { transformAttributes: false })).toMatchSnapshot() + }) + it('transforms "aria-x"', () => { const code = `` expect(transform(code)).toMatchInlineSnapshot( diff --git a/packages/hast-util-to-babel-ast/src/index.ts b/packages/hast-util-to-babel-ast/src/index.ts index f20914b9..1d3c0655 100644 --- a/packages/hast-util-to-babel-ast/src/index.ts +++ b/packages/hast-util-to-babel-ast/src/index.ts @@ -2,7 +2,11 @@ import type { RootNode } from 'svg-parser' import type * as t from '@babel/types' import { root } from './handlers' import { helpers } from './helpers' +import type { Configuration } from './configuration' -const toBabelAST = (tree: RootNode): t.Program => root(helpers, tree) +const toBabelAST = ( + tree: RootNode, + config: Partial = {}, +): t.Program => root({ ...helpers, config }, tree) export default toBabelAST diff --git a/packages/plugin-jsx/src/index.ts b/packages/plugin-jsx/src/index.ts index d05faca5..1ce829b9 100644 --- a/packages/plugin-jsx/src/index.ts +++ b/packages/plugin-jsx/src/index.ts @@ -39,7 +39,9 @@ const jsxPlugin: Plugin = (code, config, state) => { const filePath = state.filePath || 'unknown' const hastTree = parse(code) - const babelTree = hastToBabelAst(hastTree) + const babelTree = hastToBabelAst(hastTree, { + transformAttributes: config.jsx?.transformAttributes ?? true, + }) const svgPresetOptions: SvgrPresetOptions = { ref: config.ref, From a7d27a203985c00bb3db59c3d241c16a3a03345b Mon Sep 17 00:00:00 2001 From: Ben Saufley Date: Tue, 21 Nov 2023 16:54:28 -0500 Subject: [PATCH 2/5] Add to README --- packages/plugin-jsx/README.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/plugin-jsx/README.md b/packages/plugin-jsx/README.md index 40df5e14..dae11f8c 100644 --- a/packages/plugin-jsx/README.md +++ b/packages/plugin-jsx/README.md @@ -30,6 +30,21 @@ npm install --save-dev @svgr/plugin-jsx - Converting the [HAST](https://github.com/syntax-tree/hast) into a [Babel AST](https://github.com/babel/babel/blob/master/packages/babel-parser/ast/spec.md) - Applying [`@svgr/babel-preset`](../babel-preset/README.md) transformations +## Skipping attribute transformations + +For non-React implementations such as Preact, you can pass `false` to `jsx.transformAttributes` +and attributes will remain in their original form. If `jsx.transformAttributes` is `true` (the +default value), attributes will be transformed to their React equivalents—usually camelCase. +```js +// .svgrrc.js + +module.exports = { + jsx: { + transformAttributes: false, + }, +} +``` + ## Applying custom transformations You can extend the Babel config applied in this plugin using `jsx.babelConfig` config path: From 9bafa6196b854d986fdc6316f6edac369c12f5e9 Mon Sep 17 00:00:00 2001 From: Ben Saufley Date: Wed, 22 Nov 2023 07:37:32 -0500 Subject: [PATCH 3/5] Run Prettier --- .../src/getAttributes.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/hast-util-to-babel-ast/src/getAttributes.ts b/packages/hast-util-to-babel-ast/src/getAttributes.ts index c2d2b232..20e39bd3 100644 --- a/packages/hast-util-to-babel-ast/src/getAttributes.ts +++ b/packages/hast-util-to-babel-ast/src/getAttributes.ts @@ -9,9 +9,13 @@ const convertAriaAttribute = (kebabKey: string) => { return `${aria}-${parts.join('').toLowerCase()}` } -const getKey = (key: string, node: ElementNode, transformAttributes: boolean) => { - if (!transformAttributes) return t.jsxIdentifier(key); - +const getKey = ( + key: string, + node: ElementNode, + transformAttributes: boolean, +) => { + if (!transformAttributes) return t.jsxIdentifier(key) + const lowerCaseKey = key.toLowerCase() const mappedElementAttribute = // @ts-ignore @@ -55,7 +59,10 @@ const getValue = (key: string, value: string[] | string | number) => { return t.stringLiteral(replaceSpaces(value)) } -export const getAttributes = (node: ElementNode, transformAttributes: boolean): t.JSXAttribute[] => { +export const getAttributes = ( + node: ElementNode, + transformAttributes: boolean, +): t.JSXAttribute[] => { if (!node.properties) return [] const keys = Object.keys(node.properties) const attributes = [] @@ -64,7 +71,10 @@ export const getAttributes = (node: ElementNode, transformAttributes: boolean): while (++index < keys.length) { const key = keys[index] const value = node.properties[key] - const attribute = t.jsxAttribute(getKey(key, node, transformAttributes), getValue(key, value)) + const attribute = t.jsxAttribute( + getKey(key, node, transformAttributes), + getValue(key, value), + ) attributes.push(attribute) } From df91afa20c1c119f02db52e922275dc926af22c8 Mon Sep 17 00:00:00 2001 From: Ben Saufley Date: Wed, 22 Nov 2023 09:02:43 -0500 Subject: [PATCH 4/5] Allow custom transformer function --- .../cli/src/__snapshots__/index.test.ts.snap | 12 +++++++++ packages/core/src/config.ts | 2 +- .../src/__snapshots__/index.test.ts.snap | 2 ++ .../src/configuration.ts | 4 ++- .../src/getAttributes.ts | 9 +++++-- .../hast-util-to-babel-ast/src/index.test.ts | 26 +++++++++++++++++++ packages/plugin-jsx/README.md | 19 ++++++++++++++ 7 files changed, 70 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/__snapshots__/index.test.ts.snap b/packages/cli/src/__snapshots__/index.test.ts.snap index 58f7d7d4..81a5ca38 100644 --- a/packages/cli/src/__snapshots__/index.test.ts.snap +++ b/packages/cli/src/__snapshots__/index.test.ts.snap @@ -13,6 +13,18 @@ export default SvgFile; " `; +exports[`cli should not work with a directory without --out-dir option 1`] = ` +"import * as React from 'react' +const SvgFile = (props) => ( + + + +) +export default SvgFile + +" +`; + exports[`cli should support --index-template in cli 1`] = ` "export { File } from './File' " diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 9292f1b7..5efc9e72 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -46,7 +46,7 @@ export interface Config { // JSX jsx?: { babelConfig?: BabelTransformOptions - transformAttributes?: boolean + transformAttributes?: boolean | ((key: string) => string) } } diff --git a/packages/hast-util-to-babel-ast/src/__snapshots__/index.test.ts.snap b/packages/hast-util-to-babel-ast/src/__snapshots__/index.test.ts.snap index c3d01abf..fcbb90a6 100644 --- a/packages/hast-util-to-babel-ast/src/__snapshots__/index.test.ts.snap +++ b/packages/hast-util-to-babel-ast/src/__snapshots__/index.test.ts.snap @@ -4,4 +4,6 @@ exports[`hast-util-to-babel-ast should handle spaces and tab 1`] = `"{"Dismiss"}{"Created with Sketch."};"`; +exports[`hast-util-to-babel-ast transforms SVG with custom attribute transformer 1`] = `"{"Dismiss"}{"Created with Sketch."};"`; + exports[`hast-util-to-babel-ast transforms SVG without transforming attributes 1`] = `"{"Dismiss"}{"Created with Sketch."};"`; diff --git a/packages/hast-util-to-babel-ast/src/configuration.ts b/packages/hast-util-to-babel-ast/src/configuration.ts index 08457e7e..1ea20b4d 100644 --- a/packages/hast-util-to-babel-ast/src/configuration.ts +++ b/packages/hast-util-to-babel-ast/src/configuration.ts @@ -1,5 +1,7 @@ +export type TransformAttributes = boolean | ((key: string) => string) + export interface Configuration { - transformAttributes: boolean + transformAttributes: TransformAttributes } export const getConfig = ( diff --git a/packages/hast-util-to-babel-ast/src/getAttributes.ts b/packages/hast-util-to-babel-ast/src/getAttributes.ts index 20e39bd3..2bc41c2e 100644 --- a/packages/hast-util-to-babel-ast/src/getAttributes.ts +++ b/packages/hast-util-to-babel-ast/src/getAttributes.ts @@ -3,6 +3,7 @@ import type { ElementNode } from 'svg-parser' import { isNumeric, kebabCase, replaceSpaces } from './util' import { stringToObjectStyle } from './stringToObjectStyle' import { ATTRIBUTE_MAPPING, ELEMENT_ATTRIBUTE_MAPPING } from './mappings' +import type { TransformAttributes } from './configuration' const convertAriaAttribute = (kebabKey: string) => { const [aria, ...parts] = kebabKey.split('-') @@ -12,10 +13,14 @@ const convertAriaAttribute = (kebabKey: string) => { const getKey = ( key: string, node: ElementNode, - transformAttributes: boolean, + transformAttributes: TransformAttributes, ) => { if (!transformAttributes) return t.jsxIdentifier(key) + if (typeof transformAttributes === 'function') { + return t.jsxIdentifier(transformAttributes(key)) + } + const lowerCaseKey = key.toLowerCase() const mappedElementAttribute = // @ts-ignore @@ -61,7 +66,7 @@ const getValue = (key: string, value: string[] | string | number) => { export const getAttributes = ( node: ElementNode, - transformAttributes: boolean, + transformAttributes: TransformAttributes, ): t.JSXAttribute[] => { if (!node.properties) return [] const keys = Object.keys(node.properties) diff --git a/packages/hast-util-to-babel-ast/src/index.test.ts b/packages/hast-util-to-babel-ast/src/index.test.ts index 68976e2f..861abd78 100644 --- a/packages/hast-util-to-babel-ast/src/index.test.ts +++ b/packages/hast-util-to-babel-ast/src/index.test.ts @@ -52,6 +52,32 @@ describe('hast-util-to-babel-ast', () => { expect(transform(code, { transformAttributes: false })).toMatchSnapshot() }) + it('transforms SVG with custom attribute transformer', () => { + const code = ` + + + + Dismiss + Created with Sketch. + + + + + + + + +` + expect( + transform(code, { + transformAttributes: (key: string) => + key.includes(':') + ? key.replace(/[^a-z0-9]([a-z])/, (_, v) => v.toUpperCase()) + : key, + }), + ).toMatchSnapshot() + }) + it('transforms "aria-x"', () => { const code = `` expect(transform(code)).toMatchInlineSnapshot( diff --git a/packages/plugin-jsx/README.md b/packages/plugin-jsx/README.md index dae11f8c..ad3426be 100644 --- a/packages/plugin-jsx/README.md +++ b/packages/plugin-jsx/README.md @@ -45,6 +45,25 @@ module.exports = { } ``` +A function can also be passed to `transformAttributes`, which will be used _instead of_ the +default logic. It will be called with the attribute to transform and expect the transformed +attribute as a return value: + +```js +// .svgrrc.js + +module.exports = { + jsx: { + transformAttributes: (attribute) => { + if (attribute === 'fill') { + return 'currentColor' + } + return attribute + }, + }, +} +``` + ## Applying custom transformations You can extend the Babel config applied in this plugin using `jsx.babelConfig` config path: From 82a48c477b4f4b9230f89f3424a5e01268f8dd03 Mon Sep 17 00:00:00 2001 From: Ben Saufley Date: Wed, 20 Dec 2023 11:13:28 -0500 Subject: [PATCH 5/5] Re-run tests to update snapshots; removed added snapshot that should not have been added --- packages/cli/src/__snapshots__/index.test.ts.snap | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/packages/cli/src/__snapshots__/index.test.ts.snap b/packages/cli/src/__snapshots__/index.test.ts.snap index 81a5ca38..58f7d7d4 100644 --- a/packages/cli/src/__snapshots__/index.test.ts.snap +++ b/packages/cli/src/__snapshots__/index.test.ts.snap @@ -13,18 +13,6 @@ export default SvgFile; " `; -exports[`cli should not work with a directory without --out-dir option 1`] = ` -"import * as React from 'react' -const SvgFile = (props) => ( - - - -) -export default SvgFile - -" -`; - exports[`cli should support --index-template in cli 1`] = ` "export { File } from './File' "