diff --git a/.changeset/sweet-plums-behave.md b/.changeset/sweet-plums-behave.md new file mode 100644 index 0000000000..0f3780900b --- /dev/null +++ b/.changeset/sweet-plums-behave.md @@ -0,0 +1,5 @@ +--- +'@emotion/eslint-plugin': patch +--- + +implement automatic adding of jsxImportSource pragma definition diff --git a/packages/eslint-plugin/src/rules/jsx-import.js b/packages/eslint-plugin/src/rules/jsx-import.js index 1618e7bb25..8b7cab8a65 100644 --- a/packages/eslint-plugin/src/rules/jsx-import.js +++ b/packages/eslint-plugin/src/rules/jsx-import.js @@ -1,4 +1,5 @@ const JSX_ANNOTATION_REGEX = /\*?\s*@jsx\s+([^\s]+)/ +const JSX_IMPORT_SOURCE_REGEX = /\*?\s*@jsxImportSource\s+([^\s]+)/ // TODO: handling this case //
@@ -7,9 +8,87 @@ const JSX_ANNOTATION_REGEX = /\*?\s*@jsx\s+([^\s]+)/ export default { meta: { - fixable: 'code' + fixable: 'code', + schema: { + type: 'array', + items: { + oneOf: [ + { + type: 'string' + }, + { + type: 'object', + properties: { + runtime: { type: 'string' }, + importSource: { type: 'string' } + }, + required: ['runtime'], + additionalProperties: false + } + ] + }, + uniqueItems: true, + minItems: 0 + } }, create(context) { + const jsxRuntimeMode = context.options.find( + option => option && option.runtime === 'automatic' + ) + + if (jsxRuntimeMode) { + return { + JSXAttribute(node) { + if (node.name.name !== 'css') { + return + } + const importSource = + (jsxRuntimeMode || {}).importSource || '@emotion/react' + let jsxImportSourcePragmaNode + let jsxImportSourceMatch + let validJsxImportSource = false + let sourceCode = context.getSourceCode() + let pragma = sourceCode.getAllComments().find(node => { + if (JSX_IMPORT_SOURCE_REGEX.test(node.value)) { + jsxImportSourcePragmaNode = node + return true + } + }) + jsxImportSourceMatch = + pragma && pragma.value.match(JSX_IMPORT_SOURCE_REGEX) + if ( + jsxImportSourceMatch && + jsxImportSourceMatch[1] === importSource + ) { + validJsxImportSource = true + } + if (!jsxImportSourceMatch) { + context.report({ + node, + message: `The css prop can only be used if jsxImportSource is set to ${importSource}`, + fix(fixer) { + return fixer.insertTextBefore( + sourceCode.ast.body[0], + `/** @jsxImportSource ${importSource} */\n` + ) + } + }) + } else if (!validJsxImportSource && jsxImportSourcePragmaNode) { + context.report({ + node, + message: `The css prop can only be used if jsxImportSource is set to ${importSource}`, + fix(fixer) { + return fixer.replaceText( + jsxImportSourcePragmaNode, + `/** @jsxImportSource ${importSource} */` + ) + } + }) + } + } + } + } + return { JSXAttribute(node) { if (node.name.name !== 'css') { diff --git a/packages/eslint-plugin/test/rules/jsx-import.test.js b/packages/eslint-plugin/test/rules/jsx-import.test.js index 6e198e56ac..6a0fa0b793 100644 --- a/packages/eslint-plugin/test/rules/jsx-import.test.js +++ b/packages/eslint-plugin/test/rules/jsx-import.test.js @@ -29,6 +29,45 @@ ruleTester.run('emotion jsx', rule, { let ele =
` }, + { + options: [{ runtime: 'classic' }], + code: ` + // @jsx jsx + import { jsx } from '@emotion/react' + let ele =
+ ` + }, + { + options: [{ runtime: 'invalidRuntime' }], + code: ` + // @jsx jsx + import { jsx } from '@emotion/react' + let ele =
+ ` + }, + { + options: [{ runtime: 'automatic' }], + code: ` + /** @jsxImportSource @emotion/react */ + + let ele =
+ ` + }, + { + options: [{ runtime: 'automatic' }], + code: ` + // no css prop usage for test coverage + let ele =
+ ` + }, + { + options: [{ runtime: 'automatic', importSource: '@emotion/react' }], + code: ` + /** @jsxImportSource @emotion/react */ + + let ele =
+ ` + }, { code: ` @@ -52,6 +91,72 @@ let ele =
output: ` // @jsx jsx import { jsx } from '@emotion/react' +let ele =
+ `.trim() + }, + { + options: [{ runtime: 'automatic' }], + code: ` +let ele =
+ `.trim(), + errors: [ + { + message: + 'The css prop can only be used if jsxImportSource is set to @emotion/react' + } + ], + output: ` +/** @jsxImportSource @emotion/react */ +let ele =
+ `.trim() + }, + { + options: [{ runtime: 'automatic', importSource: '@iChenLei/react' }], + code: ` +let ele =
+ `.trim(), + errors: [ + { + message: + 'The css prop can only be used if jsxImportSource is set to @iChenLei/react' + } + ], + output: ` +/** @jsxImportSource @iChenLei/react */ +let ele =
+ `.trim() + }, + { + options: [{ runtime: 'automatic', importSource: '@iChenLei/react' }], + code: ` +/** @jsxImportSource invalid-react */ +let ele =
+ `.trim(), + errors: [ + { + message: + 'The css prop can only be used if jsxImportSource is set to @iChenLei/react' + } + ], + output: ` +/** @jsxImportSource @iChenLei/react */ +let ele =
+ `.trim() + }, + { + options: [{ runtime: 'classic' }], + code: ` +let ele =
+ `.trim(), + errors: [ + { + message: + 'The css prop can only be used if jsx from @emotion/react is imported and it is set as the jsx pragma' + } + ], + output: ` +/** @jsx jsx */ +import { jsx } from '@emotion/react' let ele =
`.trim() },