Skip to content

Commit

Permalink
feat: support jsxRuntime (#613)
Browse files Browse the repository at this point in the history
It default to "classic", the old behaviour. But it can be "automatic" (the recommended) or "classic-preact".
  • Loading branch information
gregberge committed Nov 1, 2021
1 parent bbf0430 commit 3700aba
Show file tree
Hide file tree
Showing 11 changed files with 313 additions and 21 deletions.
@@ -1,5 +1,46 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`plugin javascript #jsxRuntime allows to specify a custom "classic" jsxRuntime using "namespace" 1`] = `
"import * as Preact from \\"preact\\";
const SvgComponent = () => <svg><g /></svg>;
export default SvgComponent;"
`;

exports[`plugin javascript #jsxRuntime allows to specify a custom "classic" jsxRuntime using "specifiers" 1`] = `
"import { h } from \\"preact\\";
const SvgComponent = () => <svg><g /></svg>;
export default SvgComponent;"
`;

exports[`plugin javascript #jsxRuntime supports "automatic" jsxRuntime 1`] = `
"const SvgComponent = () => <svg><g /></svg>;
export default SvgComponent;"
`;

exports[`plugin javascript #jsxRuntime supports "classic" jsxRuntime 1`] = `
"import * as React from \\"react\\";
const SvgComponent = () => <svg><g /></svg>;
export default SvgComponent;"
`;

exports[`plugin javascript allows to specify a different import source 1`] = `
"import { h } from \\"preact\\";
import { forwardRef, memo } from \\"preact/compat\\";
const SvgComponent = (_, ref) => <svg><g /></svg>;
const ForwardRef = forwardRef(SvgComponent);
const Memo = memo(ForwardRef);
export default Memo;"
`;

exports[`plugin javascript custom templates support basic template 1`] = `
"import * as React from 'react';
Expand Down Expand Up @@ -162,6 +203,47 @@ const Memo = memo(ForwardRef);
export default Memo;"
`;
exports[`plugin typescript #jsxRuntime allows to specify a custom "classic" jsxRuntime using "namespace" 1`] = `
"import * as Preact from \\"preact\\";
const SvgComponent = () => <svg><g /></svg>;
export default SvgComponent;"
`;
exports[`plugin typescript #jsxRuntime allows to specify a custom "classic" jsxRuntime using "specifiers" 1`] = `
"import { h } from \\"preact\\";
const SvgComponent = () => <svg><g /></svg>;
export default SvgComponent;"
`;
exports[`plugin typescript #jsxRuntime supports "automatic" jsxRuntime 1`] = `
"const SvgComponent = () => <svg><g /></svg>;
export default SvgComponent;"
`;
exports[`plugin typescript #jsxRuntime supports "classic" jsxRuntime 1`] = `
"import * as React from \\"react\\";
const SvgComponent = () => <svg><g /></svg>;
export default SvgComponent;"
`;
exports[`plugin typescript allows to specify a different import source 1`] = `
"import { h } from \\"preact\\";
import { Ref, forwardRef, memo } from \\"preact/compat\\";
const SvgComponent = (_, ref: Ref<SVGSVGElement>) => <svg><g /></svg>;
const ForwardRef = forwardRef(SvgComponent);
const Memo = memo(ForwardRef);
export default Memo;"
`;
exports[`plugin typescript custom templates support basic template 1`] = `
"import * as React from 'react';
Expand Down
53 changes: 53 additions & 0 deletions packages/babel-plugin-transform-svg-component/src/index.test.ts
Expand Up @@ -228,5 +228,58 @@ describe('plugin', () => {
expect(code).toMatchSnapshot()
})
})

describe('#jsxRuntime', () => {
it('supports "automatic" jsxRuntime', () => {
const { code } = testPlugin(language)('<svg><g /></svg>', {
jsxRuntime: 'automatic',
})
expect(code).toMatchSnapshot()
})

it('supports "classic" jsxRuntime', () => {
const { code } = testPlugin(language)('<svg><g /></svg>', {
jsxRuntime: 'classic',
})
expect(code).toMatchSnapshot()
})

it('allows to specify a custom "classic" jsxRuntime using "specifiers"', () => {
const { code } = testPlugin(language)('<svg><g /></svg>', {
jsxRuntime: 'classic',
jsxRuntimeImport: { specifiers: ['h'], source: 'preact' },
})
expect(code).toMatchSnapshot()
})

it('allows to specify a custom "classic" jsxRuntime using "namespace"', () => {
const { code } = testPlugin(language)('<svg><g /></svg>', {
jsxRuntime: 'classic',
jsxRuntimeImport: { namespace: 'Preact', source: 'preact' },
})
expect(code).toMatchSnapshot()
})

it('throws with invalid configuration', () => {
expect(() => {
testPlugin(language)('<svg><g /></svg>', {
jsxRuntime: 'classic',
jsxRuntimeImport: { source: 'preact' },
})
}).toThrow(
'Specify either "namespace" or "specifiers" in "jsxRuntimeImport" option',
)
})
})

it('allows to specify a different import source', () => {
const { code } = testPlugin(language)('<svg><g /></svg>', {
memo: true,
ref: true,
importSource: 'preact/compat',
jsxRuntimeImport: { specifiers: ['h'], source: 'preact' },
})
expect(code).toMatchSnapshot()
})
})
})
11 changes: 10 additions & 1 deletion packages/babel-plugin-transform-svg-component/src/types.ts
Expand Up @@ -26,6 +26,12 @@ interface State {
caller?: { previousExport?: string | null }
}

export interface JSXRuntimeImport {
source: string
namespace?: string
specifiers?: string[]
}

export interface Options {
typescript?: boolean
titleProp?: boolean
Expand All @@ -36,5 +42,8 @@ export interface Options {
native?: boolean
memo?: boolean
exportType?: 'named' | 'default'
namedExport: string
namedExport?: string
jsxRuntime?: 'automatic' | 'classic'
jsxRuntimeImport?: JSXRuntimeImport
importSource?: string
}
49 changes: 38 additions & 11 deletions packages/babel-plugin-transform-svg-component/src/variables.ts
@@ -1,5 +1,5 @@
import { types as t } from '@babel/core'
import type { Options, TemplateVariables } from './types'
import type { Options, TemplateVariables, JSXRuntimeImport } from './types'

const tsOptionalPropertySignature = (
...args: Parameters<typeof t.tsPropertySignature>
Expand All @@ -15,6 +15,7 @@ interface Context {
interfaces: t.TSInterfaceDeclaration[]
props: (t.Identifier | t.ObjectPattern)[]
imports: t.ImportDeclaration[]
importSource: string
}

const getOrCreateImport = ({ imports }: Context, sourceValue: string) => {
Expand All @@ -40,7 +41,7 @@ const tsTypeReferenceSVGProps = (ctx: Context) => {
return t.tsTypeReference(identifier)
}
const identifier = t.identifier('SVGProps')
getOrCreateImport(ctx, 'react').specifiers.push(
getOrCreateImport(ctx, ctx.importSource).specifiers.push(
t.importSpecifier(identifier, identifier),
)
return t.tsTypeReference(
Expand All @@ -53,7 +54,7 @@ const tsTypeReferenceSVGProps = (ctx: Context) => {

const tsTypeReferenceSVGRef = (ctx: Context) => {
const identifier = t.identifier('Ref')
getOrCreateImport(ctx, 'react').specifiers.push(
getOrCreateImport(ctx, ctx.importSource).specifiers.push(
t.importSpecifier(identifier, identifier),
)
return t.tsTypeReference(
Expand All @@ -64,6 +65,29 @@ const tsTypeReferenceSVGRef = (ctx: Context) => {
)
}

const getJsxRuntimeImport = (cfg: JSXRuntimeImport) => {
const specifiers = (() => {
if (cfg.namespace)
return [t.importNamespaceSpecifier(t.identifier(cfg.namespace))]
if (cfg.specifiers)
return cfg.specifiers.map((specifier) => {
const identifier = t.identifier(specifier)
return t.importSpecifier(identifier, identifier)
})
throw new Error(
`Specify either "namespace" or "specifiers" in "jsxRuntimeImport" option`,
)
})()
return t.importDeclaration(specifiers, t.stringLiteral(cfg.source))
}

const defaultJsxRuntimeImport: JSXRuntimeImport = {
source: 'react',
namespace: 'React',
}

const defaultImportSource = 'react'

export const getVariables = ({
opts,
jsx,
Expand All @@ -77,6 +101,7 @@ export const getVariables = ({
const imports: t.ImportDeclaration[] = []
const exports: (t.VariableDeclaration | t.ExportDeclaration)[] = []
const ctx = {
importSource: opts.importSource ?? defaultImportSource,
exportIdentifier: componentName,
opts,
interfaces,
Expand All @@ -85,12 +110,11 @@ export const getVariables = ({
exports,
}

imports.push(
t.importDeclaration(
[t.importNamespaceSpecifier(t.identifier('React'))],
t.stringLiteral('react'),
),
)
if (opts.jsxRuntime !== 'automatic') {
imports.push(
getJsxRuntimeImport(opts.jsxRuntimeImport ?? defaultJsxRuntimeImport),
)
}

if (opts.native) {
getOrCreateImport(ctx, 'react-native-svg').specifiers.push(
Expand Down Expand Up @@ -171,7 +195,7 @@ export const getVariables = ({
}
const forwardRef = t.identifier('forwardRef')
const ForwardRef = t.identifier('ForwardRef')
getOrCreateImport(ctx, 'react').specifiers.push(
getOrCreateImport(ctx, ctx.importSource).specifiers.push(
t.importSpecifier(forwardRef, forwardRef),
)
exports.push(
Expand All @@ -188,7 +212,7 @@ export const getVariables = ({
if (opts.memo) {
const memo = t.identifier('memo')
const Memo = t.identifier('Memo')
getOrCreateImport(ctx, 'react').specifiers.push(
getOrCreateImport(ctx, ctx.importSource).specifiers.push(
t.importSpecifier(memo, memo),
)
exports.push(
Expand All @@ -203,6 +227,9 @@ export const getVariables = ({
}

if (opts.state.caller?.previousExport || opts.exportType === 'named') {
if (!opts.namedExport) {
throw new Error(`"namedExport" not specified`)
}
exports.push(
t.exportNamedDeclaration(null, [
t.exportSpecifier(ctx.exportIdentifier, t.identifier(opts.namedExport)),
Expand Down
26 changes: 26 additions & 0 deletions packages/cli/src/__snapshots__/index.test.ts.snap
Expand Up @@ -196,6 +196,32 @@ export default SvgFile
"
`;

exports[`cli should support various args: --jsx-runtime automatic 1`] = `
"const SvgFile = (props) => (
<svg width={48} height={1} xmlns=\\"http://www.w3.org/2000/svg\\" {...props}>
<path d=\\"M0 0h48v1H0z\\" fill=\\"#063855\\" fillRule=\\"evenodd\\" />
</svg>
)
export default SvgFile
"
`;

exports[`cli should support various args: --jsx-runtime classic-preact 1`] = `
"import { h } from 'preact'
const SvgFile = (props) => (
<svg width={48} height={1} xmlns=\\"http://www.w3.org/2000/svg\\" {...props}>
<path d=\\"M0 0h48v1H0z\\" fill=\\"#063855\\" fillRule=\\"evenodd\\" />
</svg>
)
export default SvgFile
"
`;

exports[`cli should support various args: --native --expand-props none 1`] = `
"import * as React from 'react'
import Svg, { Path } from 'react-native-svg'
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/index.test.ts
Expand Up @@ -114,6 +114,8 @@ describe('cli', () => {

it.each([
['--no-dimensions'],
['--jsx-runtime classic-preact'],
['--jsx-runtime automatic'],
['--expand-props none'],
['--expand-props start'],
['--icon'],
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/index.ts
Expand Up @@ -106,6 +106,10 @@ program
'specify filename case ("pascal", "kebab", "camel") (default: "pascal")',
)
.option('--icon', 'use "1em" as width and height')
.option(
'--jsx-runtime <runtime>',
'specify JSX runtime ("automatic", "classic", "classic-preact") (default: "classic")',
)
.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')
Expand Down
23 changes: 20 additions & 3 deletions packages/core/src/config.ts
Expand Up @@ -6,23 +6,40 @@ import type { TransformOptions as BabelTransformOptions } from '@babel/core'
import type { ConfigPlugin } from './plugins'
import type { State } from './state'

export interface Config extends Partial<Omit<TransformOptions, 'state'>> {
export interface Config {
ref?: boolean
titleProp?: boolean
expandProps?: boolean | 'start' | 'end'
dimensions?: boolean
runtimeConfig?: boolean
icon?: boolean
native?: boolean
svgProps?: {
[key: string]: string
}
replaceAttrValues?: {
[key: string]: string
}
runtimeConfig?: boolean
typescript?: boolean
prettier?: boolean
prettierConfig?: PrettierOptions
svgo?: boolean
svgoConfig?: SvgoOptions
configFile?: string
template?: TransformOptions['template']
memo?: boolean
exportType?: 'named' | 'default'
namedExport?: string
jsxRuntime?: 'classic' | 'classic-preact' | 'automatic'

// CLI only
index?: boolean
plugins?: ConfigPlugin[]

// JSX
jsx?: { babelConfig?: BabelTransformOptions }
jsx?: {
babelConfig?: BabelTransformOptions
}
}

export const DEFAULT_CONFIG: Config = {
Expand Down

1 comment on commit 3700aba

@vercel
Copy link

@vercel vercel bot commented on 3700aba Nov 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.