diff --git a/index.js b/index.js index d77c95d..01f5c58 100644 --- a/index.js +++ b/index.js @@ -8,6 +8,7 @@ const regex = /@(jsx|jsxFrag|jsxImportSource|jsxRuntime)\s+(\S+)/g * @typedef {import('estree-jsx').Comment} Comment * @typedef {import('estree-jsx').Expression} Expression * @typedef {import('estree-jsx').Pattern} Pattern + * @typedef {import('estree-jsx').ObjectExpression} ObjectExpression * @typedef {import('estree-jsx').Property} Property * @typedef {import('estree-jsx').ImportSpecifier} ImportSpecifier * @typedef {import('estree-jsx').SpreadElement} SpreadElement @@ -35,6 +36,8 @@ const regex = /@(jsx|jsxFrag|jsxImportSource|jsxRuntime)\s+(\S+)/g * @property {string} [importSource='react'] * @property {string} [pragma='React.createElement'] * @property {string} [pragmaFrag='React.Fragment'] + * @property {boolean} [development=false] + * @property {string} [filePath] */ /** @@ -55,7 +58,7 @@ export function buildJsx(tree, options = {}) { let automatic = options.runtime === 'automatic' /** @type {Annotations} */ const annotations = {} - /** @type {{fragment?: boolean, jsx?: boolean, jsxs?: boolean}} */ + /** @type {{fragment?: boolean, jsx?: boolean, jsxs?: boolean, jsxDEV?: boolean}} */ const imports = {} walk(tree, { @@ -139,6 +142,14 @@ export function buildJsx(tree, options = {}) { }) } + if (imports.jsxDEV) { + specifiers.push({ + type: 'ImportSpecifier', + imported: {type: 'Identifier', name: 'jsxDEV'}, + local: {type: 'Identifier', name: '_jsxDEV'} + }) + } + if (specifiers.length > 0) { node.body.unshift({ type: 'ImportDeclaration', @@ -148,7 +159,8 @@ export function buildJsx(tree, options = {}) { value: (annotations.jsxImportSource || options.importSource || - 'react') + '/jsx-runtime' + 'react') + + (options.development ? '/jsx-dev-runtime' : '/jsx-runtime') } }) } @@ -267,19 +279,21 @@ export function buildJsx(tree, options = {}) { ) } - if (automatic && children.length > 0) { - fields.push({ - type: 'Property', - key: {type: 'Identifier', name: 'children'}, - value: - children.length > 1 - ? {type: 'ArrayExpression', elements: children} - : children[0], - kind: 'init', - method: false, - shorthand: false, - computed: false - }) + if (automatic) { + if (children.length > 0) { + fields.push({ + type: 'Property', + key: {type: 'Identifier', name: 'children'}, + value: + children.length > 1 + ? {type: 'ArrayExpression', elements: children} + : children[0], + kind: 'init', + method: false, + shorthand: false, + computed: false + }) + } } else { parameters = children } @@ -310,19 +324,74 @@ export function buildJsx(tree, options = {}) { } if (automatic) { - if (children.length > 1) { + parameters.push(props || {type: 'ObjectExpression', properties: []}) + + if (key) { + parameters.push(key) + } else if (options.development) { + parameters.push({type: 'Identifier', name: 'undefined'}) + } + + const isStaticChildren = children.length > 1 + + if (options.development) { + imports.jsxDEV = true + callee = { + type: 'Identifier', + name: '_jsxDEV' + } + parameters.push({type: 'Literal', value: isStaticChildren}) + + /** @type {ObjectExpression} */ + const source = { + type: 'ObjectExpression', + properties: [ + { + type: 'Property', + method: false, + shorthand: false, + computed: false, + kind: 'init', + key: {type: 'Identifier', name: 'fileName'}, + value: { + type: 'Literal', + value: options.filePath || '' + } + } + ] + } + + if (node.loc) { + source.properties.push( + { + type: 'Property', + method: false, + shorthand: false, + computed: false, + kind: 'init', + key: {type: 'Identifier', name: 'lineNumber'}, + value: {type: 'Literal', value: node.loc.start.line} + }, + { + type: 'Property', + method: false, + shorthand: false, + computed: false, + kind: 'init', + key: {type: 'Identifier', name: 'columnNumber'}, + value: {type: 'Literal', value: node.loc.start.column + 1} + } + ) + } + + parameters.push(source, {type: 'ThisExpression'}) + } else if (isStaticChildren) { imports.jsxs = true callee = {type: 'Identifier', name: '_jsxs'} } else { imports.jsx = true callee = {type: 'Identifier', name: '_jsx'} } - - parameters.push(props || {type: 'ObjectExpression', properties: []}) - - if (key) { - parameters.push(key) - } } // Classic. else { diff --git a/readme.md b/readme.md index 5dedba7..c9c53c5 100644 --- a/readme.md +++ b/readme.md @@ -113,6 +113,19 @@ runtime is automatic (`string`, default: `'react'`). Comment: `@jsxImportSource theSource`. Note that `/jsx-runtime` is appended to this provided source. +##### `options.development` + +Add location info on where a component originated from (`boolean`, default: +`false`). +This helps debugging but adds a lot of code that you don’t want in production. +Only used in the automatic runtime. + +###### `options.filePath` + +File path to the original source file (`string`, example: `'path/to/file.js'`). +Used in the location info when using the automatic runtime with +`development: true`. + ###### `options.pragma` Identifier or member expression to call when the effective runtime is classic diff --git a/test.js b/test.js index e90400b..55f1f9e 100644 --- a/test.js +++ b/test.js @@ -1232,6 +1232,245 @@ test('estree-util-build-jsx', (t) => { 'should support the automatic runtime (key, no props, no children)' ) + t.deepEqual( + generate( + buildJsx(parse('<>a', false), { + runtime: 'automatic', + development: true, + filePath: 'index.js' + }) + ), + [ + 'import {Fragment as _Fragment, jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', + '_jsxDEV(_Fragment, {', + ' children: "a"', + '}, undefined, false, {', + ' fileName: "index.js",', + ' lineNumber: 1,', + ' columnNumber: 1', + '}, this);', + '' + ].join('\n'), + 'should support the automatic runtime (fragment, jsx, settings, development)' + ) + + t.deepEqual( + generate( + buildJsx(parse('b{1}', false), { + runtime: 'automatic', + development: true, + filePath: 'index.js' + }) + ), + [ + 'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', + '_jsxDEV("a", {', + ' children: ["b", 1]', + '}, "a", true, {', + ' fileName: "index.js",', + ' lineNumber: 1,', + ' columnNumber: 1', + '}, this);', + '' + ].join('\n'), + 'should support the automatic runtime (jsxs, key, comment, development)' + ) + + t.deepEqual( + generate( + buildJsx(parse('d', false), { + runtime: 'automatic', + development: true, + filePath: 'index.js' + }) + ), + [ + 'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', + '_jsxDEV("a", Object.assign({', + ' b: "1"', + '}, c, {', + ' children: "d"', + '}), undefined, false, {', + ' fileName: "index.js",', + ' lineNumber: 1,', + ' columnNumber: 1', + '}, this);', + '' + ].join('\n'), + 'should support the automatic runtime (props, spread, children, development)' + ) + + t.deepEqual( + generate( + buildJsx(parse('f', false), { + runtime: 'automatic', + development: true, + filePath: 'index.js' + }) + ), + [ + 'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', + '_jsxDEV("a", Object.assign({', + ' b: 1,', + ' c: 2', + '}, {', + ' d: "e",', + ' children: "f"', + '}), undefined, false, {', + ' fileName: "index.js",', + ' lineNumber: 1,', + ' columnNumber: 1', + '}, this);', + '' + ].join('\n'), + 'should support the automatic runtime (spread, props, children, development)' + ) + + t.deepEqual( + generate( + buildJsx(parse('b', false), { + runtime: 'automatic', + development: true, + filePath: 'index.js' + }) + ), + [ + 'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', + '_jsxDEV("a", {', + ' children: "b"', + '}, undefined, false, {', + ' fileName: "index.js",', + ' lineNumber: 1,', + ' columnNumber: 1', + '}, this);', + '' + ].join('\n'), + 'should support the automatic runtime (no props, children, development)' + ) + + t.deepEqual( + generate( + buildJsx(parse('', false), { + runtime: 'automatic', + development: true, + filePath: 'index.js' + }) + ), + [ + 'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', + '_jsxDEV("a", {}, undefined, false, {', + ' fileName: "index.js",', + ' lineNumber: 1,', + ' columnNumber: 1', + '}, this);', + '' + ].join('\n'), + 'should support the automatic runtime (no props, no children, development)' + ) + + t.deepEqual( + generate( + buildJsx(parse('', false), { + runtime: 'automatic', + development: true, + filePath: 'index.js' + }) + ), + [ + 'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', + '_jsxDEV("a", {}, true, false, {', + ' fileName: "index.js",', + ' lineNumber: 1,', + ' columnNumber: 1', + '}, this);', + '' + ].join('\n'), + 'should support the automatic runtime (key, no props, no children, development)' + ) + + t.deepEqual( + generate( + buildJsx(parse('', false), { + runtime: 'automatic', + development: true + }) + ), + [ + 'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', + '_jsxDEV("a", {}, undefined, false, {', + ' fileName: "",', + ' lineNumber: 1,', + ' columnNumber: 1', + '}, this);', + '' + ].join('\n'), + 'should support the automatic runtime (no props, no children, development, no filePath)' + ) + + t.deepEqual( + generate( + buildJsx(parse('', false), { + runtime: 'automatic', + development: true, + filePath: '' + }) + ), + [ + 'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', + '_jsxDEV("a", {}, undefined, false, {', + ' fileName: "",', + ' lineNumber: 1,', + ' columnNumber: 1', + '}, this);', + '' + ].join('\n'), + 'should support the automatic runtime (no props, no children, development, empty filePath)' + ) + + t.deepEqual( + generate( + buildJsx(parse(''), { + runtime: 'automatic', + development: true, + filePath: 'index.js' + }) + ), + [ + 'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', + '_jsxDEV("a", {}, undefined, false, {', + ' fileName: "index.js"', + '}, this);', + '' + ].join('\n'), + 'should support the automatic runtime (no props, no children, development, no locations)' + ) + + t.deepEqual( + generate( + buildJsx(parse('\n \n', false), { + runtime: 'automatic', + development: true, + filePath: 'index.js' + }) + ), + [ + 'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', + '_jsxDEV("a", {', + ' children: _jsxDEV("b", {}, undefined, false, {', + ' fileName: "index.js",', + ' lineNumber: 2,', + ' columnNumber: 3', + ' }, this)', + '}, undefined, false, {', + ' fileName: "index.js",', + ' lineNumber: 1,', + ' columnNumber: 1', + '}, this);', + '' + ].join('\n'), + 'should support the automatic runtime (no props, nested children, development, positional info)' + ) + t.throws( () => { buildJsx(parse(''), {runtime: 'automatic'})