Skip to content

Commit 3700aba

Browse files
authoredNov 1, 2021
feat: support jsxRuntime (#613)
It default to "classic", the old behaviour. But it can be "automatic" (the recommended) or "classic-preact".
1 parent bbf0430 commit 3700aba

File tree

11 files changed

+313
-21
lines changed

11 files changed

+313
-21
lines changed
 

‎packages/babel-plugin-transform-svg-component/src/__snapshots__/index.test.ts.snap

+82
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,46 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

3+
exports[`plugin javascript #jsxRuntime allows to specify a custom "classic" jsxRuntime using "namespace" 1`] = `
4+
"import * as Preact from \\"preact\\";
5+
6+
const SvgComponent = () => <svg><g /></svg>;
7+
8+
export default SvgComponent;"
9+
`;
10+
11+
exports[`plugin javascript #jsxRuntime allows to specify a custom "classic" jsxRuntime using "specifiers" 1`] = `
12+
"import { h } from \\"preact\\";
13+
14+
const SvgComponent = () => <svg><g /></svg>;
15+
16+
export default SvgComponent;"
17+
`;
18+
19+
exports[`plugin javascript #jsxRuntime supports "automatic" jsxRuntime 1`] = `
20+
"const SvgComponent = () => <svg><g /></svg>;
21+
22+
export default SvgComponent;"
23+
`;
24+
25+
exports[`plugin javascript #jsxRuntime supports "classic" jsxRuntime 1`] = `
26+
"import * as React from \\"react\\";
27+
28+
const SvgComponent = () => <svg><g /></svg>;
29+
30+
export default SvgComponent;"
31+
`;
32+
33+
exports[`plugin javascript allows to specify a different import source 1`] = `
34+
"import { h } from \\"preact\\";
35+
import { forwardRef, memo } from \\"preact/compat\\";
36+
37+
const SvgComponent = (_, ref) => <svg><g /></svg>;
38+
39+
const ForwardRef = forwardRef(SvgComponent);
40+
const Memo = memo(ForwardRef);
41+
export default Memo;"
42+
`;
43+
344
exports[`plugin javascript custom templates support basic template 1`] = `
445
"import * as React from 'react';
546
@@ -162,6 +203,47 @@ const Memo = memo(ForwardRef);
162203
export default Memo;"
163204
`;
164205
206+
exports[`plugin typescript #jsxRuntime allows to specify a custom "classic" jsxRuntime using "namespace" 1`] = `
207+
"import * as Preact from \\"preact\\";
208+
209+
const SvgComponent = () => <svg><g /></svg>;
210+
211+
export default SvgComponent;"
212+
`;
213+
214+
exports[`plugin typescript #jsxRuntime allows to specify a custom "classic" jsxRuntime using "specifiers" 1`] = `
215+
"import { h } from \\"preact\\";
216+
217+
const SvgComponent = () => <svg><g /></svg>;
218+
219+
export default SvgComponent;"
220+
`;
221+
222+
exports[`plugin typescript #jsxRuntime supports "automatic" jsxRuntime 1`] = `
223+
"const SvgComponent = () => <svg><g /></svg>;
224+
225+
export default SvgComponent;"
226+
`;
227+
228+
exports[`plugin typescript #jsxRuntime supports "classic" jsxRuntime 1`] = `
229+
"import * as React from \\"react\\";
230+
231+
const SvgComponent = () => <svg><g /></svg>;
232+
233+
export default SvgComponent;"
234+
`;
235+
236+
exports[`plugin typescript allows to specify a different import source 1`] = `
237+
"import { h } from \\"preact\\";
238+
import { Ref, forwardRef, memo } from \\"preact/compat\\";
239+
240+
const SvgComponent = (_, ref: Ref<SVGSVGElement>) => <svg><g /></svg>;
241+
242+
const ForwardRef = forwardRef(SvgComponent);
243+
const Memo = memo(ForwardRef);
244+
export default Memo;"
245+
`;
246+
165247
exports[`plugin typescript custom templates support basic template 1`] = `
166248
"import * as React from 'react';
167249

‎packages/babel-plugin-transform-svg-component/src/index.test.ts

+53
Original file line numberDiff line numberDiff line change
@@ -228,5 +228,58 @@ describe('plugin', () => {
228228
expect(code).toMatchSnapshot()
229229
})
230230
})
231+
232+
describe('#jsxRuntime', () => {
233+
it('supports "automatic" jsxRuntime', () => {
234+
const { code } = testPlugin(language)('<svg><g /></svg>', {
235+
jsxRuntime: 'automatic',
236+
})
237+
expect(code).toMatchSnapshot()
238+
})
239+
240+
it('supports "classic" jsxRuntime', () => {
241+
const { code } = testPlugin(language)('<svg><g /></svg>', {
242+
jsxRuntime: 'classic',
243+
})
244+
expect(code).toMatchSnapshot()
245+
})
246+
247+
it('allows to specify a custom "classic" jsxRuntime using "specifiers"', () => {
248+
const { code } = testPlugin(language)('<svg><g /></svg>', {
249+
jsxRuntime: 'classic',
250+
jsxRuntimeImport: { specifiers: ['h'], source: 'preact' },
251+
})
252+
expect(code).toMatchSnapshot()
253+
})
254+
255+
it('allows to specify a custom "classic" jsxRuntime using "namespace"', () => {
256+
const { code } = testPlugin(language)('<svg><g /></svg>', {
257+
jsxRuntime: 'classic',
258+
jsxRuntimeImport: { namespace: 'Preact', source: 'preact' },
259+
})
260+
expect(code).toMatchSnapshot()
261+
})
262+
263+
it('throws with invalid configuration', () => {
264+
expect(() => {
265+
testPlugin(language)('<svg><g /></svg>', {
266+
jsxRuntime: 'classic',
267+
jsxRuntimeImport: { source: 'preact' },
268+
})
269+
}).toThrow(
270+
'Specify either "namespace" or "specifiers" in "jsxRuntimeImport" option',
271+
)
272+
})
273+
})
274+
275+
it('allows to specify a different import source', () => {
276+
const { code } = testPlugin(language)('<svg><g /></svg>', {
277+
memo: true,
278+
ref: true,
279+
importSource: 'preact/compat',
280+
jsxRuntimeImport: { specifiers: ['h'], source: 'preact' },
281+
})
282+
expect(code).toMatchSnapshot()
283+
})
231284
})
232285
})

‎packages/babel-plugin-transform-svg-component/src/types.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ interface State {
2626
caller?: { previousExport?: string | null }
2727
}
2828

29+
export interface JSXRuntimeImport {
30+
source: string
31+
namespace?: string
32+
specifiers?: string[]
33+
}
34+
2935
export interface Options {
3036
typescript?: boolean
3137
titleProp?: boolean
@@ -36,5 +42,8 @@ export interface Options {
3642
native?: boolean
3743
memo?: boolean
3844
exportType?: 'named' | 'default'
39-
namedExport: string
45+
namedExport?: string
46+
jsxRuntime?: 'automatic' | 'classic'
47+
jsxRuntimeImport?: JSXRuntimeImport
48+
importSource?: string
4049
}

‎packages/babel-plugin-transform-svg-component/src/variables.ts

+38-11
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { types as t } from '@babel/core'
2-
import type { Options, TemplateVariables } from './types'
2+
import type { Options, TemplateVariables, JSXRuntimeImport } from './types'
33

44
const tsOptionalPropertySignature = (
55
...args: Parameters<typeof t.tsPropertySignature>
@@ -15,6 +15,7 @@ interface Context {
1515
interfaces: t.TSInterfaceDeclaration[]
1616
props: (t.Identifier | t.ObjectPattern)[]
1717
imports: t.ImportDeclaration[]
18+
importSource: string
1819
}
1920

2021
const getOrCreateImport = ({ imports }: Context, sourceValue: string) => {
@@ -40,7 +41,7 @@ const tsTypeReferenceSVGProps = (ctx: Context) => {
4041
return t.tsTypeReference(identifier)
4142
}
4243
const identifier = t.identifier('SVGProps')
43-
getOrCreateImport(ctx, 'react').specifiers.push(
44+
getOrCreateImport(ctx, ctx.importSource).specifiers.push(
4445
t.importSpecifier(identifier, identifier),
4546
)
4647
return t.tsTypeReference(
@@ -53,7 +54,7 @@ const tsTypeReferenceSVGProps = (ctx: Context) => {
5354

5455
const tsTypeReferenceSVGRef = (ctx: Context) => {
5556
const identifier = t.identifier('Ref')
56-
getOrCreateImport(ctx, 'react').specifiers.push(
57+
getOrCreateImport(ctx, ctx.importSource).specifiers.push(
5758
t.importSpecifier(identifier, identifier),
5859
)
5960
return t.tsTypeReference(
@@ -64,6 +65,29 @@ const tsTypeReferenceSVGRef = (ctx: Context) => {
6465
)
6566
}
6667

68+
const getJsxRuntimeImport = (cfg: JSXRuntimeImport) => {
69+
const specifiers = (() => {
70+
if (cfg.namespace)
71+
return [t.importNamespaceSpecifier(t.identifier(cfg.namespace))]
72+
if (cfg.specifiers)
73+
return cfg.specifiers.map((specifier) => {
74+
const identifier = t.identifier(specifier)
75+
return t.importSpecifier(identifier, identifier)
76+
})
77+
throw new Error(
78+
`Specify either "namespace" or "specifiers" in "jsxRuntimeImport" option`,
79+
)
80+
})()
81+
return t.importDeclaration(specifiers, t.stringLiteral(cfg.source))
82+
}
83+
84+
const defaultJsxRuntimeImport: JSXRuntimeImport = {
85+
source: 'react',
86+
namespace: 'React',
87+
}
88+
89+
const defaultImportSource = 'react'
90+
6791
export const getVariables = ({
6892
opts,
6993
jsx,
@@ -77,6 +101,7 @@ export const getVariables = ({
77101
const imports: t.ImportDeclaration[] = []
78102
const exports: (t.VariableDeclaration | t.ExportDeclaration)[] = []
79103
const ctx = {
104+
importSource: opts.importSource ?? defaultImportSource,
80105
exportIdentifier: componentName,
81106
opts,
82107
interfaces,
@@ -85,12 +110,11 @@ export const getVariables = ({
85110
exports,
86111
}
87112

88-
imports.push(
89-
t.importDeclaration(
90-
[t.importNamespaceSpecifier(t.identifier('React'))],
91-
t.stringLiteral('react'),
92-
),
93-
)
113+
if (opts.jsxRuntime !== 'automatic') {
114+
imports.push(
115+
getJsxRuntimeImport(opts.jsxRuntimeImport ?? defaultJsxRuntimeImport),
116+
)
117+
}
94118

95119
if (opts.native) {
96120
getOrCreateImport(ctx, 'react-native-svg').specifiers.push(
@@ -171,7 +195,7 @@ export const getVariables = ({
171195
}
172196
const forwardRef = t.identifier('forwardRef')
173197
const ForwardRef = t.identifier('ForwardRef')
174-
getOrCreateImport(ctx, 'react').specifiers.push(
198+
getOrCreateImport(ctx, ctx.importSource).specifiers.push(
175199
t.importSpecifier(forwardRef, forwardRef),
176200
)
177201
exports.push(
@@ -188,7 +212,7 @@ export const getVariables = ({
188212
if (opts.memo) {
189213
const memo = t.identifier('memo')
190214
const Memo = t.identifier('Memo')
191-
getOrCreateImport(ctx, 'react').specifiers.push(
215+
getOrCreateImport(ctx, ctx.importSource).specifiers.push(
192216
t.importSpecifier(memo, memo),
193217
)
194218
exports.push(
@@ -203,6 +227,9 @@ export const getVariables = ({
203227
}
204228

205229
if (opts.state.caller?.previousExport || opts.exportType === 'named') {
230+
if (!opts.namedExport) {
231+
throw new Error(`"namedExport" not specified`)
232+
}
206233
exports.push(
207234
t.exportNamedDeclaration(null, [
208235
t.exportSpecifier(ctx.exportIdentifier, t.identifier(opts.namedExport)),

‎packages/cli/src/__snapshots__/index.test.ts.snap

+26
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,32 @@ export default SvgFile
196196
"
197197
`;
198198

199+
exports[`cli should support various args: --jsx-runtime automatic 1`] = `
200+
"const SvgFile = (props) => (
201+
<svg width={48} height={1} xmlns=\\"http://www.w3.org/2000/svg\\" {...props}>
202+
<path d=\\"M0 0h48v1H0z\\" fill=\\"#063855\\" fillRule=\\"evenodd\\" />
203+
</svg>
204+
)
205+
206+
export default SvgFile
207+
208+
"
209+
`;
210+
211+
exports[`cli should support various args: --jsx-runtime classic-preact 1`] = `
212+
"import { h } from 'preact'
213+
214+
const SvgFile = (props) => (
215+
<svg width={48} height={1} xmlns=\\"http://www.w3.org/2000/svg\\" {...props}>
216+
<path d=\\"M0 0h48v1H0z\\" fill=\\"#063855\\" fillRule=\\"evenodd\\" />
217+
</svg>
218+
)
219+
220+
export default SvgFile
221+
222+
"
223+
`;
224+
199225
exports[`cli should support various args: --native --expand-props none 1`] = `
200226
"import * as React from 'react'
201227
import Svg, { Path } from 'react-native-svg'

‎packages/cli/src/index.test.ts

+2
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ describe('cli', () => {
114114

115115
it.each([
116116
['--no-dimensions'],
117+
['--jsx-runtime classic-preact'],
118+
['--jsx-runtime automatic'],
117119
['--expand-props none'],
118120
['--expand-props start'],
119121
['--icon'],

‎packages/cli/src/index.ts

+4
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ program
106106
'specify filename case ("pascal", "kebab", "camel") (default: "pascal")',
107107
)
108108
.option('--icon', 'use "1em" as width and height')
109+
.option(
110+
'--jsx-runtime <runtime>',
111+
'specify JSX runtime ("automatic", "classic", "classic-preact") (default: "classic")',
112+
)
109113
.option('--typescript', 'transform svg into typescript')
110114
.option('--native', 'add react-native support with react-native-svg')
111115
.option('--memo', 'add React.memo into the result component')

‎packages/core/src/config.ts

+20-3
Original file line numberDiff line numberDiff line change
@@ -6,23 +6,40 @@ import type { TransformOptions as BabelTransformOptions } from '@babel/core'
66
import type { ConfigPlugin } from './plugins'
77
import type { State } from './state'
88

9-
export interface Config extends Partial<Omit<TransformOptions, 'state'>> {
9+
export interface Config {
10+
ref?: boolean
11+
titleProp?: boolean
12+
expandProps?: boolean | 'start' | 'end'
1013
dimensions?: boolean
11-
runtimeConfig?: boolean
14+
icon?: boolean
1215
native?: boolean
16+
svgProps?: {
17+
[key: string]: string
18+
}
19+
replaceAttrValues?: {
20+
[key: string]: string
21+
}
22+
runtimeConfig?: boolean
1323
typescript?: boolean
1424
prettier?: boolean
1525
prettierConfig?: PrettierOptions
1626
svgo?: boolean
1727
svgoConfig?: SvgoOptions
1828
configFile?: string
29+
template?: TransformOptions['template']
30+
memo?: boolean
31+
exportType?: 'named' | 'default'
32+
namedExport?: string
33+
jsxRuntime?: 'classic' | 'classic-preact' | 'automatic'
1934

2035
// CLI only
2136
index?: boolean
2237
plugins?: ConfigPlugin[]
2338

2439
// JSX
25-
jsx?: { babelConfig?: BabelTransformOptions }
40+
jsx?: {
41+
babelConfig?: BabelTransformOptions
42+
}
2643
}
2744

2845
export const DEFAULT_CONFIG: Config = {

‎packages/plugin-jsx/src/index.test.ts

+30-2
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const svgBaseCode = `
1717
`
1818

1919
describe('plugin', () => {
20-
it('should transform code', () => {
20+
it('transforms code', () => {
2121
const result = jsx(svgBaseCode, {}, { componentName: 'SvgComponent' })
2222
expect(result).toMatchInlineSnapshot(`
2323
"import * as React from \\"react\\";
@@ -28,7 +28,35 @@ describe('plugin', () => {
2828
`)
2929
})
3030

31-
it('should accept jsx config', () => {
31+
it('supports "automatic" runtime', () => {
32+
const result = jsx(
33+
svgBaseCode,
34+
{ jsxRuntime: 'automatic' },
35+
{ componentName: 'SvgComponent' },
36+
)
37+
expect(result).toMatchInlineSnapshot(`
38+
"const SvgComponent = () => <svg viewBox=\\"0 0 88 88\\" xmlns=\\"http://www.w3.org/2000/svg\\" xmlnsXlink=\\"http://www.w3.org/1999/xlink\\"><title>{\\"Dismiss\\"}</title><desc>{\\"Created with Sketch.\\"}</desc><defs /><g id=\\"Blocks\\" stroke=\\"none\\" strokeWidth={1} fill=\\"none\\" fillRule=\\"evenodd\\" strokeLinecap=\\"square\\"><g id=\\"Dismiss\\" stroke=\\"#063855\\" strokeWidth={2}><path d=\\"M51,37 L37,51\\" id=\\"Shape\\" /><path d=\\"M51,51 L37,37\\" id=\\"Shape\\" /></g></g></svg>;
39+
40+
export default SvgComponent;"
41+
`)
42+
})
43+
44+
it('supports "preact" preset', () => {
45+
const result = jsx(
46+
svgBaseCode,
47+
{ jsxRuntime: 'classic-preact' },
48+
{ componentName: 'SvgComponent' },
49+
)
50+
expect(result).toMatchInlineSnapshot(`
51+
"import { h } from \\"preact\\";
52+
53+
const SvgComponent = () => <svg viewBox=\\"0 0 88 88\\" xmlns=\\"http://www.w3.org/2000/svg\\" xmlnsXlink=\\"http://www.w3.org/1999/xlink\\"><title>{\\"Dismiss\\"}</title><desc>{\\"Created with Sketch.\\"}</desc><defs /><g id=\\"Blocks\\" stroke=\\"none\\" strokeWidth={1} fill=\\"none\\" fillRule=\\"evenodd\\" strokeLinecap=\\"square\\"><g id=\\"Dismiss\\" stroke=\\"#063855\\" strokeWidth={2}><path d=\\"M51,37 L37,51\\" id=\\"Shape\\" /><path d=\\"M51,51 L37,37\\" id=\\"Shape\\" /></g></g></svg>;
54+
55+
export default SvgComponent;"
56+
`)
57+
})
58+
59+
it('accepts jsx config', () => {
3260
const dropTitle = () => ({
3361
visitor: {
3462
JSXElement(path: any) {

‎packages/plugin-jsx/src/index.ts

+46-3
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,64 @@
11
import { parse } from 'svg-parser'
22
import hastToBabelAst from '@svgr/hast-util-to-babel-ast'
33
import { transformFromAstSync, createConfigItem } from '@babel/core'
4-
import svgrBabelPreset from '@svgr/babel-preset'
5-
import type { Plugin } from '@svgr/core'
4+
import svgrBabelPreset, {
5+
Options as SvgrPresetOptions,
6+
} from '@svgr/babel-preset'
7+
import type { Plugin, Config } from '@svgr/core'
8+
9+
const getJsxRuntimeOptions = (config: Config): Partial<SvgrPresetOptions> => {
10+
switch (config.jsxRuntime) {
11+
case null:
12+
case undefined:
13+
case 'classic':
14+
return {
15+
jsxRuntime: 'classic',
16+
importSource: 'react',
17+
jsxRuntimeImport: { namespace: 'React', source: 'react' },
18+
}
19+
case 'classic-preact':
20+
return {
21+
jsxRuntime: 'classic',
22+
importSource: 'preact/compat',
23+
jsxRuntimeImport: { specifiers: ['h'], source: 'preact' },
24+
}
25+
case 'automatic':
26+
return { jsxRuntime: 'automatic' }
27+
default:
28+
throw new Error(`Unsupported "jsxRuntime" "${config.jsxRuntime}"`)
29+
}
30+
}
631

732
const jsxPlugin: Plugin = (code, config, state) => {
833
const filePath = state.filePath || 'unknown'
934
const hastTree = parse(code)
1035

1136
const babelTree = hastToBabelAst(hastTree)
1237

38+
const svgPresetOptions: SvgrPresetOptions = {
39+
ref: config.ref,
40+
titleProp: config.titleProp,
41+
expandProps: config.expandProps,
42+
dimensions: config.dimensions,
43+
icon: config.icon,
44+
native: config.native,
45+
svgProps: config.svgProps,
46+
replaceAttrValues: config.replaceAttrValues,
47+
typescript: config.typescript,
48+
template: config.template,
49+
memo: config.memo,
50+
exportType: config.exportType,
51+
namedExport: config.namedExport,
52+
...getJsxRuntimeOptions(config),
53+
state,
54+
}
55+
1356
const result = transformFromAstSync(babelTree, code, {
1457
caller: {
1558
name: 'svgr',
1659
},
1760
presets: [
18-
createConfigItem([svgrBabelPreset, { ...config, state }], {
61+
createConfigItem([svgrBabelPreset, svgPresetOptions], {
1962
type: 'preset',
2063
}),
2164
],

‎tsconfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"module": "ESNext",
1212
"strict": true,
1313
"sourceMap": true,
14-
"declaration": true
14+
"declaration": true,
15+
"resolveJsonModule": true
1516
}
1617
}

1 commit comments

Comments
 (1)

vercel[bot] commented on Nov 1, 2021

@vercel[bot]
Please sign in to comment.