From 3700aba837b3b6e708009559071fb5e7e6e606f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Greg=20Berg=C3=A9?= Date: Mon, 1 Nov 2021 08:39:18 +0100 Subject: [PATCH] feat: support jsxRuntime (#613) It default to "classic", the old behaviour. But it can be "automatic" (the recommended) or "classic-preact". --- .../src/__snapshots__/index.test.ts.snap | 82 +++++++++++++++++++ .../src/index.test.ts | 53 ++++++++++++ .../src/types.ts | 11 ++- .../src/variables.ts | 49 ++++++++--- .../cli/src/__snapshots__/index.test.ts.snap | 26 ++++++ packages/cli/src/index.test.ts | 2 + packages/cli/src/index.ts | 4 + packages/core/src/config.ts | 23 +++++- packages/plugin-jsx/src/index.test.ts | 32 +++++++- packages/plugin-jsx/src/index.ts | 49 ++++++++++- tsconfig.json | 3 +- 11 files changed, 313 insertions(+), 21 deletions(-) diff --git a/packages/babel-plugin-transform-svg-component/src/__snapshots__/index.test.ts.snap b/packages/babel-plugin-transform-svg-component/src/__snapshots__/index.test.ts.snap index 6ab44453..10ab6f63 100644 --- a/packages/babel-plugin-transform-svg-component/src/__snapshots__/index.test.ts.snap +++ b/packages/babel-plugin-transform-svg-component/src/__snapshots__/index.test.ts.snap @@ -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 = () => ; + +export default SvgComponent;" +`; + +exports[`plugin javascript #jsxRuntime allows to specify a custom "classic" jsxRuntime using "specifiers" 1`] = ` +"import { h } from \\"preact\\"; + +const SvgComponent = () => ; + +export default SvgComponent;" +`; + +exports[`plugin javascript #jsxRuntime supports "automatic" jsxRuntime 1`] = ` +"const SvgComponent = () => ; + +export default SvgComponent;" +`; + +exports[`plugin javascript #jsxRuntime supports "classic" jsxRuntime 1`] = ` +"import * as React from \\"react\\"; + +const SvgComponent = () => ; + +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) => ; + +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'; @@ -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 = () => ; + +export default SvgComponent;" +`; + +exports[`plugin typescript #jsxRuntime allows to specify a custom "classic" jsxRuntime using "specifiers" 1`] = ` +"import { h } from \\"preact\\"; + +const SvgComponent = () => ; + +export default SvgComponent;" +`; + +exports[`plugin typescript #jsxRuntime supports "automatic" jsxRuntime 1`] = ` +"const SvgComponent = () => ; + +export default SvgComponent;" +`; + +exports[`plugin typescript #jsxRuntime supports "classic" jsxRuntime 1`] = ` +"import * as React from \\"react\\"; + +const SvgComponent = () => ; + +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) => ; + +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'; diff --git a/packages/babel-plugin-transform-svg-component/src/index.test.ts b/packages/babel-plugin-transform-svg-component/src/index.test.ts index 0d0ff38b..35aa2628 100644 --- a/packages/babel-plugin-transform-svg-component/src/index.test.ts +++ b/packages/babel-plugin-transform-svg-component/src/index.test.ts @@ -228,5 +228,58 @@ describe('plugin', () => { expect(code).toMatchSnapshot() }) }) + + describe('#jsxRuntime', () => { + it('supports "automatic" jsxRuntime', () => { + const { code } = testPlugin(language)('', { + jsxRuntime: 'automatic', + }) + expect(code).toMatchSnapshot() + }) + + it('supports "classic" jsxRuntime', () => { + const { code } = testPlugin(language)('', { + jsxRuntime: 'classic', + }) + expect(code).toMatchSnapshot() + }) + + it('allows to specify a custom "classic" jsxRuntime using "specifiers"', () => { + const { code } = testPlugin(language)('', { + jsxRuntime: 'classic', + jsxRuntimeImport: { specifiers: ['h'], source: 'preact' }, + }) + expect(code).toMatchSnapshot() + }) + + it('allows to specify a custom "classic" jsxRuntime using "namespace"', () => { + const { code } = testPlugin(language)('', { + jsxRuntime: 'classic', + jsxRuntimeImport: { namespace: 'Preact', source: 'preact' }, + }) + expect(code).toMatchSnapshot() + }) + + it('throws with invalid configuration', () => { + expect(() => { + testPlugin(language)('', { + 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)('', { + memo: true, + ref: true, + importSource: 'preact/compat', + jsxRuntimeImport: { specifiers: ['h'], source: 'preact' }, + }) + expect(code).toMatchSnapshot() + }) }) }) diff --git a/packages/babel-plugin-transform-svg-component/src/types.ts b/packages/babel-plugin-transform-svg-component/src/types.ts index fe4a9889..f1af5586 100644 --- a/packages/babel-plugin-transform-svg-component/src/types.ts +++ b/packages/babel-plugin-transform-svg-component/src/types.ts @@ -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 @@ -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 } diff --git a/packages/babel-plugin-transform-svg-component/src/variables.ts b/packages/babel-plugin-transform-svg-component/src/variables.ts index 198fb2df..6eb386c1 100644 --- a/packages/babel-plugin-transform-svg-component/src/variables.ts +++ b/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 @@ -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) => { @@ -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( @@ -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( @@ -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, @@ -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, @@ -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( @@ -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( @@ -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( @@ -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)), diff --git a/packages/cli/src/__snapshots__/index.test.ts.snap b/packages/cli/src/__snapshots__/index.test.ts.snap index dad8c0d9..a11c02f9 100644 --- a/packages/cli/src/__snapshots__/index.test.ts.snap +++ b/packages/cli/src/__snapshots__/index.test.ts.snap @@ -196,6 +196,32 @@ export default SvgFile " `; +exports[`cli should support various args: --jsx-runtime automatic 1`] = ` +"const SvgFile = (props) => ( + + + +) + +export default SvgFile + +" +`; + +exports[`cli should support various args: --jsx-runtime classic-preact 1`] = ` +"import { h } from 'preact' + +const SvgFile = (props) => ( + + + +) + +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' diff --git a/packages/cli/src/index.test.ts b/packages/cli/src/index.test.ts index 676a20f5..4017bf3e 100644 --- a/packages/cli/src/index.test.ts +++ b/packages/cli/src/index.test.ts @@ -114,6 +114,8 @@ describe('cli', () => { it.each([ ['--no-dimensions'], + ['--jsx-runtime classic-preact'], + ['--jsx-runtime automatic'], ['--expand-props none'], ['--expand-props start'], ['--icon'], diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 429e8a0c..ff78db6a 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -106,6 +106,10 @@ program 'specify filename case ("pascal", "kebab", "camel") (default: "pascal")', ) .option('--icon', 'use "1em" as width and height') + .option( + '--jsx-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') diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 09ba6bfc..113e38ab 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -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> { +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 = { diff --git a/packages/plugin-jsx/src/index.test.ts b/packages/plugin-jsx/src/index.test.ts index b1e968e0..f4e3bccd 100644 --- a/packages/plugin-jsx/src/index.test.ts +++ b/packages/plugin-jsx/src/index.test.ts @@ -17,7 +17,7 @@ const svgBaseCode = ` ` describe('plugin', () => { - it('should transform code', () => { + it('transforms code', () => { const result = jsx(svgBaseCode, {}, { componentName: 'SvgComponent' }) expect(result).toMatchInlineSnapshot(` "import * as React from \\"react\\"; @@ -28,7 +28,35 @@ describe('plugin', () => { `) }) - it('should accept jsx config', () => { + it('supports "automatic" runtime', () => { + const result = jsx( + svgBaseCode, + { jsxRuntime: 'automatic' }, + { componentName: 'SvgComponent' }, + ) + expect(result).toMatchInlineSnapshot(` + "const SvgComponent = () => {\\"Dismiss\\"}{\\"Created with Sketch.\\"}; + + export default SvgComponent;" + `) + }) + + it('supports "preact" preset', () => { + const result = jsx( + svgBaseCode, + { jsxRuntime: 'classic-preact' }, + { componentName: 'SvgComponent' }, + ) + expect(result).toMatchInlineSnapshot(` + "import { h } from \\"preact\\"; + + const SvgComponent = () => {\\"Dismiss\\"}{\\"Created with Sketch.\\"}; + + export default SvgComponent;" + `) + }) + + it('accepts jsx config', () => { const dropTitle = () => ({ visitor: { JSXElement(path: any) { diff --git a/packages/plugin-jsx/src/index.ts b/packages/plugin-jsx/src/index.ts index 87dfd3ba..57e9c844 100644 --- a/packages/plugin-jsx/src/index.ts +++ b/packages/plugin-jsx/src/index.ts @@ -1,8 +1,33 @@ import { parse } from 'svg-parser' import hastToBabelAst from '@svgr/hast-util-to-babel-ast' import { transformFromAstSync, createConfigItem } from '@babel/core' -import svgrBabelPreset from '@svgr/babel-preset' -import type { Plugin } from '@svgr/core' +import svgrBabelPreset, { + Options as SvgrPresetOptions, +} from '@svgr/babel-preset' +import type { Plugin, Config } from '@svgr/core' + +const getJsxRuntimeOptions = (config: Config): Partial => { + switch (config.jsxRuntime) { + case null: + case undefined: + case 'classic': + return { + jsxRuntime: 'classic', + importSource: 'react', + jsxRuntimeImport: { namespace: 'React', source: 'react' }, + } + case 'classic-preact': + return { + jsxRuntime: 'classic', + importSource: 'preact/compat', + jsxRuntimeImport: { specifiers: ['h'], source: 'preact' }, + } + case 'automatic': + return { jsxRuntime: 'automatic' } + default: + throw new Error(`Unsupported "jsxRuntime" "${config.jsxRuntime}"`) + } +} const jsxPlugin: Plugin = (code, config, state) => { const filePath = state.filePath || 'unknown' @@ -10,12 +35,30 @@ const jsxPlugin: Plugin = (code, config, state) => { const babelTree = hastToBabelAst(hastTree) + const svgPresetOptions: SvgrPresetOptions = { + ref: config.ref, + titleProp: config.titleProp, + expandProps: config.expandProps, + dimensions: config.dimensions, + icon: config.icon, + native: config.native, + svgProps: config.svgProps, + replaceAttrValues: config.replaceAttrValues, + typescript: config.typescript, + template: config.template, + memo: config.memo, + exportType: config.exportType, + namedExport: config.namedExport, + ...getJsxRuntimeOptions(config), + state, + } + const result = transformFromAstSync(babelTree, code, { caller: { name: 'svgr', }, presets: [ - createConfigItem([svgrBabelPreset, { ...config, state }], { + createConfigItem([svgrBabelPreset, svgPresetOptions], { type: 'preset', }), ], diff --git a/tsconfig.json b/tsconfig.json index a5fb6577..9eed761b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,6 +11,7 @@ "module": "ESNext", "strict": true, "sourceMap": true, - "declaration": true + "declaration": true, + "resolveJsonModule": true } }