├── .eslintignore ├── .prettierignore ├── tests ├── setupTests.js └── smoke │ ├── .gitignore │ ├── run.js │ ├── smoke.js │ └── prepare.js ├── renovate.json ├── .gitignore ├── commitlint.config.js ├── .babelrc ├── src ├── formatter │ ├── spacer.js │ ├── formatTree.js │ ├── createPropFilter.js │ ├── formatTree.spec.js │ ├── spacer.spec.js │ ├── sortPropsByNames.spec.js │ ├── formatFunction.js │ ├── sortPropsByNames.js │ ├── createPropFilter.spec.js │ ├── mergeSiblingPlainStringChildrenReducer.js │ ├── sortObject.js │ ├── formatTreeNode.js │ ├── formatComplexDataStructure.js │ ├── formatFunction.spec.js │ ├── formatProp.js │ ├── formatTreeNode.spec.js │ ├── formatReactFragmentNode.js │ ├── formatPropValue.js │ ├── sortObject.spec.js │ ├── mergeSiblingPlainStringChildrenReducer.spec.js │ ├── formatReactFragmentNode.spec.js │ ├── formatReactElementNode.spec.js │ ├── formatComplexDataStructure.spec.js │ ├── formatPropValue.spec.js │ ├── formatReactElementNode.js │ └── formatProp.spec.js ├── AnonymousStatelessComponent.js ├── options.js ├── index.js ├── tree.spec.js ├── tree.js ├── parser │ ├── parseReactElement.js │ └── parseReactElement.spec.js └── index.spec.js ├── .npmignore ├── .flowconfig ├── .prettierrc ├── .eslintrc.js ├── .vscode └── launch.json ├── release.sh ├── .github └── workflows │ └── continuous-integration.yaml ├── index.d.ts ├── rollup.config.js ├── LICENSE ├── package.json ├── README.md └── CHANGELOG.md /.eslintignore: -------------------------------------------------------------------------------- 1 | **/dist/**/* 2 | **/node_modules/**/* 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/dist/**/* 2 | **/node_modules/**/* 3 | -------------------------------------------------------------------------------- /tests/setupTests.js: -------------------------------------------------------------------------------- 1 | import '@testing-library/jest-dom'; 2 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["config:js-app", "algolia"] 3 | } 4 | -------------------------------------------------------------------------------- /tests/smoke/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package.json 3 | yarn.lock 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | index-dist.js 2 | node_modules/ 3 | dist/ 4 | npm-debug.log 5 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@commitlint/config-angular'], 3 | }; 4 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env", "@babel/preset-react", "@babel/preset-flow"] 3 | } 4 | -------------------------------------------------------------------------------- /src/formatter/spacer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export default (times: number, tabStop: number): string => { 4 | if (times === 0) { 5 | return ''; 6 | } 7 | 8 | return new Array(times * tabStop).fill(' ').join(''); 9 | }; 10 | -------------------------------------------------------------------------------- /src/AnonymousStatelessComponent.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | // eslint-disable-next-line react/display-name 4 | export default function(props) { 5 | const { children } = props; // eslint-disable-line react/prop-types 6 | return
{children}
; 7 | } 8 | -------------------------------------------------------------------------------- /src/formatter/formatTree.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import formatTreeNode from './formatTreeNode'; 4 | import type { Options } from './../options'; 5 | import type { TreeNode } from './../tree'; 6 | 7 | export default (node: TreeNode, options: Options): string => 8 | formatTreeNode(node, false, 0, options); 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | **/*.spec.js 2 | **/*.spec.js.flow 3 | 4 | .github/ 5 | !dist/ 6 | src/ 7 | tests/ 8 | 9 | .babelrc 10 | .eslintignore 11 | .eslintrc.js 12 | .flowconfig 13 | .travis.yml 14 | .prettier 15 | .prettierignore 16 | commitlint.config.js 17 | release.sh 18 | renovate.json 19 | rollup.config.js 20 | -------------------------------------------------------------------------------- /src/formatter/createPropFilter.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export default function createPropFilter( 4 | props: {}, 5 | filter: string[] | ((any, string) => boolean) 6 | ) { 7 | if (Array.isArray(filter)) { 8 | return (key: string) => filter.indexOf(key) === -1; 9 | } else { 10 | return (key: string) => filter(props[key], key); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | /node_modules/.*/test/.* 3 | /node_modules/.*.spec\.js 4 | /node_modules/editions/es2015/index.js 5 | /dist/.* 6 | # FIXME: do not ignore local tests 7 | /.*\.spec\.js 8 | 9 | [include] 10 | 11 | [libs] 12 | 13 | [options] 14 | emoji=true 15 | #experimental.const_params=true 16 | munge_underscores=true 17 | 18 | [lints] 19 | -------------------------------------------------------------------------------- /src/options.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import * as React from 'react'; 4 | 5 | export type Options = {| 6 | filterProps: string[], 7 | showDefaultProps: boolean, 8 | showFunctions: boolean, 9 | functionValue: Function, 10 | tabStop: number, 11 | useBooleanShorthandSyntax: boolean, 12 | useFragmentShortSyntax: boolean, 13 | sortProps: boolean, 14 | 15 | maxInlineAttributesLineLength?: number, 16 | displayName?: (element: React.Element<*>) => string, 17 | |}; 18 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "flow", 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "overrides": [ 6 | { 7 | "files": ".prettierrc", 8 | "options": { "parser": "json" } 9 | }, 10 | { 11 | "files": ".babelrc", 12 | "options": { "parser": "json" } 13 | }, 14 | { 15 | "files": "*.json", 16 | "options": { "parser": "json" } 17 | }, 18 | { 19 | "files": "package.json", 20 | "options": { "printWidth": 999 } 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['algolia', 'algolia/jest', 'algolia/react'], 3 | 4 | env: { 5 | es6: true, 6 | }, 7 | 8 | settings: { 9 | react: { 10 | version: 'detect', 11 | }, 12 | }, 13 | 14 | rules: { 15 | 'max-params': ['error', 10], 16 | 'no-warning-comments': 'error', 17 | 18 | 'import/no-commonjs': 'off', 19 | }, 20 | 21 | overrides: [ 22 | { 23 | files: ['*.spec.js'], 24 | env: { 25 | jest: true, 26 | }, 27 | }, 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Jest Current File", 8 | "program": "${workspaceFolder}/node_modules/.bin/jest", 9 | "args": ["${fileBasenameNoExtension}"], 10 | "console": "integratedTerminal", 11 | "internalConsoleOptions": "neverOpen", 12 | "disableOptimisticBPs": true, 13 | "windows": { 14 | "program": "${workspaceFolder}/node_modules/jest/bin/jest" 15 | } 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/formatter/formatTree.spec.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import formatTree from './formatTree'; 4 | import formatTreeNode from './formatTreeNode'; 5 | 6 | jest.mock('./formatTreeNode', () => jest.fn(() => '')); 7 | 8 | describe('formatTree', () => { 9 | it('should format the node as a root node', () => { 10 | const tree = {}; 11 | const options = {}; 12 | 13 | const result = formatTree(tree, options); 14 | 15 | expect(formatTreeNode).toHaveBeenCalledWith(tree, false, 0, options); 16 | 17 | expect(result).toBe(''); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/formatter/spacer.spec.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import spacer from './spacer'; 4 | 5 | describe('spacer', () => { 6 | it('should generate a spaced string', () => { 7 | expect(spacer(0, 1)).toEqual(''); 8 | expect(spacer(0, 2)).toEqual(''); 9 | expect(spacer(0, 3)).toEqual(''); 10 | 11 | expect(spacer(1, 1)).toEqual(' '); 12 | expect(spacer(1, 2)).toEqual(' '); 13 | expect(spacer(1, 3)).toEqual(' '); 14 | 15 | expect(spacer(2, 1)).toEqual(' '); 16 | expect(spacer(2, 2)).toEqual(' '); 17 | expect(spacer(2, 3)).toEqual(' '); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e # exit when error 4 | 5 | currentBranch=`git rev-parse --abbrev-ref HEAD` 6 | if [ $currentBranch != 'master' ]; then 7 | printf "Release: You must be on master\n" 8 | exit 1 9 | fi 10 | 11 | if [[ $# -eq 0 ]] ; then 12 | printf "Release: use ``yarn release [major|minor|patch|x.x.x]``\n" 13 | exit 1 14 | fi 15 | 16 | yarn run mversion $1 17 | yarn run conventional-changelog --infile CHANGELOG.md --same-file --preset angular 18 | yarn run doctoc README.md 19 | git commit -am "$(json -f package.json version)" 20 | git tag v`json -f package.json version` 21 | git push origin master 22 | git push --tags origin master 23 | npm publish 24 | -------------------------------------------------------------------------------- /tests/smoke/run.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const execFileSync = require('child_process').execFileSync; 4 | const path = require('path'); 5 | 6 | const buildType = process.argv[2]; 7 | if (!buildType) { 8 | throw new Error('The build type to test is missing'); 9 | } 10 | 11 | const requestedReactVersion = process.argv[3]; 12 | if (!requestedReactVersion) { 13 | throw new Error('React version to use for the test is missing'); 14 | } 15 | 16 | execFileSync(path.join(__dirname, 'prepare.js'), [requestedReactVersion], { 17 | cwd: __dirname, 18 | stdio: 'inherit', 19 | }); 20 | 21 | execFileSync(path.join(__dirname, 'smoke.js'), [buildType], { 22 | cwd: __dirname, 23 | stdio: 'inherit', 24 | }); 25 | -------------------------------------------------------------------------------- /src/formatter/sortPropsByNames.spec.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import sortPropsByNames from './sortPropsByNames'; 4 | 5 | test('sortPropsByNames should always move the `key` and `ref` keys first', () => { 6 | const fixtures = ['c', 'key', 'a', 'ref', 'b']; 7 | 8 | expect(sortPropsByNames(false)(fixtures)).toEqual([ 9 | 'key', 10 | 'ref', 11 | 'c', 12 | 'a', 13 | 'b', 14 | ]); 15 | }); 16 | 17 | test('sortPropsByNames should always sort the props and keep `key` and `ref` keys first', () => { 18 | const fixtures = ['c', 'key', 'a', 'ref', 'b']; 19 | 20 | expect(sortPropsByNames(true)(fixtures)).toEqual([ 21 | 'key', 22 | 'ref', 23 | 'a', 24 | 'b', 25 | 'c', 26 | ]); 27 | }); 28 | -------------------------------------------------------------------------------- /src/formatter/formatFunction.js: -------------------------------------------------------------------------------- 1 | import type { Options } from './../options'; 2 | 3 | function noRefCheck() {} 4 | 5 | export const inlineFunction = (fn: any): string => 6 | fn 7 | .toString() 8 | .split('\n') 9 | .map(line => line.trim()) 10 | .join(''); 11 | 12 | export const preserveFunctionLineBreak = (fn: any): string => fn.toString(); 13 | 14 | const defaultFunctionValue = inlineFunction; 15 | 16 | export default (fn: Function, options: Options): string => { 17 | const { functionValue = defaultFunctionValue, showFunctions } = options; 18 | if (!showFunctions && functionValue === defaultFunctionValue) { 19 | return functionValue(noRefCheck); 20 | } 21 | 22 | return functionValue(fn); 23 | }; 24 | -------------------------------------------------------------------------------- /.github/workflows/continuous-integration.yaml: -------------------------------------------------------------------------------- 1 | on: pull_request 2 | 3 | jobs: 4 | continuous-integration: 5 | name: Continuous Integration 6 | runs-on: ubuntu-latest 7 | steps: 8 | - uses: actions/checkout@v4 9 | - uses: actions/setup-node@v4 10 | with: 11 | node-version: '20.19.6' 12 | cache: 'yarn' 13 | - run: yarn install --frozen-lockfile 14 | - run: yarn run test 15 | - run: yarn run lint 16 | - run: yarn run flow 17 | - run: yarn run smoke cjs 19.0.0 18 | - run: yarn run smoke esm 19.0.0 19 | - run: yarn run smoke cjs latest 20 | - run: yarn run smoke esm latest 21 | - run: yarn run smoke cjs next 22 | - run: yarn run smoke esm next 23 | 24 | -------------------------------------------------------------------------------- /src/formatter/sortPropsByNames.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | const isKeyOrRefProps = (propName: string) => ['key', 'ref'].includes(propName); 4 | 5 | export default (shouldSortUserProps: boolean) => ( 6 | props: string[] 7 | ): string[] => { 8 | const haveKeyProp = props.includes('key'); 9 | const haveRefProp = props.includes('ref'); 10 | 11 | const userPropsOnly = props.filter(oneProp => !isKeyOrRefProps(oneProp)); 12 | 13 | const sortedProps = shouldSortUserProps 14 | ? [...userPropsOnly.sort()] // We use basic lexical order 15 | : [...userPropsOnly]; 16 | 17 | if (haveRefProp) { 18 | sortedProps.unshift('ref'); 19 | } 20 | 21 | if (haveKeyProp) { 22 | sortedProps.unshift('key'); 23 | } 24 | 25 | return sortedProps; 26 | }; 27 | -------------------------------------------------------------------------------- /src/formatter/createPropFilter.spec.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import createPropFilter from './createPropFilter'; 4 | 5 | describe('createPropFilter', () => { 6 | it('should filter based on an array of keys', () => { 7 | const props = { a: 1, b: 2, c: 3 }; 8 | const filter = createPropFilter(props, ['b']); 9 | 10 | const filteredPropKeys = Object.keys(props).filter(filter); 11 | 12 | expect(filteredPropKeys).toEqual(['a', 'c']); 13 | }); 14 | 15 | it('should filter based on a callback', () => { 16 | const props = { a: 1, b: 2, c: 3 }; 17 | const filter = createPropFilter( 18 | props, 19 | (val, key) => key !== 'b' && val < 3 20 | ); 21 | 22 | const filteredPropKeys = Object.keys(props).filter(filter); 23 | 24 | expect(filteredPropKeys).toEqual(['a']); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'react-element-to-jsx-string' { 2 | import { ReactNode } from 'react'; 3 | 4 | type FilterPropsFunction = (value: any, key: string) => boolean 5 | 6 | interface ReactElementToJSXStringOptions { 7 | displayName?: (element: ReactNode) => string; 8 | filterProps?: string[] | FilterPropsFunction; 9 | showDefaultProps?: boolean; 10 | showFunctions?: boolean; 11 | functionValue?: (fn: any) => any; 12 | tabStop?: number; 13 | useBooleanShorthandSyntax?: boolean; 14 | maxInlineAttributesLineLength?: number; 15 | sortProps?: boolean; 16 | useFragmentShortSyntax?: boolean; 17 | } 18 | 19 | const reactElementToJSXString: (element: ReactNode, options?: ReactElementToJSXStringOptions) => string; 20 | 21 | export { ReactElementToJSXStringOptions as Options }; 22 | 23 | export default reactElementToJSXString; 24 | } 25 | -------------------------------------------------------------------------------- /src/formatter/mergeSiblingPlainStringChildrenReducer.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { createStringTreeNode } from './../tree'; 4 | import type { TreeNode } from './../tree'; 5 | 6 | export default ( 7 | previousNodes: TreeNode[], 8 | currentNode: TreeNode 9 | ): TreeNode[] => { 10 | const nodes = previousNodes.slice( 11 | 0, 12 | previousNodes.length > 0 ? previousNodes.length - 1 : 0 13 | ); 14 | const previousNode = previousNodes[previousNodes.length - 1]; 15 | 16 | if ( 17 | previousNode && 18 | (currentNode.type === 'string' || currentNode.type === 'number') && 19 | (previousNode.type === 'string' || previousNode.type === 'number') 20 | ) { 21 | nodes.push( 22 | createStringTreeNode( 23 | String(previousNode.value) + String(currentNode.value) 24 | ) 25 | ); 26 | } else { 27 | if (previousNode) { 28 | nodes.push(previousNode); 29 | } 30 | 31 | nodes.push(currentNode); 32 | } 33 | 34 | return nodes; 35 | }; 36 | -------------------------------------------------------------------------------- /tests/smoke/smoke.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* eslint-disable no-console, import/no-extraneous-dependencies, no-global-assign */ 4 | 5 | require = require('esm')(module); 6 | 7 | const requireReactElementToJsxString = buildType => { 8 | if (buildType === 'esm') { 9 | return require(`./../../dist/esm`).default; 10 | } else if (buildType === 'cjs') { 11 | return require('./../../dist/cjs').default; 12 | } 13 | 14 | throw new Error(`Unknown build type: "${buildType}"`); 15 | }; 16 | 17 | const expect = require('expect'); 18 | const React = require('react'); 19 | const reactElementToJsxString = requireReactElementToJsxString(process.argv[2]); 20 | 21 | console.log(`Tested "react" version: "${React.version}"`); 22 | 23 | const tree = React.createElement( 24 | 'div', 25 | { foo: 51 }, 26 | React.createElement('h1', {}, 'Hello world') 27 | ); 28 | 29 | expect(reactElementToJsxString(tree)).toEqual( 30 | `
31 |

32 | Hello world 33 |

34 |
` 35 | ); 36 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import babel from 'rollup-plugin-babel'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | import builtins from 'rollup-plugin-node-builtins'; 4 | import globals from 'rollup-plugin-node-globals'; 5 | import pkg from './package.json'; 6 | 7 | const extractExternals = () => [ 8 | ...Object.keys(pkg.dependencies || {}), 9 | ...Object.keys(pkg.peerDependencies || {}), 10 | ]; 11 | 12 | export default { 13 | input: 'src/index.js', 14 | output: [ 15 | { 16 | file: pkg.main, 17 | format: 'cjs', 18 | sourcemap: true, 19 | }, 20 | { 21 | file: pkg.module, 22 | format: 'es', 23 | sourcemap: true, 24 | }, 25 | ], 26 | external: extractExternals(), 27 | plugins: [ 28 | babel({ 29 | babelrc: false, 30 | exclude: 'node_modules/**', 31 | presets: [ 32 | '@babel/preset-env', 33 | '@babel/preset-react', 34 | '@babel/preset-flow', 35 | ], 36 | }), 37 | resolve({ 38 | mainFields: ['module', 'main', 'jsnext', 'browser'], 39 | }), 40 | globals(), 41 | builtins(), 42 | ], 43 | }; 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Algolia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import formatTree from './formatter/formatTree'; 4 | import parseReactElement from './parser/parseReactElement'; 5 | import type { Element as ReactElement } from 'react'; 6 | import type { Options } from './options'; 7 | 8 | const reactElementToJsxString = ( 9 | element: ReactElement, 10 | { 11 | filterProps = [], 12 | showDefaultProps = true, 13 | showFunctions = false, 14 | functionValue, 15 | tabStop = 2, 16 | useBooleanShorthandSyntax = true, 17 | useFragmentShortSyntax = true, 18 | sortProps = true, 19 | maxInlineAttributesLineLength, 20 | displayName, 21 | }: Options = {} 22 | ) => { 23 | if (!element) { 24 | throw new Error('react-element-to-jsx-string: Expected a ReactElement'); 25 | } 26 | 27 | const options = { 28 | filterProps, 29 | showDefaultProps, 30 | showFunctions, 31 | functionValue, 32 | tabStop, 33 | useBooleanShorthandSyntax, 34 | useFragmentShortSyntax, 35 | sortProps, 36 | maxInlineAttributesLineLength, 37 | displayName, 38 | }; 39 | 40 | return formatTree(parseReactElement(element, options), options); 41 | }; 42 | 43 | export default reactElementToJsxString; 44 | 45 | export { 46 | inlineFunction, 47 | preserveFunctionLineBreak, 48 | } from './formatter/formatFunction'; 49 | -------------------------------------------------------------------------------- /src/tree.spec.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { 4 | createStringTreeNode, 5 | createNumberTreeNode, 6 | createReactElementTreeNode, 7 | createReactFragmentTreeNode, 8 | } from './tree'; 9 | 10 | describe('createStringTreeNode', () => { 11 | it('generate a string typed node payload', () => { 12 | expect(createStringTreeNode('foo')).toEqual({ 13 | type: 'string', 14 | value: 'foo', 15 | }); 16 | }); 17 | }); 18 | 19 | describe('createNumberTreeNode', () => { 20 | it('generate a number typed node payload', () => { 21 | expect(createNumberTreeNode(42)).toEqual({ 22 | type: 'number', 23 | value: 42, 24 | }); 25 | }); 26 | }); 27 | 28 | describe('createReactElementTreeNode', () => { 29 | it('generate a react element typed node payload', () => { 30 | expect( 31 | createReactElementTreeNode('MyComponent', { foo: 42 }, { bar: 51 }, [ 32 | 'abc', 33 | ]) 34 | ).toEqual({ 35 | type: 'ReactElement', 36 | displayName: 'MyComponent', 37 | props: { foo: 42 }, 38 | defaultProps: { bar: 51 }, 39 | childrens: ['abc'], 40 | }); 41 | }); 42 | }); 43 | 44 | describe('createReactFragmentTreeNode', () => { 45 | it('generate a react fragment typed node payload', () => { 46 | expect(createReactFragmentTreeNode('foo', ['abc'])).toEqual({ 47 | type: 'ReactFragment', 48 | key: 'foo', 49 | childrens: ['abc'], 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/formatter/sortObject.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | import * as React from 'react'; 3 | 4 | function safeSortObject(value: any, seen: WeakSet): any { 5 | // return non-object value as is 6 | if (value === null || typeof value !== 'object') { 7 | return value; 8 | } 9 | 10 | // return date and regexp values as is 11 | if (value instanceof Date || value instanceof RegExp) { 12 | return value; 13 | } 14 | 15 | // return react element as is but remove _owner key because it can lead to recursion 16 | if (React.isValidElement(value)) { 17 | const copyObj = { ...value }; 18 | delete copyObj._owner; 19 | return copyObj; 20 | } 21 | 22 | seen.add(value); 23 | 24 | // make a copy of array with each item passed through the sorting algorithm 25 | if (Array.isArray(value)) { 26 | return value.map(v => safeSortObject(v, seen)); 27 | } 28 | 29 | // make a copy of object with key sorted 30 | return Object.keys(value) 31 | .sort() 32 | .reduce((result, key) => { 33 | if (key === 'current' || seen.has(value[key])) { 34 | // eslint-disable-next-line no-param-reassign 35 | result[key] = '[Circular]'; 36 | } else { 37 | // eslint-disable-next-line no-param-reassign 38 | result[key] = safeSortObject(value[key], seen); 39 | } 40 | return result; 41 | }, {}); 42 | } 43 | 44 | export default function sortObject(value: any): any { 45 | return safeSortObject(value, new WeakSet()); 46 | } 47 | -------------------------------------------------------------------------------- /src/formatter/formatTreeNode.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import formatReactElementNode from './formatReactElementNode'; 4 | import formatReactFragmentNode from './formatReactFragmentNode'; 5 | import type { Options } from './../options'; 6 | import type { TreeNode } from './../tree'; 7 | 8 | const jsxStopChars = ['<', '>', '{', '}']; 9 | const shouldBeEscaped = (s: string) => 10 | jsxStopChars.some(jsxStopChar => s.includes(jsxStopChar)); 11 | 12 | const escape = (s: string) => { 13 | if (!shouldBeEscaped(s)) { 14 | return s; 15 | } 16 | 17 | return `{\`${s}\`}`; 18 | }; 19 | 20 | const preserveTrailingSpace = (s: string) => { 21 | let result = s; 22 | if (result.endsWith(' ')) { 23 | result = result.replace(/^(.*?)(\s+)$/, "$1{'$2'}"); 24 | } 25 | 26 | if (result.startsWith(' ')) { 27 | result = result.replace(/^(\s+)(.*)$/, "{'$1'}$2"); 28 | } 29 | 30 | return result; 31 | }; 32 | 33 | export default ( 34 | node: TreeNode, 35 | inline: boolean, 36 | lvl: number, 37 | options: Options 38 | ): string => { 39 | if (node.type === 'number') { 40 | return String(node.value); 41 | } 42 | 43 | if (node.type === 'string') { 44 | return node.value 45 | ? `${preserveTrailingSpace(escape(String(node.value)))}` 46 | : ''; 47 | } 48 | 49 | if (node.type === 'ReactElement') { 50 | return formatReactElementNode(node, inline, lvl, options); 51 | } 52 | 53 | if (node.type === 'ReactFragment') { 54 | return formatReactFragmentNode(node, inline, lvl, options); 55 | } 56 | 57 | throw new TypeError(`Unknow format type "${node.type}"`); 58 | }; 59 | -------------------------------------------------------------------------------- /src/tree.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | /* eslint-disable no-use-before-define */ 3 | 4 | import type { Key } from 'react'; 5 | 6 | type PropsType = { [key: string]: any }; 7 | type DefaultPropsType = { [key: string]: any }; 8 | 9 | export type StringTreeNode = {| 10 | type: 'string', 11 | value: string, 12 | |}; 13 | 14 | export type NumberTreeNode = {| 15 | type: 'number', 16 | value: number, 17 | |}; 18 | 19 | export type ReactElementTreeNode = {| 20 | type: 'ReactElement', 21 | displayName: string, 22 | props: PropsType, 23 | defaultProps: DefaultPropsType, 24 | childrens: TreeNode[], 25 | |}; 26 | 27 | export type ReactFragmentTreeNode = {| 28 | type: 'ReactFragment', 29 | key: ?Key, 30 | childrens: TreeNode[], 31 | |}; 32 | 33 | export type TreeNode = 34 | | StringTreeNode 35 | | NumberTreeNode 36 | | ReactElementTreeNode 37 | | ReactFragmentTreeNode; 38 | 39 | export const createStringTreeNode = (value: string): StringTreeNode => ({ 40 | type: 'string', 41 | value, 42 | }); 43 | 44 | export const createNumberTreeNode = (value: number): NumberTreeNode => ({ 45 | type: 'number', 46 | value, 47 | }); 48 | 49 | export const createReactElementTreeNode = ( 50 | displayName: string, 51 | props: PropsType, 52 | defaultProps: DefaultPropsType, 53 | childrens: TreeNode[] 54 | ): ReactElementTreeNode => ({ 55 | type: 'ReactElement', 56 | displayName, 57 | props, 58 | defaultProps, 59 | childrens, 60 | }); 61 | 62 | export const createReactFragmentTreeNode = ( 63 | key: ?Key, 64 | childrens: TreeNode[] 65 | ): ReactFragmentTreeNode => ({ 66 | type: 'ReactFragment', 67 | key, 68 | childrens, 69 | }); 70 | -------------------------------------------------------------------------------- /src/formatter/formatComplexDataStructure.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { isValidElement } from 'react'; 4 | import { prettyPrint } from '@base2/pretty-print-object'; 5 | import sortObject from './sortObject'; 6 | import parseReactElement from './../parser/parseReactElement'; 7 | import formatTreeNode from './formatTreeNode'; 8 | import formatFunction from './formatFunction'; 9 | import spacer from './spacer'; 10 | import type { Options } from './../options'; 11 | 12 | export default ( 13 | value: Object | Array, 14 | inline: boolean, 15 | lvl: number, 16 | options: Options 17 | ): string => { 18 | const normalizedValue = sortObject(value); 19 | 20 | const stringifiedValue = prettyPrint(normalizedValue, { 21 | transform: (currentObj, prop, originalResult) => { 22 | const currentValue = currentObj[prop]; 23 | 24 | if (currentValue && isValidElement(currentValue)) { 25 | return formatTreeNode( 26 | parseReactElement(currentValue, options), 27 | true, 28 | lvl, 29 | options 30 | ); 31 | } 32 | 33 | if (typeof currentValue === 'function') { 34 | return formatFunction(currentValue, options); 35 | } 36 | 37 | return originalResult; 38 | }, 39 | }); 40 | 41 | if (inline) { 42 | return stringifiedValue 43 | .replace(/\s+/g, ' ') 44 | .replace(/{ /g, '{') 45 | .replace(/ }/g, '}') 46 | .replace(/\[ /g, '[') 47 | .replace(/ ]/g, ']'); 48 | } 49 | 50 | // Replace tabs with spaces, and add necessary indentation in front of each new line 51 | return stringifiedValue 52 | .replace(/\t/g, spacer(1, options.tabStop)) 53 | .replace(/\n([^$])/g, `\n${spacer(lvl + 1, options.tabStop)}$1`); 54 | }; 55 | -------------------------------------------------------------------------------- /src/formatter/formatFunction.spec.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import formatFunction from './formatFunction'; 4 | 5 | jest.mock('./formatReactElementNode.js', () => node => 6 | `<${node.displayName} />` 7 | ); 8 | 9 | function hello() { 10 | return 1; 11 | } 12 | 13 | describe('formatFunction', () => { 14 | it('should replace a function with noRefCheck without showFunctions option', () => { 15 | expect(formatFunction(hello, {})).toEqual('function noRefCheck() {}'); 16 | }); 17 | 18 | it('should replace a function with noRefCheck if showFunctions is false', () => { 19 | expect(formatFunction(hello, { showFunctions: false })).toEqual( 20 | 'function noRefCheck() {}' 21 | ); 22 | }); 23 | 24 | it('should format a function if showFunctions is true', () => { 25 | expect(formatFunction(hello, { showFunctions: true })).toEqual( 26 | 'function hello() {return 1;}' 27 | ); 28 | }); 29 | 30 | it('should format a function without name if showFunctions is true', () => { 31 | expect(formatFunction(() => 1, { showFunctions: true })).toEqual( 32 | 'function () {return 1;}' 33 | ); 34 | }); 35 | 36 | it('should use the functionValue option', () => { 37 | expect(formatFunction(hello, { functionValue: () => '' })).toEqual( 38 | '' 39 | ); 40 | }); 41 | 42 | it('should use the functionValue option even if showFunctions is true', () => { 43 | expect( 44 | formatFunction(hello, { 45 | showFunctions: true, 46 | functionValue: () => '', 47 | }) 48 | ).toEqual(''); 49 | }); 50 | 51 | it('should use the functionValue option even if showFunctions is false', () => { 52 | expect( 53 | formatFunction(hello, { 54 | showFunctions: false, 55 | functionValue: () => '', 56 | }) 57 | ).toEqual(''); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /src/formatter/formatProp.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import spacer from './spacer'; 4 | import formatPropValue from './formatPropValue'; 5 | import type { Options } from './../options'; 6 | 7 | export default ( 8 | name: string, 9 | hasValue: boolean, 10 | value: any, 11 | hasDefaultValue: boolean, 12 | defaultValue: any, 13 | inline: boolean, 14 | lvl: number, 15 | options: Options 16 | ): { 17 | attributeFormattedInline: string, 18 | attributeFormattedMultiline: string, 19 | isMultilineAttribute: boolean, 20 | } => { 21 | if (!hasValue && !hasDefaultValue) { 22 | throw new Error( 23 | `The prop "${name}" has no value and no default: could not be formatted` 24 | ); 25 | } 26 | 27 | const usedValue = hasValue ? value : defaultValue; 28 | 29 | const { useBooleanShorthandSyntax, tabStop } = options; 30 | 31 | const formattedPropValue = formatPropValue(usedValue, inline, lvl, options); 32 | 33 | let attributeFormattedInline = ' '; 34 | let attributeFormattedMultiline = `\n${spacer(lvl + 1, tabStop)}`; 35 | const isMultilineAttribute = formattedPropValue.includes('\n'); 36 | 37 | if ( 38 | useBooleanShorthandSyntax && 39 | formattedPropValue === '{false}' && 40 | !hasDefaultValue 41 | ) { 42 | // If a boolean is false and not different from it's default, we do not render the attribute 43 | attributeFormattedInline = ''; 44 | attributeFormattedMultiline = ''; 45 | } else if (useBooleanShorthandSyntax && formattedPropValue === '{true}') { 46 | attributeFormattedInline += `${name}`; 47 | attributeFormattedMultiline += `${name}`; 48 | } else { 49 | attributeFormattedInline += `${name}=${formattedPropValue}`; 50 | attributeFormattedMultiline += `${name}=${formattedPropValue}`; 51 | } 52 | 53 | return { 54 | attributeFormattedInline, 55 | attributeFormattedMultiline, 56 | isMultilineAttribute, 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /src/formatter/formatTreeNode.spec.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import formatTreeNode from './formatTreeNode'; 4 | 5 | jest.mock('./formatReactElementNode', () => () => 6 | '' 7 | ); 8 | 9 | describe('formatTreeNode', () => { 10 | it('should format number tree node', () => { 11 | expect(formatTreeNode({ type: 'number', value: 42 }, true, 0, {})).toBe( 12 | '42' 13 | ); 14 | }); 15 | 16 | it('should format string tree node', () => { 17 | expect(formatTreeNode({ type: 'string', value: 'foo' }, true, 0, {})).toBe( 18 | 'foo' 19 | ); 20 | }); 21 | 22 | it('should format react element tree node', () => { 23 | expect( 24 | formatTreeNode( 25 | { 26 | type: 'ReactElement', 27 | displayName: 'Foo', 28 | }, 29 | true, 30 | 0, 31 | {} 32 | ) 33 | ).toBe(''); 34 | }); 35 | 36 | const jsxDelimiters = ['<', '>', '{', '}']; 37 | jsxDelimiters.forEach(char => { 38 | it(`should escape string that contains the JSX delimiter "${char}"`, () => { 39 | expect( 40 | formatTreeNode( 41 | { type: 'string', value: `I contain ${char}, is will be escaped` }, 42 | true, 43 | 0, 44 | {} 45 | ) 46 | ).toBe(`{\`I contain ${char}, is will be escaped\`}`); 47 | }); 48 | }); 49 | 50 | it('should preserve the format of string', () => { 51 | expect(formatTreeNode({ type: 'string', value: 'foo\nbar' }, true, 0, {})) 52 | .toBe(`foo 53 | bar`); 54 | 55 | expect( 56 | formatTreeNode( 57 | { 58 | type: 'string', 59 | value: JSON.stringify({ foo: 'bar' }, null, 2), 60 | }, 61 | false, 62 | 0, 63 | { 64 | tabStop: 2, 65 | } 66 | ) 67 | ).toBe(`{\`{ 68 | "foo": "bar" 69 | }\`}`); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /tests/smoke/prepare.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | /* eslint-disable no-console */ 4 | 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | const execSync = require('child_process').execSync; 8 | 9 | const requestedReactVersion = process.argv[2]; 10 | if (!requestedReactVersion) { 11 | throw new Error("React version is missing: '$ ./prepare 16.0.0'"); 12 | } 13 | 14 | const nodeModulesPath = path.join(__dirname, 'node_modules'); 15 | const packageJsonPath = path.join(__dirname, 'package.json'); 16 | 17 | const deleteExistingDependencies = () => () => { 18 | if (fs.existsSync(nodeModulesPath)) { 19 | execSync(`rm -r "${nodeModulesPath}"`, { 20 | cwd: __dirname, 21 | stdio: 'inherit', 22 | }); 23 | } 24 | 25 | if (fs.existsSync(packageJsonPath)) { 26 | execSync(`rm "${packageJsonPath}"`, { 27 | cwd: __dirname, 28 | stdio: 'inherit', 29 | }); 30 | } 31 | }; 32 | 33 | const preparePackageJson = reactVersion => () => { 34 | const packageJson = { 35 | name: 'smoke', 36 | version: '0.0.1', 37 | main: 'index.js', 38 | license: 'MIT', 39 | private: true, 40 | dependencies: { 41 | react: reactVersion, 42 | }, 43 | }; 44 | 45 | fs.writeFileSync( 46 | path.join(__dirname, 'package.json'), 47 | JSON.stringify(packageJson, null, 2) 48 | ); 49 | }; 50 | 51 | const installDependencies = () => () => 52 | new Promise(() => { 53 | if (!fs.existsSync(packageJsonPath)) { 54 | return; 55 | } 56 | 57 | execSync('yarn install --no-lockfile', { 58 | cwd: __dirname, 59 | stdio: 'inherit', 60 | }); 61 | }); 62 | 63 | Promise.resolve() 64 | .then(() => 65 | console.log(`Requested "react" version: "${requestedReactVersion}"`) 66 | ) 67 | .then(deleteExistingDependencies()) 68 | .then(preparePackageJson(requestedReactVersion)) 69 | .then(installDependencies()) 70 | .catch(err => console.error(err)); 71 | -------------------------------------------------------------------------------- /src/formatter/formatReactFragmentNode.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type { Key } from 'react'; 4 | import formatReactElementNode from './formatReactElementNode'; 5 | import type { Options } from './../options'; 6 | import type { 7 | ReactElementTreeNode, 8 | ReactFragmentTreeNode, 9 | TreeNode, 10 | } from './../tree'; 11 | 12 | const REACT_FRAGMENT_TAG_NAME_SHORT_SYNTAX = ''; 13 | const REACT_FRAGMENT_TAG_NAME_EXPLICIT_SYNTAX = 'React.Fragment'; 14 | 15 | const toReactElementTreeNode = ( 16 | displayName: string, 17 | key: ?Key, 18 | childrens: TreeNode[] 19 | ): ReactElementTreeNode => { 20 | let props = {}; 21 | if (key) { 22 | props = { key }; 23 | } 24 | 25 | return { 26 | type: 'ReactElement', 27 | displayName, 28 | props, 29 | defaultProps: {}, 30 | childrens, 31 | }; 32 | }; 33 | 34 | const isKeyedFragment = ({ key }: ReactFragmentTreeNode) => Boolean(key); 35 | const hasNoChildren = ({ childrens }: ReactFragmentTreeNode) => 36 | childrens.length === 0; 37 | 38 | export default ( 39 | node: ReactFragmentTreeNode, 40 | inline: boolean, 41 | lvl: number, 42 | options: Options 43 | ): string => { 44 | const { type, key, childrens } = node; 45 | 46 | if (type !== 'ReactFragment') { 47 | throw new Error( 48 | `The "formatReactFragmentNode" function could only format node of type "ReactFragment". Given: ${type}` 49 | ); 50 | } 51 | 52 | const { useFragmentShortSyntax } = options; 53 | 54 | let displayName; 55 | if (useFragmentShortSyntax) { 56 | if (hasNoChildren(node) || isKeyedFragment(node)) { 57 | displayName = REACT_FRAGMENT_TAG_NAME_EXPLICIT_SYNTAX; 58 | } else { 59 | displayName = REACT_FRAGMENT_TAG_NAME_SHORT_SYNTAX; 60 | } 61 | } else { 62 | displayName = REACT_FRAGMENT_TAG_NAME_EXPLICIT_SYNTAX; 63 | } 64 | 65 | return formatReactElementNode( 66 | toReactElementTreeNode(displayName, key, childrens), 67 | inline, 68 | lvl, 69 | options 70 | ); 71 | }; 72 | -------------------------------------------------------------------------------- /src/formatter/formatPropValue.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { isPlainObject } from 'is-plain-object'; 4 | import { isValidElement } from 'react'; 5 | import formatComplexDataStructure from './formatComplexDataStructure'; 6 | import formatFunction from './formatFunction'; 7 | import formatTreeNode from './formatTreeNode'; 8 | import type { Options } from './../options'; 9 | import parseReactElement from './../parser/parseReactElement'; 10 | 11 | const escape = (s: string): string => s.replace(/"/g, '"'); 12 | 13 | const formatPropValue = ( 14 | propValue: any, 15 | inline: boolean, 16 | lvl: number, 17 | options: Options 18 | ): string => { 19 | if (typeof propValue === 'number') { 20 | return `{${String(propValue)}}`; 21 | } 22 | 23 | if (typeof propValue === 'string') { 24 | return `"${escape(propValue)}"`; 25 | } 26 | 27 | // > "Symbols (new in ECMAScript 2015, not yet supported in Flow)" 28 | // @see: https://flow.org/en/docs/types/primitives/ 29 | // $FlowFixMe: Flow does not support Symbol 30 | if (typeof propValue === 'symbol') { 31 | const symbolDescription = propValue 32 | .valueOf() 33 | .toString() 34 | .replace(/Symbol\((.*)\)/, '$1'); 35 | 36 | if (!symbolDescription) { 37 | return `{Symbol()}`; 38 | } 39 | 40 | return `{Symbol('${symbolDescription}')}`; 41 | } 42 | 43 | if (typeof propValue === 'function') { 44 | return `{${formatFunction(propValue, options)}}`; 45 | } 46 | 47 | if (isValidElement(propValue)) { 48 | return `{${formatTreeNode( 49 | parseReactElement(propValue, options), 50 | true, 51 | lvl, 52 | options 53 | )}}`; 54 | } 55 | 56 | if (propValue instanceof Date) { 57 | if (isNaN(propValue.valueOf())) { 58 | return `{new Date(NaN)}`; 59 | } 60 | return `{new Date("${propValue.toISOString()}")}`; 61 | } 62 | 63 | if (isPlainObject(propValue) || Array.isArray(propValue)) { 64 | return `{${formatComplexDataStructure(propValue, inline, lvl, options)}}`; 65 | } 66 | 67 | return `{${String(propValue)}}`; 68 | }; 69 | 70 | export default formatPropValue; 71 | -------------------------------------------------------------------------------- /src/formatter/sortObject.spec.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import sortObject from './sortObject'; 5 | 6 | describe('sortObject', () => { 7 | it('should sort keys in objects', () => { 8 | const fixture = { 9 | c: 2, 10 | b: { x: 1, c: 'ccc' }, 11 | a: [{ foo: 1, bar: 2 }], 12 | }; 13 | 14 | expect(JSON.stringify(sortObject(fixture))).toEqual( 15 | JSON.stringify({ 16 | a: [{ bar: 2, foo: 1 }], 17 | b: { c: 'ccc', x: 1 }, 18 | c: 2, 19 | }) 20 | ); 21 | }); 22 | 23 | it('should process an array', () => { 24 | const fixture = [{ foo: 1, bar: 2 }, null, { b: 1, c: 2, a: 3 }]; 25 | 26 | expect(JSON.stringify(sortObject(fixture))).toEqual( 27 | JSON.stringify([{ bar: 2, foo: 1 }, null, { a: 3, b: 1, c: 2 }]) 28 | ); 29 | }); 30 | 31 | it('should not break special values', () => { 32 | const date = new Date(); 33 | const regexp = /test/g; 34 | const fixture = { 35 | a: [date, regexp], 36 | b: regexp, 37 | c: date, 38 | }; 39 | 40 | expect(sortObject(fixture)).toEqual({ 41 | a: [date, regexp], 42 | b: regexp, 43 | c: date, 44 | }); 45 | }); 46 | 47 | describe('_owner key', () => { 48 | it('should preserve the _owner key for objects that are not react elements', () => { 49 | const fixture = { 50 | _owner: "_owner that doesn't belong to react element", 51 | foo: 'bar', 52 | }; 53 | 54 | expect(JSON.stringify(sortObject(fixture))).toEqual( 55 | JSON.stringify({ 56 | _owner: "_owner that doesn't belong to react element", 57 | foo: 'bar', 58 | }) 59 | ); 60 | }); 61 | 62 | it('should remove the _owner key from top level react element', () => { 63 | const fixture = { 64 | reactElement: ( 65 |
66 | 67 |
68 | ), 69 | }; 70 | 71 | expect(JSON.stringify(sortObject(fixture))).toEqual( 72 | JSON.stringify({ 73 | reactElement: { 74 | type: 'div', 75 | key: null, 76 | props: { 77 | children: { 78 | type: 'span', 79 | key: null, 80 | props: {}, 81 | _owner: null, 82 | _store: {}, 83 | }, 84 | }, 85 | _store: {}, 86 | }, 87 | }) 88 | ); 89 | }); 90 | }); 91 | }); 92 | -------------------------------------------------------------------------------- /src/formatter/mergeSiblingPlainStringChildrenReducer.spec.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import mergeSiblingPlainStringChildrenReducer from './mergeSiblingPlainStringChildrenReducer'; 4 | import { 5 | createNumberTreeNode, 6 | createStringTreeNode, 7 | createReactElementTreeNode, 8 | } from './../tree'; 9 | import type { TreeNode } from './../tree'; 10 | 11 | test('mergeSiblingPlainStringChildrenReducer should merge sibling string tree nodes', () => { 12 | const childrens: TreeNode[] = [ 13 | createStringTreeNode('a'), 14 | createStringTreeNode('b'), 15 | createStringTreeNode('c'), 16 | ]; 17 | 18 | expect(childrens.reduce(mergeSiblingPlainStringChildrenReducer, [])).toEqual([ 19 | { 20 | type: 'string', 21 | value: 'abc', 22 | }, 23 | ]); 24 | }); 25 | 26 | test('mergeSiblingPlainStringChildrenReducer should consider number as string', () => { 27 | expect( 28 | [ 29 | createStringTreeNode('a'), 30 | createNumberTreeNode(51), 31 | createStringTreeNode('c'), 32 | ].reduce(mergeSiblingPlainStringChildrenReducer, []) 33 | ).toEqual([ 34 | { 35 | type: 'string', 36 | value: 'a51c', 37 | }, 38 | ]); 39 | 40 | expect( 41 | [ 42 | createStringTreeNode(5), 43 | createNumberTreeNode(1), 44 | createStringTreeNode('a'), 45 | ].reduce(mergeSiblingPlainStringChildrenReducer, []) 46 | ).toEqual([ 47 | { 48 | type: 'string', 49 | value: '51a', 50 | }, 51 | ]); 52 | }); 53 | 54 | test('mergeSiblingPlainStringChildrenReducer should detect non string node', () => { 55 | const childrens: TreeNode[] = [ 56 | createReactElementTreeNode('MyFoo', {}, {}, ['foo']), 57 | createStringTreeNode('a'), 58 | createNumberTreeNode('b'), 59 | createReactElementTreeNode('MyBar', {}, {}, ['bar']), 60 | createStringTreeNode('c'), 61 | createNumberTreeNode(42), 62 | createReactElementTreeNode('MyBaz', {}, {}, ['baz']), 63 | ]; 64 | 65 | expect(childrens.reduce(mergeSiblingPlainStringChildrenReducer, [])).toEqual([ 66 | { 67 | type: 'ReactElement', 68 | displayName: 'MyFoo', 69 | props: {}, 70 | defaultProps: {}, 71 | childrens: ['foo'], 72 | }, 73 | { 74 | type: 'string', 75 | value: 'ab', 76 | }, 77 | { 78 | type: 'ReactElement', 79 | displayName: 'MyBar', 80 | props: {}, 81 | defaultProps: {}, 82 | childrens: ['bar'], 83 | }, 84 | { 85 | type: 'string', 86 | value: 'c42', 87 | }, 88 | { 89 | type: 'ReactElement', 90 | displayName: 'MyBaz', 91 | props: {}, 92 | defaultProps: {}, 93 | childrens: ['baz'], 94 | }, 95 | ]); 96 | }); 97 | 98 | test('mergeSiblingPlainStringChildrenReducer should reduce empty array to an empty array', () => { 99 | expect([].reduce(mergeSiblingPlainStringChildrenReducer, [])).toEqual([]); 100 | }); 101 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-element-to-jsx-string", 3 | "version": "17.0.1", 4 | "description": "Turn a ReactElement into the corresponding JSX string.", 5 | "packageManager": "yarn@1.22.22+sha1.ac34549e6aa8e7ead463a7407e1c7390f61a6610", 6 | "main": "dist/cjs/index.js", 7 | "module": "dist/esm/index.js", 8 | "browser": "dist/cjs/index.js", 9 | "types": "index.d.ts", 10 | "scripts": { 11 | "build": "rollup -c", 12 | "build:flow": "flow-copy-source -v --ignore=*.spec.js src/ dist/cjs", 13 | "prebuild": "rm -rf dist/", 14 | "postbuild": "npm run build:flow", 15 | "prepare": "npm run build", 16 | "lint": "eslint .", 17 | "lint:fix": "npm run lint -- --fix", 18 | "flow": "flow", 19 | "precommit": "lint-staged", 20 | "prepublishOnly": "npm run build", 21 | "prettier:fix": "prettier --write \"**/*.{js,json}\"", 22 | "test": "jest", 23 | "test:watch": "jest --watch", 24 | "release": "./release.sh", 25 | "smoke": "node tests/smoke/run" 26 | }, 27 | "lint-staged": { 28 | "*.js": [ 29 | "prettier --write \"**/*.{js,json}\"", 30 | "git add" 31 | ] 32 | }, 33 | "author": { 34 | "name": "Algolia, Inc.", 35 | "url": "https://github.com/algolia" 36 | }, 37 | "license": "MIT", 38 | "repository": "algolia/react-element-to-jsx-string", 39 | "devDependencies": { 40 | "@babel/cli": "7.28.3", 41 | "@babel/core": "7.28.5", 42 | "@babel/preset-env": "7.28.5", 43 | "@babel/preset-flow": "7.27.1", 44 | "@babel/preset-react": "7.28.5", 45 | "@commitlint/cli": "8.3.6", 46 | "@commitlint/config-angular": "8.3.6", 47 | "@testing-library/dom": "10.4.1", 48 | "@testing-library/jest-dom": "5.17.0", 49 | "@testing-library/react": "16.3.1", 50 | "babel-eslint": "10.1.0", 51 | "babel-jest": "24.9.0", 52 | "babel-register": "6.26.0", 53 | "conventional-changelog-cli": "2.2.2", 54 | "doctoc": "1.4.0", 55 | "eslint": "6.8.0", 56 | "eslint-config-algolia": "14.0.1", 57 | "eslint-config-prettier": "6.15.0", 58 | "eslint-plugin-import": "2.32.0", 59 | "eslint-plugin-jest": "22.21.0", 60 | "eslint-plugin-prettier": "3.4.1", 61 | "eslint-plugin-react": "7.31.1", 62 | "esm": "3.2.25", 63 | "expect": "27.5.1", 64 | "flow-bin": "0.119.1", 65 | "flow-copy-source": "2.0.9", 66 | "husky": "3.1.0", 67 | "jest": "27.5.1", 68 | "json": "10.0.0", 69 | "lint-staged": "10.5.4", 70 | "mversion": "2.0.1", 71 | "prettier": "1.19.1", 72 | "react": "19.2.3", 73 | "react-dom": "19.2.3", 74 | "react-is": "19.2.3", 75 | "rollup": "2.79.1", 76 | "rollup-plugin-babel": "4.4.0", 77 | "rollup-plugin-node-builtins": "2.1.2", 78 | "rollup-plugin-node-globals": "1.4.0", 79 | "rollup-plugin-node-resolve": "5.2.0", 80 | "rollup-plugin-sourcemaps": "0.6.3" 81 | }, 82 | "peerDependencies": { 83 | "react": "^19.0.0", 84 | "react-dom": "^19.0.0", 85 | "react-is": "^19.0.0" 86 | }, 87 | "dependencies": { 88 | "@base2/pretty-print-object": "1.0.2", 89 | "is-plain-object": "5.0.0" 90 | }, 91 | "jest": { 92 | "setupFilesAfterEnv": [ 93 | "tests/setupTests.js" 94 | ] 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/formatter/formatReactFragmentNode.spec.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import formatReactFragmentNode from './formatReactFragmentNode'; 4 | 5 | const defaultOptions = { 6 | filterProps: [], 7 | showDefaultProps: true, 8 | showFunctions: false, 9 | tabStop: 2, 10 | useBooleanShorthandSyntax: true, 11 | useFragmentShortSyntax: true, 12 | sortProps: true, 13 | }; 14 | 15 | describe('formatReactFragmentNode', () => { 16 | it('should format a react fragment with a string as children', () => { 17 | const tree = { 18 | type: 'ReactFragment', 19 | childrens: [ 20 | { 21 | value: 'Hello world', 22 | type: 'string', 23 | }, 24 | ], 25 | }; 26 | 27 | expect(formatReactFragmentNode(tree, false, 0, defaultOptions)).toEqual( 28 | `<> 29 | Hello world 30 | ` 31 | ); 32 | }); 33 | 34 | it('should format a react fragment with a key', () => { 35 | const tree = { 36 | type: 'ReactFragment', 37 | key: 'foo', 38 | childrens: [ 39 | { 40 | value: 'Hello world', 41 | type: 'string', 42 | }, 43 | ], 44 | }; 45 | 46 | expect(formatReactFragmentNode(tree, false, 0, defaultOptions)).toEqual( 47 | ` 48 | Hello world 49 | ` 50 | ); 51 | }); 52 | 53 | it('should format a react fragment with multiple childrens', () => { 54 | const tree = { 55 | type: 'ReactFragment', 56 | childrens: [ 57 | { 58 | type: 'ReactElement', 59 | displayName: 'div', 60 | props: { a: 'foo' }, 61 | childrens: [], 62 | }, 63 | { 64 | type: 'ReactElement', 65 | displayName: 'div', 66 | props: { b: 'bar' }, 67 | childrens: [], 68 | }, 69 | ], 70 | }; 71 | 72 | expect(formatReactFragmentNode(tree, false, 0, defaultOptions)).toEqual( 73 | `<> 74 |
75 |
76 | ` 77 | ); 78 | }); 79 | 80 | it('should format an empty react fragment', () => { 81 | const tree = { 82 | type: 'ReactFragment', 83 | childrens: [], 84 | }; 85 | 86 | expect(formatReactFragmentNode(tree, false, 0, defaultOptions)).toEqual( 87 | '' 88 | ); 89 | }); 90 | 91 | it('should format an empty react fragment with key', () => { 92 | const tree = { 93 | type: 'ReactFragment', 94 | key: 'foo', 95 | childrens: [], 96 | }; 97 | 98 | expect(formatReactFragmentNode(tree, false, 0, defaultOptions)).toEqual( 99 | '' 100 | ); 101 | }); 102 | 103 | it('should format a react fragment using the explicit syntax', () => { 104 | const tree = { 105 | type: 'ReactFragment', 106 | childrens: [ 107 | { 108 | value: 'Hello world', 109 | type: 'string', 110 | }, 111 | ], 112 | }; 113 | 114 | expect( 115 | formatReactFragmentNode(tree, false, 0, { 116 | ...defaultOptions, 117 | ...{ useFragmentShortSyntax: false }, 118 | }) 119 | ).toEqual( 120 | ` 121 | Hello world 122 | ` 123 | ); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /src/formatter/formatReactElementNode.spec.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import formatReactElementNode from './formatReactElementNode'; 5 | 6 | const defaultOptions = { 7 | filterProps: [], 8 | showDefaultProps: true, 9 | showFunctions: false, 10 | tabStop: 2, 11 | useBooleanShorthandSyntax: true, 12 | sortProps: true, 13 | }; 14 | 15 | describe('formatReactElementNode', () => { 16 | it('should format a react element with a string a children', () => { 17 | const tree = { 18 | type: 'ReactElement', 19 | displayName: 'h1', 20 | defaultProps: {}, 21 | props: {}, 22 | childrens: [ 23 | { 24 | value: 'Hello world', 25 | type: 'string', 26 | }, 27 | ], 28 | }; 29 | 30 | expect(formatReactElementNode(tree, false, 0, defaultOptions)).toEqual( 31 | `

32 | Hello world 33 |

` 34 | ); 35 | }); 36 | 37 | it('should format a single depth react element', () => { 38 | const tree = { 39 | type: 'ReactElement', 40 | displayName: 'aaa', 41 | props: { 42 | foo: '41', 43 | }, 44 | defaultProps: { 45 | foo: '41', 46 | }, 47 | childrens: [], 48 | }; 49 | 50 | expect(formatReactElementNode(tree, false, 0, defaultOptions)).toEqual( 51 | '' 52 | ); 53 | }); 54 | 55 | it('should format a react element with an object as props', () => { 56 | const tree = { 57 | type: 'ReactElement', 58 | displayName: 'div', 59 | defaultProps: { 60 | a: { aa: '1', bb: { cc: '3' } }, 61 | }, 62 | props: { 63 | a: { aa: '1', bb: { cc: '3' } }, 64 | }, 65 | childrens: [], 66 | }; 67 | 68 | expect(formatReactElementNode(tree, false, 0, defaultOptions)).toEqual( 69 | `
` 77 | ); 78 | }); 79 | 80 | it('should format a react element with another react element as props', () => { 81 | const tree = { 82 | type: 'ReactElement', 83 | displayName: 'div', 84 | defaultProps: { 85 | a: , 86 | }, 87 | props: { 88 | a: , 89 | }, 90 | childrens: [], 91 | }; 92 | 93 | expect(formatReactElementNode(tree, false, 0, defaultOptions)).toEqual( 94 | '
} />' 95 | ); 96 | }); 97 | 98 | it('should format a react element with multiline children', () => { 99 | const tree = { 100 | type: 'ReactElement', 101 | displayName: 'div', 102 | defaultProps: {}, 103 | props: {}, 104 | childrens: [ 105 | { 106 | type: 'string', 107 | value: 'first line\nsecond line\nthird line', 108 | }, 109 | ], 110 | }; 111 | 112 | expect(formatReactElementNode(tree, false, 0, defaultOptions)).toEqual( 113 | `
114 | first line 115 | second line 116 | third line 117 |
` 118 | ); 119 | 120 | expect(formatReactElementNode(tree, false, 2, defaultOptions)).toEqual( 121 | `
122 | first line 123 | second line 124 | third line 125 |
` 126 | ); 127 | }); 128 | 129 | it('should allow filtering props by function', () => { 130 | const tree = { 131 | type: 'ReactElement', 132 | displayName: 'h1', 133 | defaultProps: {}, 134 | props: { className: 'myClass', onClick: () => {} }, 135 | childrens: [ 136 | { 137 | value: 'Hello world', 138 | type: 'string', 139 | }, 140 | ], 141 | }; 142 | 143 | const options = { 144 | ...defaultOptions, 145 | filterProps: (val, key) => !key.startsWith('on'), 146 | }; 147 | 148 | expect(formatReactElementNode(tree, false, 0, options)).toEqual( 149 | `

150 | Hello world 151 |

` 152 | ); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /src/formatter/formatComplexDataStructure.spec.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import formatComplexDataStructure from './formatComplexDataStructure'; 5 | 6 | jest.mock('./formatReactElementNode.js', () => node => 7 | `<${node.displayName} />` 8 | ); 9 | 10 | const createFakeReactElement = (tagName = 'Foo') => 11 | React.createElement(tagName, {}, null); 12 | const options = { tabStop: 2 }; 13 | 14 | describe('formatComplexDataStructure', () => { 15 | it('should format an object', () => { 16 | const fixture = { a: 1, b: { c: 'ccc' } }; 17 | 18 | expect(formatComplexDataStructure(fixture, false, 0, options)).toEqual( 19 | `{ 20 | a: 1, 21 | b: { 22 | c: 'ccc' 23 | } 24 | }` 25 | ); 26 | }); 27 | 28 | it('should format inline an object', () => { 29 | const fixture = { a: 1, b: { c: 'ccc' } }; 30 | 31 | expect(formatComplexDataStructure(fixture, true, 0, options)).toEqual( 32 | "{a: 1, b: {c: 'ccc'}}" 33 | ); 34 | }); 35 | 36 | it('should format an empty object', () => { 37 | expect(formatComplexDataStructure({}, false, 0, options)).toEqual('{}'); 38 | }); 39 | 40 | it('should order the object keys', () => { 41 | const fixture = { b: { d: 'ddd', c: 'ccc' }, a: 1 }; 42 | 43 | expect(formatComplexDataStructure(fixture, false, 0, options)).toEqual( 44 | `{ 45 | a: 1, 46 | b: { 47 | c: 'ccc', 48 | d: 'ddd' 49 | } 50 | }` 51 | ); 52 | }); 53 | 54 | it('should format an array', () => { 55 | const fixture = [1, '2', true, false, null]; 56 | 57 | expect(formatComplexDataStructure(fixture, false, 0, options)).toEqual( 58 | `[ 59 | 1, 60 | '2', 61 | true, 62 | false, 63 | null 64 | ]` 65 | ); 66 | }); 67 | 68 | it('should format inline an array ', () => { 69 | const fixture = [1, '2', true, false, null]; 70 | 71 | expect(formatComplexDataStructure(fixture, true, 0, options)).toEqual( 72 | "[1, '2', true, false, null]" 73 | ); 74 | }); 75 | 76 | it('should format an object that contains a react element', () => { 77 | const fixture = { a: createFakeReactElement('BarBar') }; 78 | 79 | expect(formatComplexDataStructure(fixture, false, 0, options)).toEqual( 80 | `{ 81 | a: 82 | }` 83 | ); 84 | }); 85 | 86 | it('should format an empty array', () => { 87 | expect(formatComplexDataStructure([], false, 0, options)).toEqual('[]'); 88 | }); 89 | 90 | it('should format an object that contains a date', () => { 91 | const fixture = { a: new Date('2017-11-13T00:00:00.000Z') }; 92 | 93 | expect(formatComplexDataStructure(fixture, true, 0, options)).toEqual( 94 | `{a: new Date('2017-11-13T00:00:00.000Z')}` 95 | ); 96 | }); 97 | 98 | it('should format an object that contains a regexp', () => { 99 | const fixture = { a: /test/g }; 100 | 101 | expect(formatComplexDataStructure(fixture, true, 0, options)).toEqual( 102 | `{a: /test/g}` 103 | ); 104 | }); 105 | 106 | it('should replace a function with noRefCheck', () => { 107 | const fixture = { 108 | a: function hello() { 109 | return 1; 110 | }, 111 | }; 112 | 113 | expect(formatComplexDataStructure(fixture, true, 0, options)).toEqual( 114 | '{a: function noRefCheck() {}}' 115 | ); 116 | }); 117 | 118 | it('should format a function', () => { 119 | const fixture = { 120 | a: function hello() { 121 | return 1; 122 | }, 123 | }; 124 | 125 | expect( 126 | formatComplexDataStructure(fixture, true, 0, { 127 | ...options, 128 | showFunctions: true, 129 | }) 130 | ).toEqual('{a: function hello() {return 1;}}'); 131 | }); 132 | 133 | it('should use the functionValue option', () => { 134 | const fixture = { 135 | a: function hello() { 136 | return 1; 137 | }, 138 | }; 139 | 140 | expect( 141 | formatComplexDataStructure(fixture, true, 0, { 142 | ...options, 143 | functionValue: () => '', 144 | }) 145 | ).toEqual('{a: }'); 146 | }); 147 | }); 148 | -------------------------------------------------------------------------------- /src/parser/parseReactElement.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { type Element as ReactElement, Fragment } from 'react'; 4 | import { 5 | ForwardRef, 6 | isContextConsumer, 7 | isContextProvider, 8 | isForwardRef, 9 | isLazy, 10 | isMemo, 11 | isProfiler, 12 | isStrictMode, 13 | isSuspense, 14 | Memo, 15 | } from 'react-is'; 16 | import type { Options } from './../options'; 17 | import { 18 | createStringTreeNode, 19 | createNumberTreeNode, 20 | createReactElementTreeNode, 21 | createReactFragmentTreeNode, 22 | } from './../tree'; 23 | import type { TreeNode } from './../tree'; 24 | 25 | const supportFragment = Boolean(Fragment); 26 | 27 | const getFunctionTypeName = (functionType): string => { 28 | if (!functionType.name || functionType.name === '_default') { 29 | return 'No Display Name'; 30 | } 31 | return functionType.name; 32 | }; 33 | 34 | const getWrappedComponentDisplayName = (Component: *): string => { 35 | switch (true) { 36 | case Boolean(Component.displayName): 37 | return Component.displayName; 38 | case Component.$$typeof === Memo: 39 | return getWrappedComponentDisplayName(Component.type); 40 | case Component.$$typeof === ForwardRef: 41 | return getWrappedComponentDisplayName(Component.render); 42 | default: 43 | return getFunctionTypeName(Component); 44 | } 45 | }; 46 | 47 | // heavily inspired by: 48 | // https://github.com/facebook/react/blob/3746eaf985dd92f8aa5f5658941d07b6b855e9d9/packages/react-devtools-shared/src/backend/renderer.js#L399-L496 49 | const getReactElementDisplayName = (element: ReactElement<*>): string => { 50 | switch (true) { 51 | case typeof element.type === 'string': 52 | return element.type; 53 | case typeof element.type === 'function': 54 | if (element.type.displayName) { 55 | return element.type.displayName; 56 | } 57 | return getFunctionTypeName(element.type); 58 | case isForwardRef(element): 59 | case isMemo(element): 60 | return getWrappedComponentDisplayName(element.type); 61 | case isContextConsumer(element): 62 | return `${element.type._context.displayName || 'Context'}.Consumer`; 63 | case isContextProvider(element): 64 | return `${element.type.displayName || 'Context'}.Provider`; 65 | case isLazy(element): 66 | return 'Lazy'; 67 | case isProfiler(element): 68 | return 'Profiler'; 69 | case isStrictMode(element): 70 | return 'StrictMode'; 71 | case isSuspense(element): 72 | return 'Suspense'; 73 | default: 74 | return 'UnknownElementType'; 75 | } 76 | }; 77 | 78 | const noChildren = (propsValue, propName) => propName !== 'children'; 79 | 80 | const onlyMeaningfulChildren = (children): boolean => 81 | children !== true && 82 | children !== false && 83 | children !== null && 84 | children !== ''; 85 | 86 | const filterProps = (originalProps: {}, cb: (any, string) => boolean) => { 87 | const filteredProps = {}; 88 | 89 | Object.keys(originalProps) 90 | .filter(key => cb(originalProps[key], key)) 91 | .forEach(key => (filteredProps[key] = originalProps[key])); 92 | 93 | return filteredProps; 94 | }; 95 | 96 | const parseReactElement = ( 97 | element: ReactElement<*> | string | number, 98 | options: Options 99 | ): TreeNode => { 100 | const { displayName: displayNameFn = getReactElementDisplayName } = options; 101 | 102 | if (typeof element === 'string') { 103 | return createStringTreeNode(element); 104 | } else if (typeof element === 'number') { 105 | return createNumberTreeNode(element); 106 | } else if (!React.isValidElement(element)) { 107 | throw new Error( 108 | `react-element-to-jsx-string: Expected a React.Element, got \`${typeof element}\`` 109 | ); 110 | } 111 | 112 | const displayName = displayNameFn(element); 113 | 114 | const props = filterProps(element.props, noChildren); 115 | 116 | const key = element.key; 117 | if (typeof key === 'string' && key.search(/^\./)) { 118 | // React automatically add key=".X" when there are some children 119 | props.key = key; 120 | } 121 | 122 | const defaultProps = filterProps(element.type.defaultProps || {}, noChildren); 123 | const childrens = React.Children.toArray(element.props.children) 124 | .filter(onlyMeaningfulChildren) 125 | .map(child => parseReactElement(child, options)); 126 | 127 | if (supportFragment && element.type === Fragment) { 128 | return createReactFragmentTreeNode(key, childrens); 129 | } 130 | 131 | return createReactElementTreeNode( 132 | displayName, 133 | props, 134 | defaultProps, 135 | childrens 136 | ); 137 | }; 138 | 139 | export default parseReactElement; 140 | -------------------------------------------------------------------------------- /src/formatter/formatPropValue.spec.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React from 'react'; 4 | import formatPropValue from './formatPropValue'; 5 | import parseReactElement from './../parser/parseReactElement'; 6 | import formatTreeNode from './formatTreeNode'; 7 | import formatComplexDataStructure from './formatComplexDataStructure'; 8 | 9 | jest.mock('./../parser/parseReactElement'); 10 | jest.mock('./formatTreeNode', () => 11 | jest.fn().mockReturnValue('') 12 | ); 13 | jest.mock('./formatComplexDataStructure', () => 14 | jest.fn().mockReturnValue('*Mocked formatComplexDataStructure result*') 15 | ); 16 | 17 | describe('formatPropValue', () => { 18 | beforeEach(() => { 19 | jest.clearAllMocks(); 20 | }); 21 | 22 | it('should format an integer prop value', () => { 23 | expect(formatPropValue(42, false, 0, {})).toBe('{42}'); 24 | }); 25 | 26 | it('should escape double quote on prop value of string type', () => { 27 | expect(formatPropValue('Hello "Jonh"!', false, 0, {})).toBe( 28 | '"Hello "Jonh"!"' 29 | ); 30 | }); 31 | 32 | it('should format a symbol prop value', () => { 33 | expect(formatPropValue(Symbol('Foo'), false, 0, {})).toBe( 34 | "{Symbol('Foo')}" 35 | ); 36 | 37 | // eslint-disable-next-line symbol-description 38 | expect(formatPropValue(Symbol(), false, 0, {})).toBe('{Symbol()}'); 39 | }); 40 | 41 | it('should replace a function prop value by a an empty generic function by default', () => { 42 | const doThings = a => a * 2; 43 | 44 | expect(formatPropValue(doThings, false, 0, {})).toBe( 45 | '{function noRefCheck() {}}' 46 | ); 47 | }); 48 | 49 | it('should show the function prop value implementation if "showFunctions" option is true', () => { 50 | const doThings = a => a * 2; 51 | 52 | expect(formatPropValue(doThings, false, 0, { showFunctions: true })).toBe( 53 | '{function doThings(a) {return a * 2;}}' 54 | ); 55 | }); 56 | 57 | it('should format the function prop value with the "functionValue" option', () => { 58 | const doThings = a => a * 2; 59 | 60 | const functionValue = fn => { 61 | expect(fn).toBe(doThings); 62 | 63 | return 'function Myfunction() {}'; 64 | }; 65 | 66 | expect( 67 | formatPropValue(doThings, false, 0, { 68 | functionValue, 69 | showFunctions: true, 70 | }) 71 | ).toBe('{function Myfunction() {}}'); 72 | 73 | expect( 74 | formatPropValue(doThings, false, 0, { 75 | functionValue, 76 | showFunctions: false, 77 | }) 78 | ).toBe('{function Myfunction() {}}'); 79 | }); 80 | 81 | it('should parse and format a react element prop value', () => { 82 | expect(formatPropValue(
, false, 0, {})).toBe( 83 | '{}' 84 | ); 85 | 86 | expect(parseReactElement).toHaveBeenCalledTimes(1); 87 | expect(formatTreeNode).toHaveBeenCalledTimes(1); 88 | }); 89 | 90 | it('should format a date prop value', () => { 91 | expect( 92 | formatPropValue(new Date('2017-01-01T11:00:00.000Z'), false, 0, {}) 93 | ).toBe('{new Date("2017-01-01T11:00:00.000Z")}'); 94 | }); 95 | 96 | it('should format an invalid date prop value', () => { 97 | expect(formatPropValue(new Date(NaN), false, 0, {})).toBe( 98 | '{new Date(NaN)}' 99 | ); 100 | }); 101 | 102 | it('should format an object prop value', () => { 103 | expect(formatPropValue({ foo: 42 }, false, 0, {})).toBe( 104 | '{*Mocked formatComplexDataStructure result*}' 105 | ); 106 | 107 | expect(formatComplexDataStructure).toHaveBeenCalledTimes(1); 108 | }); 109 | 110 | it('should format an array prop value', () => { 111 | expect(formatPropValue(['a', 'b', 'c'], false, 0, {})).toBe( 112 | '{*Mocked formatComplexDataStructure result*}' 113 | ); 114 | 115 | expect(formatComplexDataStructure).toHaveBeenCalledTimes(1); 116 | }); 117 | 118 | it('should format a boolean prop value', () => { 119 | expect(formatPropValue(true, false, 0, {})).toBe('{true}'); 120 | expect(formatPropValue(false, false, 0, {})).toBe('{false}'); 121 | }); 122 | 123 | it('should format null prop value', () => { 124 | expect(formatPropValue(null, false, 0, {})).toBe('{null}'); 125 | }); 126 | 127 | it('should format undefined prop value', () => { 128 | expect(formatPropValue(undefined, false, 0, {})).toBe('{undefined}'); 129 | }); 130 | 131 | it('should call the ".toString()" method on object instance prop value', () => { 132 | expect(formatPropValue(new Set(['a', 'b', 42]), false, 0, {})).toBe( 133 | '{[object Set]}' 134 | ); 135 | 136 | expect(formatPropValue(new Map(), false, 0, {})).toBe('{[object Map]}'); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /src/parser/parseReactElement.spec.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import React, { Fragment } from 'react'; 4 | import parseReactElement from './parseReactElement'; 5 | 6 | const options = {}; 7 | 8 | describe('parseReactElement', () => { 9 | it('should parse a react element with a string as children', () => { 10 | expect(parseReactElement(

Hello world

, options)).toEqual({ 11 | type: 'ReactElement', 12 | displayName: 'h1', 13 | defaultProps: {}, 14 | props: {}, 15 | childrens: [ 16 | { 17 | type: 'string', 18 | value: 'Hello world', 19 | }, 20 | ], 21 | }); 22 | }); 23 | 24 | it('should filter empty childrens', () => { 25 | expect( 26 | parseReactElement( 27 |

28 | Hello 29 | {null} 30 | {true} 31 | {false} 32 | {''} 33 | world 34 |

, 35 | options 36 | ) 37 | ).toEqual({ 38 | type: 'ReactElement', 39 | displayName: 'h1', 40 | defaultProps: {}, 41 | props: {}, 42 | childrens: [ 43 | { 44 | type: 'string', 45 | value: 'Hello', 46 | }, 47 | { 48 | type: 'string', 49 | value: 'world', 50 | }, 51 | ], 52 | }); 53 | }); 54 | 55 | it('should parse a single depth react element', () => { 56 | expect(parseReactElement(, options)).toEqual({ 57 | type: 'ReactElement', 58 | displayName: 'aaa', 59 | props: { 60 | foo: '41', 61 | }, 62 | defaultProps: {}, 63 | childrens: [], 64 | }); 65 | }); 66 | 67 | it('should parse a react element with an object as props', () => { 68 | expect( 69 | parseReactElement(
, options) 70 | ).toEqual({ 71 | type: 'ReactElement', 72 | displayName: 'div', 73 | defaultProps: {}, 74 | props: { 75 | a: { aa: '1', bb: { cc: '3' } }, 76 | }, 77 | childrens: [], 78 | }); 79 | }); 80 | 81 | it('should parse a react element with another react element as props', () => { 82 | expect(parseReactElement(
} />, options)).toEqual({ 83 | type: 'ReactElement', 84 | displayName: 'div', 85 | defaultProps: {}, 86 | props: { 87 | a: , 88 | }, 89 | childrens: [], 90 | }); 91 | }); 92 | 93 | it('should parse the react element defaultProps', () => { 94 | const Foo = () => {}; 95 | Foo.defaultProps = { 96 | bar: 'Hello Bar!', 97 | baz: 'Hello Baz!', 98 | }; 99 | 100 | expect( 101 | parseReactElement(, options) 102 | ).toEqual({ 103 | type: 'ReactElement', 104 | displayName: 'Foo', 105 | defaultProps: { 106 | bar: 'Hello Bar!', 107 | baz: 'Hello Baz!', 108 | }, 109 | props: { 110 | bar: 'Hello world!', 111 | baz: 'Hello Baz!', 112 | foo: 'Hello Foo!', 113 | }, 114 | childrens: [], 115 | }); 116 | }); 117 | 118 | it('should extract the component key', () => { 119 | expect(parseReactElement(
, options)).toEqual({ 120 | type: 'ReactElement', 121 | displayName: 'div', 122 | defaultProps: {}, 123 | props: { 124 | key: 'foo-1', 125 | }, 126 | childrens: [], 127 | }); 128 | }); 129 | 130 | it('should extract the component ref', () => { 131 | const refFn = () => 'foo'; 132 | 133 | expect(parseReactElement(
, options)).toEqual({ 134 | type: 'ReactElement', 135 | displayName: 'div', 136 | defaultProps: {}, 137 | props: { 138 | ref: refFn, 139 | }, 140 | childrens: [], 141 | }); 142 | 143 | // eslint-disable-next-line react/no-string-refs 144 | expect(parseReactElement(
, options)).toEqual({ 145 | type: 'ReactElement', 146 | displayName: 'div', 147 | defaultProps: {}, 148 | props: { 149 | ref: 'foo', 150 | }, 151 | childrens: [], 152 | }); 153 | }); 154 | 155 | it('should parse a react fragment', () => { 156 | expect( 157 | parseReactElement( 158 | 159 |
160 |
161 | , 162 | options 163 | ) 164 | ).toEqual({ 165 | type: 'ReactFragment', 166 | key: 'foo', 167 | childrens: [ 168 | { 169 | type: 'ReactElement', 170 | displayName: 'div', 171 | defaultProps: {}, 172 | props: {}, 173 | childrens: [], 174 | }, 175 | { 176 | type: 'ReactElement', 177 | displayName: 'div', 178 | defaultProps: {}, 179 | props: {}, 180 | childrens: [], 181 | }, 182 | ], 183 | }); 184 | }); 185 | }); 186 | -------------------------------------------------------------------------------- /src/formatter/formatReactElementNode.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import spacer from './spacer'; 4 | import formatTreeNode from './formatTreeNode'; 5 | import formatProp from './formatProp'; 6 | import mergeSiblingPlainStringChildrenReducer from './mergeSiblingPlainStringChildrenReducer'; 7 | import sortPropsByNames from './sortPropsByNames'; 8 | import createPropFilter from './createPropFilter'; 9 | import type { Options } from './../options'; 10 | import type { ReactElementTreeNode } from './../tree'; 11 | 12 | const compensateMultilineStringElementIndentation = ( 13 | element, 14 | formattedElement: string, 15 | inline: boolean, 16 | lvl: number, 17 | options: Options 18 | ) => { 19 | const { tabStop } = options; 20 | 21 | if (element.type === 'string') { 22 | return formattedElement 23 | .split('\n') 24 | .map((line, offset) => { 25 | if (offset === 0) { 26 | return line; 27 | } 28 | 29 | return `${spacer(lvl, tabStop)}${line}`; 30 | }) 31 | .join('\n'); 32 | } 33 | 34 | return formattedElement; 35 | }; 36 | 37 | const formatOneChildren = ( 38 | inline: boolean, 39 | lvl: number, 40 | options: Options 41 | ) => element => 42 | compensateMultilineStringElementIndentation( 43 | element, 44 | formatTreeNode(element, inline, lvl, options), 45 | inline, 46 | lvl, 47 | options 48 | ); 49 | 50 | const onlyPropsWithOriginalValue = (defaultProps, props) => propName => { 51 | const haveDefaultValue = Object.keys(defaultProps).includes(propName); 52 | return ( 53 | !haveDefaultValue || 54 | (haveDefaultValue && defaultProps[propName] !== props[propName]) 55 | ); 56 | }; 57 | 58 | const isInlineAttributeTooLong = ( 59 | attributes: string[], 60 | inlineAttributeString: string, 61 | lvl: number, 62 | tabStop: number, 63 | maxInlineAttributesLineLength: ?number 64 | ): boolean => { 65 | if (!maxInlineAttributesLineLength) { 66 | return attributes.length > 1; 67 | } 68 | 69 | return ( 70 | spacer(lvl, tabStop).length + inlineAttributeString.length > 71 | maxInlineAttributesLineLength 72 | ); 73 | }; 74 | 75 | const shouldRenderMultilineAttr = ( 76 | attributes: string[], 77 | inlineAttributeString: string, 78 | containsMultilineAttr: boolean, 79 | inline: boolean, 80 | lvl: number, 81 | tabStop: number, 82 | maxInlineAttributesLineLength: ?number 83 | ): boolean => 84 | (isInlineAttributeTooLong( 85 | attributes, 86 | inlineAttributeString, 87 | lvl, 88 | tabStop, 89 | maxInlineAttributesLineLength 90 | ) || 91 | containsMultilineAttr) && 92 | !inline; 93 | 94 | export default ( 95 | node: ReactElementTreeNode, 96 | inline: boolean, 97 | lvl: number, 98 | options: Options 99 | ): string => { 100 | const { 101 | type, 102 | displayName = '', 103 | childrens, 104 | props = {}, 105 | defaultProps = {}, 106 | } = node; 107 | 108 | if (type !== 'ReactElement') { 109 | throw new Error( 110 | `The "formatReactElementNode" function could only format node of type "ReactElement". Given: ${type}` 111 | ); 112 | } 113 | 114 | const { 115 | filterProps, 116 | maxInlineAttributesLineLength, 117 | showDefaultProps, 118 | sortProps, 119 | tabStop, 120 | } = options; 121 | 122 | let out = `<${displayName}`; 123 | 124 | let outInlineAttr = out; 125 | let outMultilineAttr = out; 126 | let containsMultilineAttr = false; 127 | 128 | const visibleAttributeNames = []; 129 | 130 | const propFilter = createPropFilter(props, filterProps); 131 | 132 | Object.keys(props) 133 | .filter(propFilter) 134 | .filter(onlyPropsWithOriginalValue(defaultProps, props)) 135 | .forEach(propName => visibleAttributeNames.push(propName)); 136 | 137 | Object.keys(defaultProps) 138 | .filter(propFilter) 139 | .filter(() => showDefaultProps) 140 | .filter(defaultPropName => !visibleAttributeNames.includes(defaultPropName)) 141 | .forEach(defaultPropName => visibleAttributeNames.push(defaultPropName)); 142 | 143 | const attributes = sortPropsByNames(sortProps)(visibleAttributeNames); 144 | 145 | attributes.forEach(attributeName => { 146 | const { 147 | attributeFormattedInline, 148 | attributeFormattedMultiline, 149 | isMultilineAttribute, 150 | } = formatProp( 151 | attributeName, 152 | Object.keys(props).includes(attributeName), 153 | props[attributeName], 154 | Object.keys(defaultProps).includes(attributeName), 155 | defaultProps[attributeName], 156 | inline, 157 | lvl, 158 | options 159 | ); 160 | 161 | if (isMultilineAttribute) { 162 | containsMultilineAttr = true; 163 | } 164 | 165 | outInlineAttr += attributeFormattedInline; 166 | outMultilineAttr += attributeFormattedMultiline; 167 | }); 168 | 169 | outMultilineAttr += `\n${spacer(lvl, tabStop)}`; 170 | 171 | if ( 172 | shouldRenderMultilineAttr( 173 | attributes, 174 | outInlineAttr, 175 | containsMultilineAttr, 176 | inline, 177 | lvl, 178 | tabStop, 179 | maxInlineAttributesLineLength 180 | ) 181 | ) { 182 | out = outMultilineAttr; 183 | } else { 184 | out = outInlineAttr; 185 | } 186 | 187 | if (childrens && childrens.length > 0) { 188 | const newLvl = lvl + 1; 189 | 190 | out += '>'; 191 | 192 | if (!inline) { 193 | out += '\n'; 194 | out += spacer(newLvl, tabStop); 195 | } 196 | 197 | out += childrens 198 | .reduce(mergeSiblingPlainStringChildrenReducer, []) 199 | .map(formatOneChildren(inline, newLvl, options)) 200 | .join(!inline ? `\n${spacer(newLvl, tabStop)}` : ''); 201 | 202 | if (!inline) { 203 | out += '\n'; 204 | out += spacer(newLvl - 1, tabStop); 205 | } 206 | out += ``; 207 | } else { 208 | if ( 209 | !isInlineAttributeTooLong( 210 | attributes, 211 | outInlineAttr, 212 | lvl, 213 | tabStop, 214 | maxInlineAttributesLineLength 215 | ) 216 | ) { 217 | out += ' '; 218 | } 219 | 220 | out += '/>'; 221 | } 222 | 223 | return out; 224 | }; 225 | -------------------------------------------------------------------------------- /src/formatter/formatProp.spec.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import formatProp from './formatProp'; 4 | import formatPropValue from './formatPropValue'; 5 | 6 | jest.mock('./formatPropValue'); 7 | 8 | const defaultOptions = { 9 | useBooleanShorthandSyntax: true, 10 | tabStop: 2, 11 | }; 12 | 13 | describe('formatProp', () => { 14 | beforeEach(() => { 15 | jest.clearAllMocks(); 16 | jest.resetAllMocks(); 17 | }); 18 | 19 | it('should format prop with only a value', () => { 20 | formatPropValue.mockReturnValue('"MockedPropValue"'); 21 | 22 | expect( 23 | formatProp('foo', true, 'bar', false, null, true, 0, defaultOptions) 24 | ).toEqual({ 25 | attributeFormattedInline: ' foo="MockedPropValue"', 26 | attributeFormattedMultiline: ` 27 | foo="MockedPropValue"`, 28 | isMultilineAttribute: false, 29 | }); 30 | 31 | expect(formatPropValue).toHaveBeenCalledWith( 32 | 'bar', 33 | true, 34 | 0, 35 | defaultOptions 36 | ); 37 | }); 38 | 39 | it('should format prop with only a default value', () => { 40 | formatPropValue.mockReturnValue('"MockedPropValue"'); 41 | 42 | expect( 43 | formatProp('foo', false, null, true, 'baz', true, 0, defaultOptions) 44 | ).toEqual({ 45 | attributeFormattedInline: ' foo="MockedPropValue"', 46 | attributeFormattedMultiline: ` 47 | foo="MockedPropValue"`, 48 | isMultilineAttribute: false, 49 | }); 50 | 51 | expect(formatPropValue).toHaveBeenCalledWith( 52 | 'baz', 53 | true, 54 | 0, 55 | defaultOptions 56 | ); 57 | }); 58 | 59 | it('should format prop with a value and a default value', () => { 60 | formatPropValue.mockReturnValue('"MockedPropValue"'); 61 | 62 | expect( 63 | formatProp('foo', true, 'bar', true, 'baz', true, 0, defaultOptions) 64 | ).toEqual({ 65 | attributeFormattedInline: ' foo="MockedPropValue"', 66 | attributeFormattedMultiline: ` 67 | foo="MockedPropValue"`, 68 | isMultilineAttribute: false, 69 | }); 70 | 71 | expect(formatPropValue).toHaveBeenCalledWith( 72 | 'bar', 73 | true, 74 | 0, 75 | defaultOptions 76 | ); 77 | }); 78 | 79 | it('should format a truthy boolean prop (with short syntax)', () => { 80 | const options = { 81 | useBooleanShorthandSyntax: true, 82 | tabStop: 2, 83 | }; 84 | 85 | formatPropValue.mockReturnValue('{true}'); 86 | 87 | expect( 88 | formatProp('foo', true, true, false, false, true, 0, options) 89 | ).toEqual({ 90 | attributeFormattedInline: ' foo', 91 | attributeFormattedMultiline: ` 92 | foo`, 93 | isMultilineAttribute: false, 94 | }); 95 | 96 | expect(formatPropValue).toHaveBeenCalledWith(true, true, 0, options); 97 | }); 98 | 99 | it('should ignore a falsy boolean prop (with short syntax)', () => { 100 | const options = { 101 | useBooleanShorthandSyntax: true, 102 | tabStop: 2, 103 | }; 104 | 105 | formatPropValue.mockReturnValue('{false}'); 106 | 107 | expect( 108 | formatProp('foo', true, false, false, null, true, 0, options) 109 | ).toEqual({ 110 | attributeFormattedInline: '', 111 | attributeFormattedMultiline: '', 112 | isMultilineAttribute: false, 113 | }); 114 | 115 | expect(formatPropValue).toHaveBeenCalledWith(false, true, 0, options); 116 | }); 117 | 118 | it('should format a truthy boolean prop (with explicit syntax)', () => { 119 | const options = { 120 | useBooleanShorthandSyntax: false, 121 | tabStop: 2, 122 | }; 123 | 124 | formatPropValue.mockReturnValue('{true}'); 125 | 126 | expect( 127 | formatProp('foo', true, true, false, false, true, 0, options) 128 | ).toEqual({ 129 | attributeFormattedInline: ' foo={true}', 130 | attributeFormattedMultiline: ` 131 | foo={true}`, 132 | isMultilineAttribute: false, 133 | }); 134 | 135 | expect(formatPropValue).toHaveBeenCalledWith(true, true, 0, options); 136 | }); 137 | 138 | it('should format a falsy boolean prop (with explicit syntax)', () => { 139 | const options = { 140 | useBooleanShorthandSyntax: false, 141 | tabStop: 2, 142 | }; 143 | 144 | formatPropValue.mockReturnValue('{false}'); 145 | 146 | expect( 147 | formatProp('foo', true, false, false, false, true, 0, options) 148 | ).toEqual({ 149 | attributeFormattedInline: ' foo={false}', 150 | attributeFormattedMultiline: ` 151 | foo={false}`, 152 | isMultilineAttribute: false, 153 | }); 154 | 155 | expect(formatPropValue).toHaveBeenCalledWith(false, true, 0, options); 156 | }); 157 | 158 | it('should format a mulitline props', () => { 159 | formatPropValue.mockReturnValue(`{[ 160 | "a", 161 | "b" 162 | ]}`); 163 | 164 | expect( 165 | formatProp( 166 | 'foo', 167 | true, 168 | ['a', 'b'], 169 | false, 170 | false, 171 | false, 172 | 0, 173 | defaultOptions 174 | ) 175 | ).toEqual({ 176 | attributeFormattedInline: ` foo={[ 177 | "a", 178 | "b" 179 | ]}`, 180 | attributeFormattedMultiline: ` 181 | foo={[ 182 | "a", 183 | "b" 184 | ]}`, 185 | isMultilineAttribute: true, 186 | }); 187 | 188 | expect(formatPropValue).toHaveBeenCalledWith( 189 | ['a', 'b'], 190 | false, 191 | 0, 192 | defaultOptions 193 | ); 194 | }); 195 | 196 | it('should indent the formatted string', () => { 197 | /* 198 | * lvl 4 and tabStop 2 : 199 | * - 4 * 2 = 8 spaces 200 | * - One new indentation = 2 spaces 201 | * - Expected indentation : 10 spaces 202 | */ 203 | 204 | const options = { 205 | useBooleanShorthandSyntax: true, 206 | tabStop: 2, 207 | }; 208 | 209 | formatPropValue.mockReturnValue('"MockedPropValue"'); 210 | 211 | expect( 212 | formatProp('foo', true, 'bar', false, null, true, 4, options) 213 | ).toEqual({ 214 | attributeFormattedInline: ' foo="MockedPropValue"', 215 | attributeFormattedMultiline: ` 216 | foo="MockedPropValue"`, // 10 spaces 217 | isMultilineAttribute: false, 218 | }); 219 | 220 | expect(formatPropValue).toHaveBeenCalledWith('bar', true, 4, options); 221 | }); 222 | }); 223 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-element-to-jsx-string 2 | 3 | [![Version][version-svg]][package-url] [![Build Status][travis-svg]][travis-url] [![License][license-image]][license-url] [![Downloads][downloads-image]][downloads-url] 4 | 5 | [travis-svg]: https://img.shields.io/travis/algolia/react-element-to-jsx-string/master.svg?style=flat-square 6 | [travis-url]: https://travis-ci.org/algolia/react-element-to-jsx-string 7 | [license-image]: http://img.shields.io/badge/license-MIT-green.svg?style=flat-square 8 | [license-url]: LICENSE 9 | [downloads-image]: https://img.shields.io/npm/dm/react-element-to-jsx-string.svg?style=flat-square 10 | [downloads-url]: http://npm-stat.com/charts.html?package=react-element-to-jsx-string 11 | [version-svg]: https://img.shields.io/npm/v/react-element-to-jsx-string.svg?style=flat-square 12 | [package-url]: https://npmjs.org/package/react-element-to-jsx-string 13 | 14 | Turn a ReactElement into the corresponding JSX string. 15 | 16 | Useful for unit testing and any other need you may think of. 17 | 18 | Features: 19 | - supports nesting and deep nesting like `
}}}} />` 20 | - props: supports string, number, function (inlined as `prop={function noRefCheck() {}}`), object, ReactElement (inlined), regex, booleans (with or without [shorthand syntax](https://facebook.github.io/react/docs/jsx-in-depth.html#boolean-attributes)), ... 21 | - order props alphabetically 22 | - sort object keys in a deterministic order (`o={{a: 1, b:2}} === o={{b:2, a:1}}`) 23 | - handle `ref` and `key` attributes, they are always on top of props 24 | - React's documentation indent style for JSX 25 | 26 | 27 | 28 | **Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)* 29 | 30 | - [Setup](#setup) 31 | - [Usage](#usage) 32 | - [API](#api) 33 | - [reactElementToJSXString(ReactElement[, options])](#reactelementtojsxstringreactelement-options) 34 | - [Environment requirements](#environment-requirements) 35 | - [Test](#test) 36 | - [Build](#build) 37 | - [Release](#release) 38 | - [Thanks](#thanks) 39 | 40 | 41 | 42 | ## Setup 43 | 44 | ```sh 45 | yarn add react-element-to-jsx-string [--dev] 46 | ``` 47 | 48 | ## Usage 49 | 50 | ```js 51 | import React from 'react'; 52 | import reactElementToJSXString from 'react-element-to-jsx-string'; 53 | 54 | console.log(reactElementToJSXString(
Hello, world!
)); 55 | //
59 | // Hello, world! 60 | //
61 | ``` 62 | 63 | ## API 64 | 65 | ### reactElementToJSXString(ReactElement[, options]) 66 | 67 | **options.displayName: function(ReactElement)** 68 | 69 | Provide a different algorithm in charge of finding the right display name (name of the underlying Class) for your element. 70 | 71 | Just return the name you want for the provided ReactElement, as a string. 72 | 73 | **options.filterProps: string[] | (val: any, key: string) => boolean, default []** 74 | 75 | If an array of strings is passed, filter out any prop who's name is in 76 | the array. For example ['key'] will suppress the key="" prop from being added. 77 | 78 | If a function is passed, it will be called for each prop with two arguments, 79 | the prop value and key, and will filter out any that return false. 80 | 81 | **options.showDefaultProps: boolean, default true** 82 | 83 | If true, default props shown. 84 | 85 | If false, default props are omitted unless they differ from from the default value. 86 | 87 | **options.showFunctions: boolean, default false** 88 | 89 | If true, functions bodies are shown. 90 | 91 | If false, functions bodies are replaced with `function noRefCheck() {}`. 92 | 93 | **options.functionValue: function, default `(fn) => fn`** 94 | 95 | Allows you to override the default formatting of function values. 96 | 97 | `functionValue` receives the original function reference as input 98 | and should send any value as output. 99 | 100 | **options.tabStop: number, default 2** 101 | 102 | Provide a different number of columns for indentation. 103 | 104 | **options.useBooleanShorthandSyntax: boolean, default true** 105 | 106 | If true, Boolean prop values will be omitted for [shorthand syntax](https://facebook.github.io/react/docs/jsx-in-depth.html#boolean-attributes). 107 | 108 | If false, Boolean prop values will be explicitly output like `prop={true}` and `prop={false}` 109 | 110 | **options.maxInlineAttributesLineLength: number, default undefined** 111 | 112 | Allows to render multiple attributes on the same line and control the behaviour. 113 | 114 | You can provide the max number of characters to render inline with the tag name. If the number of characters on the line (including spacing and the tag name) 115 | exceeds this number, then all attributes will be rendered on a separate line. The default value of this option is `undefined`. If this option is `undefined` 116 | then if there is more than one attribute on an element, they will render on their own line. Note: Objects passed as attribute values are always rendered 117 | on multiple lines 118 | 119 | **options.sortProps: boolean, default true** 120 | 121 | Either to sort or not props. If you use this lib to make some isomorphic rendering you should set it to false, otherwise this would lead to react invalid checksums as the prop order is part of react isomorphic checksum algorithm. 122 | 123 | **options.useFragmentShortSyntax: boolean, default true** 124 | 125 | If true, fragment will be represented with the JSX short syntax `<>...` (when possible). 126 | 127 | If false, fragment will always be represented with the JSX explicit syntax `...`. 128 | 129 | According to [the specs](https://reactjs.org/docs/fragments.html): 130 | - A keyed fragment will always use the explicit syntax: `...` 131 | - An empty fragment will always use the explicit syntax: `` 132 | 133 | Note: to use fragment you must use React >= 16.2 134 | 135 | ## Environment requirements 136 | 137 | The environment you use to use `react-element-to-jsx-string` should have [ES2015](https://babeljs.io/learn-es2015/) support. 138 | 139 | Use the [Babel polyfill](https://babeljs.io/docs/usage/polyfill/) or any other method that will make you 140 | environment behave like an ES2015 environment. 141 | 142 | ## Test 143 | 144 | ```sh 145 | yarn test 146 | yarn test:watch 147 | ``` 148 | 149 | ## Build 150 | 151 | ```sh 152 | yarn build 153 | yarn build:watch 154 | ``` 155 | 156 | ## Release 157 | 158 | Decide if this is a `patch`, `minor` or `major` release, look at http://semver.org/ 159 | 160 | ```sh 161 | npm run release [major|minor|patch|x.x.x] 162 | ``` 163 | 164 | ## Thanks 165 | 166 | [alexlande/react-to-jsx](https://github.com/alexlande/react-to-jsx/) was a good source of inspiration. 167 | 168 | We built our own module because we had some needs like ordering props in alphabetical order. 169 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [17.0.1](https://github.com/algolia/react-element-to-jsx-string/compare/v17.0.0...v17.0.1) (2025-04-25) 2 | 3 | 4 | 5 | # [17.0.0](https://github.com/algolia/react-element-to-jsx-string/compare/v15.0.0...v17.0.0) (2025-01-18) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * **deps:** update dependency @base2/pretty-print-object to v1.0.2 ([3431228](https://github.com/algolia/react-element-to-jsx-string/commit/34312289d8b25d7d57c95c15a5b018a55d83546c)) 11 | 12 | 13 | * React 19 (#865) ([e391aae](https://github.com/algolia/react-element-to-jsx-string/commit/e391aae7f0a113b44a7f55ca176cf93d6e6dfe11)), closes [#865](https://github.com/algolia/react-element-to-jsx-string/issues/865) 14 | 15 | 16 | ### BREAKING CHANGES 17 | 18 | * To simplify the library maintenance, this major version only support React 19. 19 | 20 | 21 | 22 | # [15.0.0](https://github.com/algolia/react-element-to-jsx-string/compare/v14.3.4...v15.0.0) (2022-05-09) 23 | 24 | 25 | ### Bug Fixes 26 | 27 | * add react 18 support, fixes [#728](https://github.com/algolia/react-element-to-jsx-string/issues/728) ([#729](https://github.com/algolia/react-element-to-jsx-string/issues/729)) ([8e17e12](https://github.com/algolia/react-element-to-jsx-string/commit/8e17e1283fd48108663c7fedf7d174c957c00f68)) 28 | 29 | 30 | 31 | ## [14.3.4](https://github.com/algolia/react-element-to-jsx-string/compare/v14.3.3...v14.3.4) (2021-10-19) 32 | 33 | 34 | ### Bug Fixes 35 | 36 | * stringifying elements with props containing circular references ([#660](https://github.com/algolia/react-element-to-jsx-string/issues/660)) ([f203060](https://github.com/algolia/react-element-to-jsx-string/commit/f203060004e59af945019dad32a05f67508cc947)) 37 | 38 | 39 | 40 | ## [14.3.3](https://github.com/algolia/react-element-to-jsx-string/compare/v14.3.2...v14.3.3) (2021-10-19) 41 | 42 | 43 | ### Bug Fixes 44 | 45 | * **deps:** pin dependency react-is to 17.0.2 ([a62c5fd](https://github.com/algolia/react-element-to-jsx-string/commit/a62c5fdaa33d4e891631ba48661f8466c96d809d)) 46 | * fixed crashing on circular React elements ([#619](https://github.com/algolia/react-element-to-jsx-string/issues/619)) ([ea73118](https://github.com/algolia/react-element-to-jsx-string/commit/ea73118d80e668510d7de1ec23215248ef72b0a1)) 47 | * handle invalid Date objects ([#605](https://github.com/algolia/react-element-to-jsx-string/issues/605)) ([#606](https://github.com/algolia/react-element-to-jsx-string/issues/606)) ([dbbd9e5](https://github.com/algolia/react-element-to-jsx-string/commit/dbbd9e5e0bb1e766910809da77e5a6126897202a)) 48 | 49 | 50 | ### Features 51 | 52 | * support more element types ([#617](https://github.com/algolia/react-element-to-jsx-string/issues/617)) ([bf7f4cf](https://github.com/algolia/react-element-to-jsx-string/commit/bf7f4cf8d31494997b9d5f36f238286d67cd6ae1)) 53 | 54 | 55 | 56 | ## [14.3.2](https://github.com/algolia/react-element-to-jsx-string/compare/v14.3.1...v14.3.2) (2020-10-28) 57 | 58 | 59 | ### Bug Fixes 60 | 61 | * **deps:** update dependency is-plain-object to v3.0.1 ([4974512](https://github.com/algolia/react-element-to-jsx-string/commit/4974512273c86c72415376fea89f9d6e07e2b2e5)) 62 | * Handle multiple words before trailing space ([#572](https://github.com/algolia/react-element-to-jsx-string/issues/572)) ([e0c082e](https://github.com/algolia/react-element-to-jsx-string/commit/e0c082eb1d1e9fe2ed2918db157079d17af3af09)) 63 | 64 | 65 | 66 | ## [14.3.1](https://github.com/algolia/react-element-to-jsx-string/compare/v14.3.0...v14.3.1) (2020-01-21) 67 | 68 | 69 | ### Bug Fixes 70 | 71 | * Fix TypeScript type declaration ([#516](https://github.com/algolia/react-element-to-jsx-string/issues/516)) ([c4fe54a](https://github.com/algolia/react-element-to-jsx-string/commit/c4fe54adcafbe688d397fe652e61952a56e7cffe)) 72 | 73 | 74 | 75 | # [14.3.0](https://github.com/algolia/react-element-to-jsx-string/compare/v14.2.0...v14.3.0) (2020-01-17) 76 | 77 | 78 | ### Features 79 | 80 | * allow filterProps to be a function ([#417](https://github.com/algolia/react-element-to-jsx-string/issues/417)) ([c4908bb](https://github.com/algolia/react-element-to-jsx-string/commit/c4908bb)) 81 | 82 | 83 | 84 | # [14.2.0](https://github.com/algolia/react-element-to-jsx-string/compare/v14.1.0...v14.2.0) (2019-12-29) 85 | 86 | 87 | ### Bug Fixes 88 | 89 | * **deps:** pin dependency @base2/pretty-print-object to 1.0.0 ([07f19f9](https://github.com/algolia/react-element-to-jsx-string/commit/07f19f95844681fc4a04e440138e41e385f98a75)) 90 | * **formatting:** fix the anonymous functional component detection after babel upgrade ([7a1b93e](https://github.com/algolia/react-element-to-jsx-string/commit/7a1b93eca7446c4183298a03178f737678d85678)) 91 | 92 | 93 | ### Features 94 | 95 | * **typescript:** Add TypeScript declaration file. ([#475](https://github.com/algolia/react-element-to-jsx-string/issues/475)) ([f9ea4e5](https://github.com/algolia/react-element-to-jsx-string/commit/f9ea4e54c3d01f45ace0038f2031bf9095a48138)) 96 | 97 | 98 | 99 | # [14.1.0](https://github.com/algolia/react-element-to-jsx-string/compare/v14.0.3...v14.1.0) (2019-09-15) 100 | 101 | 102 | ### Bug Fixes 103 | 104 | * **deps:** Remove dependency stringify-object ([6dc6d8d](https://github.com/algolia/react-element-to-jsx-string/commit/6dc6d8d)) 105 | * **deps:** Replace dependency stringify-object with pretty-print-object ([940a413](https://github.com/algolia/react-element-to-jsx-string/commit/940a413)) 106 | 107 | 108 | 109 | ## [14.0.3](https://github.com/algolia/react-element-to-jsx-string/compare/v14.0.2...v14.0.3) (2019-07-19) 110 | 111 | 112 | ### Bug Fixes 113 | 114 | * **deps:** update dependency is-plain-object to v3 ([#361](https://github.com/algolia/react-element-to-jsx-string/issues/361)) ([b58cbbd](https://github.com/algolia/react-element-to-jsx-string/commit/b58cbbd)) 115 | * Rework the propNameSorter to be less dependents of node sort internals ([a9ee312](https://github.com/algolia/react-element-to-jsx-string/commit/a9ee312)) 116 | * **deps:** update dependency stringify-object to v3.3.0 ([bfe9a9f](https://github.com/algolia/react-element-to-jsx-string/commit/bfe9a9f)) 117 | * **formatting:** Make the props "key" and "ref" order predictibale ([#340](https://github.com/algolia/react-element-to-jsx-string/issues/340)) ([3853463](https://github.com/algolia/react-element-to-jsx-string/commit/3853463)) 118 | 119 | 120 | ### chore 121 | 122 | * **deps:** update jest monorepo to v23 (major) ([#305](https://github.com/algolia/react-element-to-jsx-string/issues/305)) ([aef55a2](https://github.com/algolia/react-element-to-jsx-string/commit/aef55a2)) 123 | 124 | 125 | ### Features 126 | 127 | * **sortObject:** Add a test for issue 344 ([#357](https://github.com/algolia/react-element-to-jsx-string/issues/357)) ([5fe7604](https://github.com/algolia/react-element-to-jsx-string/commit/5fe7604)), closes [#334](https://github.com/algolia/react-element-to-jsx-string/issues/334) 128 | 129 | 130 | ### BREAKING CHANGES 131 | 132 | * **deps:** If you use the `showFunctions: true` option, the function are now always inlined in the output by default. Before it was not always the case (depending one the engine, platform or babel versions) 133 | 134 | You could get back to the previous behavior by using the `preserveFunctionLineBreak` function export as a value of the option `functionValue`. 135 | 136 | * test(smoke): Adapt the CommonJS bundle import 137 | 138 | 139 | 140 | 141 | ## [14.0.2](https://github.com/algolia/react-element-to-jsx-string/compare/v14.0.1...v14.0.2) (2018-10-10) 142 | 143 | 144 | ### Bug Fixes 145 | 146 | * **formatting:** Fix JSX element in prop object recursive loop ([#309](https://github.com/algolia/react-element-to-jsx-string/issues/309)) ([fd4f53b](https://github.com/algolia/react-element-to-jsx-string/commit/fd4f53b)) 147 | * **functionValue:** handle nested datastructures ([94d1aeb](https://github.com/algolia/react-element-to-jsx-string/commit/94d1aeb)) 148 | 149 | 150 | 151 | 152 | ## [14.0.1](https://github.com/algolia/react-element-to-jsx-string/compare/v14.0.0...v14.0.1) (2018-06-20) 153 | 154 | 155 | ### Bug Fixes 156 | 157 | * **browser:** Add IE 11 support ([#288](https://github.com/algolia/react-element-to-jsx-string/issues/288)) ([6c071b6](https://github.com/algolia/react-element-to-jsx-string/commit/6c071b6)), closes [#211](https://github.com/algolia/react-element-to-jsx-string/issues/211) [#285](https://github.com/algolia/react-element-to-jsx-string/issues/285) 158 | * **build:** missing babel helpers, true esm modules, simplify ([#290](https://github.com/algolia/react-element-to-jsx-string/issues/290)) ([faa8f46](https://github.com/algolia/react-element-to-jsx-string/commit/faa8f46)) 159 | 160 | 161 | 162 | 163 | # [14.0.0](https://github.com/algolia/react-element-to-jsx-string/compare/v13.2.0...v14.0.0) (2018-05-25) 164 | 165 | 166 | ### Features 167 | 168 | * **browser:** build a dedicated version for the browser ([#242](https://github.com/algolia/react-element-to-jsx-string/issues/242)) ([574d850](https://github.com/algolia/react-element-to-jsx-string/commit/574d850)) 169 | 170 | 171 | ### BREAKING CHANGES 172 | 173 | * **browser:** This PR change of the internal directory structure of the exported code. The previous code has move from the `dist/` into the `dist/esm` directory (but remender that we do not avice you to do use internals code 🤓) 174 | 175 | * fix(bunble): do not bundle peer dependencies 176 | 177 | * qa(ci): Avoid duplicate runs of checks on CI 178 | 179 | * qa(dependencies): Upgrade to latest rollup versions 180 | 181 | * qa(test): Allow to run the smoke tests aggaint all builded versions 182 | 183 | 184 | 185 | 186 | # [13.2.0](https://github.com/algolia/react-element-to-jsx-string/compare/v13.1.0...v13.2.0) (2018-03-14) 187 | 188 | 189 | ### Bug Fixes 190 | 191 | * **deps:** update dependency stringify-object to v3.2.2 ([b1a4c5e](https://github.com/algolia/react-element-to-jsx-string/commit/b1a4c5e)) 192 | 193 | 194 | 195 | 196 | # [13.1.0](https://github.com/algolia/react-element-to-jsx-string/compare/v13.0.0...v13.1.0) (2017-11-15) 197 | 198 | 199 | ### Bug Fixes 200 | 201 | * **formatting:** Date/RegExp values output by formatComplexDataStructure ([#250](https://github.com/algolia/react-element-to-jsx-string/issues/250)) ([0387b72](https://github.com/algolia/react-element-to-jsx-string/commit/0387b72)) 202 | * **react:** Fix warning about access to PropTypes using React 15.5+ (fixes [#213](https://github.com/algolia/react-element-to-jsx-string/issues/213)) ([2dcfd10](https://github.com/algolia/react-element-to-jsx-string/commit/2dcfd10)) 203 | * **test:** Fix usage of yarn instead of npm in test script ([0db5aa4](https://github.com/algolia/react-element-to-jsx-string/commit/0db5aa4)) 204 | 205 | 206 | 207 | 208 | # [13.0.0](https://github.com/algolia/react-element-to-jsx-string/compare/v12.0.0...v13.0.0) (2017-10-09) 209 | 210 | 211 | ### Bug Fixes 212 | 213 | * **deps:** update dependency stringify-object to v3.2.1 ([539ea56](https://github.com/algolia/react-element-to-jsx-string/commit/539ea56)) 214 | * **formatting:** symbol description are now quoted ([2747f1b](https://github.com/algolia/react-element-to-jsx-string/commit/2747f1b)), closes [#134](https://github.com/algolia/react-element-to-jsx-string/issues/134) 215 | * **formatting:** trailing space ([2a07d5e](https://github.com/algolia/react-element-to-jsx-string/commit/2a07d5e)), closes [#135](https://github.com/algolia/react-element-to-jsx-string/issues/135) 216 | 217 | 218 | ### BREAKING CHANGES 219 | 220 | * **formatting:** Trailing are now preserved. In some rare case, `react-element-to-jsx-string` failed to respect the JSX specs for the trailing space. Event is the space were in the final output. There were silentrly ignored by JSX parser. This commit fix this bug by protecting the trailing space in the output. 221 | 222 | If we take the JSX: 223 | ```jsx 224 |
225 | foo bar baz 226 |
227 | ``` 228 | 229 | Before it was converted to (the trailing space are replace by `*` for the readability): 230 | ```html 231 |
232 | foo* 233 | 234 | bar 235 | 236 | *baz 237 |
238 | ``` 239 | 240 | Now there are preserved: 241 | ```html 242 |
243 | foo{' '} 244 | 245 | bar 246 | 247 | {' '}baz 248 |
249 | ``` 250 | * **formatting:** Symbol description are now correctly quoted. This change the output if you use Symbol in your code 251 | 252 | 253 | 254 | 255 | # [12.0.0](https://github.com/algolia/react-element-to-jsx-string/compare/v11.0.1...v12.0.0) (2017-08-03) 256 | 257 | 258 | ### Bug Fixes 259 | 260 | * **flow:** export flow definitions in the released bundle and fix the npm ignore too restrictive ([#115](https://github.com/algolia/react-element-to-jsx-string/issues/115)) ([c4f91b9](https://github.com/algolia/react-element-to-jsx-string/commit/c4f91b9)) 261 | * **formatting:** Children with multiline string are now correctly indented ([d18809e](https://github.com/algolia/react-element-to-jsx-string/commit/d18809e)) 262 | * **formatting:** Fix JSX delimiters escaping in string ([6e0eea3](https://github.com/algolia/react-element-to-jsx-string/commit/6e0eea3)) 263 | * **release:** revert change made by error in commit 86697517 ([903fd5c](https://github.com/algolia/react-element-to-jsx-string/commit/903fd5c)) 264 | * **travis:** manually install yarn on Travis CI ([b8a4c1a](https://github.com/algolia/react-element-to-jsx-string/commit/b8a4c1a)) 265 | 266 | 267 | ### BREAKING CHANGES 268 | 269 | * **formatting:** Improve string escaping of string that contains JSX delimiters (`{`,`}`,`<`,`>`) 270 | 271 | Before: 272 | ``` 273 | console.log(reactElementToJsxString(
{`Mustache :{`}
); 274 | 275 | //
Mustache :{
276 | ``` 277 | 278 | Now: 279 | ``` 280 | console.log(reactElementToJsxString(
{`Mustache :{`}
); 281 | 282 | //
{`Mustache :{`}
283 | ``` 284 | 285 | 286 | 287 | 288 | ## [11.0.1](https://github.com/algolia/react-element-to-jsx-string/compare/v11.0.0...v11.0.1) (2017-07-21) 289 | 290 | 291 | ### Bug Fixes 292 | 293 | * **formatting:** fix an edge case where number and string childrens are not correctly merged ([47572e0](https://github.com/algolia/react-element-to-jsx-string/commit/47572e0)) 294 | 295 | 296 | 297 | 298 | # [11.0.0](https://github.com/algolia/react-element-to-jsx-string/compare/v10.1.0...v11.0.0) (2017-07-20) 299 | 300 | ### Bug Fixes 301 | 302 | * **release:** Missing `mversion` package in dev dependencies ([0f82ee7](https://github.com/algolia/react-element-to-jsx-string/commit/0f82ee7)) 303 | * **escaping:** Complete lib refactor to handle more escaping cases ([9f3c671](https://github.com/algolia/react-element-to-jsx-string/commit/9f3c671)) 304 | 305 | ### BREAKING CHANGES 306 | 307 | * Fix escaping issue with quotes (in props value or in children of type string) 308 | * Handle props value of `Date` type: `
` 309 | * Escape brace chars (`{}`) in a children string: `` 310 | 311 | 312 | 313 | 314 | # [10.1.0](https://github.com/algolia/react-element-to-jsx-string/compare/v10.0.1...v10.1.0) (2017-06-13) 315 | 316 | 317 | ### Features 318 | 319 | * **sortProps:** add option to remove sorting of props ([66e8307](https://github.com/algolia/react-element-to-jsx-string/commit/66e8307)) 320 | 321 | 322 | 323 | 324 | ## [10.0.1](https://github.com/algolia/react-element-to-jsx-string/compare/v10.0.0...v10.0.1) (2017-06-12) 325 | 326 | 327 | 328 | 329 | # [10.0.0](https://github.com/algolia/react-element-to-jsx-string/compare/v9.0.0...v10.0.0) (2017-06-07) 330 | 331 | 332 | 333 | 334 | # [9.0.0](https://github.com/algolia/react-element-to-jsx-string/compare/v8.0.0...v9.0.0) (2017-06-07) 335 | 336 | 337 | ### Bug Fixes 338 | 339 | * **quotes:** fixes #82 #81 #80 ([3d96ddc](https://github.com/algolia/react-element-to-jsx-string/commit/3d96ddc)), closes [#82](https://github.com/algolia/react-element-to-jsx-string/issues/82) [#81](https://github.com/algolia/react-element-to-jsx-string/issues/81) [#80](https://github.com/algolia/react-element-to-jsx-string/issues/80) 340 | 341 | 342 | 343 | 344 | # [8.0.0](https://github.com/algolia/react-element-to-jsx-string/compare/v7.0.0...v8.0.0) (2017-05-24) 345 | 346 | 347 | 348 | 349 | # [7.0.0](https://github.com/algolia/react-element-to-jsx-string/compare/v6.4.0...v7.0.0) (2017-05-14) 350 | 351 | 352 | 353 | 354 | # [6.4.0](https://github.com/algolia/react-element-to-jsx-string/compare/v6.3.0...v6.4.0) (2017-04-24) 355 | 356 | 357 | ### Features 358 | 359 | * **functionValue:** format functions output the way you want ([460e0cc](https://github.com/algolia/react-element-to-jsx-string/commit/460e0cc)) 360 | * **React:** support 15.5+ ([1a99024](https://github.com/algolia/react-element-to-jsx-string/commit/1a99024)) 361 | 362 | 363 | 364 | 365 | # [6.3.0](https://github.com/algolia/react-element-to-jsx-string/compare/v6.2.0...v6.3.0) (2017-03-06) 366 | 367 | 368 | 369 | 370 | # [6.2.0](https://github.com/algolia/react-element-to-jsx-string/compare/v6.0.0...v6.2.0) (2017-02-27) 371 | 372 | 373 | ### Features 374 | 375 | * **inline attributes:** Allow formatting attribute on the same line ([da72176](https://github.com/algolia/react-element-to-jsx-string/commit/da72176)) 376 | 377 | 378 | 379 | 380 | # [6.0.0](https://github.com/algolia/react-element-to-jsx-string/compare/v5.0.7...v6.0.0) (2017-01-03) 381 | 382 | 383 | ### Chores 384 | 385 | * **build:** switch to stringify-object[@3](https://github.com/3) ([e9a5c7f](https://github.com/algolia/react-element-to-jsx-string/commit/e9a5c7f)) 386 | 387 | 388 | ### BREAKING CHANGES 389 | 390 | * build: You need an ES2015 env to use 391 | react-element-to-jsx-string now 392 | 393 | You can use the Babel polyfill to do so. 394 | 395 | 396 | 397 | 398 | ## [5.0.7](https://github.com/algolia/react-element-to-jsx-string/compare/v5.0.6...v5.0.7) (2017-01-03) 399 | 400 | 401 | ### Bug Fixes 402 | 403 | * **build:** revert to stringify-object[@2](https://github.com/2) ([58542bc](https://github.com/algolia/react-element-to-jsx-string/commit/58542bc)), closes [#71](https://github.com/algolia/react-element-to-jsx-string/issues/71) 404 | 405 | 406 | 407 | 408 | ## [5.0.6](https://github.com/algolia/react-element-to-jsx-string/compare/v5.0.5...v5.0.6) (2017-01-02) 409 | 410 | 411 | 412 | 413 | ## [5.0.5](https://github.com/algolia/react-element-to-jsx-string/compare/v5.0.4...v5.0.5) (2017-01-02) 414 | 415 | 416 | 417 | 418 | ## [5.0.4](https://github.com/algolia/react-element-to-jsx-string/compare/v5.0.3...v5.0.4) (2016-12-08) 419 | 420 | 421 | 422 | 423 | ## [5.0.3](https://github.com/algolia/react-element-to-jsx-string/compare/v5.0.2...v5.0.3) (2016-12-08) 424 | 425 | 426 | 427 | 428 | ## [5.0.2](https://github.com/algolia/react-element-to-jsx-string/compare/v5.0.1...v5.0.2) (2016-11-17) 429 | 430 | 431 | 432 | 433 | ## [5.0.1](https://github.com/algolia/react-element-to-jsx-string/compare/v5.0.0...v5.0.1) (2016-11-16) 434 | 435 | 436 | ### Bug Fixes 437 | 438 | * **deps:** remove direct dep on react-addons-test-utils ([06d2588](https://github.com/algolia/react-element-to-jsx-string/commit/06d2588)), closes [#56](https://github.com/algolia/react-element-to-jsx-string/issues/56) 439 | 440 | 441 | 442 | 443 | # [5.0.0](https://github.com/algolia/react-element-to-jsx-string/compare/v4.2.0...v5.0.0) (2016-10-24) 444 | 445 | 446 | ### Features 447 | 448 | * **pretty:** prettify objects, arrays, nested ([864b9db](https://github.com/algolia/react-element-to-jsx-string/commit/864b9db)) 449 | 450 | 451 | ### BREAKING CHANGES 452 | 453 | * pretty: objects and arrays are now prettified by default following #50 454 | If this is a concern to you, open a PR that adds an option to inline parts or the whole output like before 455 | 456 | 457 | 458 | 459 | # [4.2.0](https://github.com/algolia/react-element-to-jsx-string/compare/v4.1.0...v4.2.0) (2016-09-21) 460 | 461 | 462 | 463 | 464 | # [4.1.0](https://github.com/algolia/react-element-to-jsx-string/compare/v4.0.0...v4.1.0) (2016-08-30) 465 | 466 | 467 | 468 | 469 | # [4.0.0](https://github.com/algolia/react-element-to-jsx-string/compare/v3.2.0...v4.0.0) (2016-08-04) 470 | 471 | 472 | 473 | 474 | # [3.2.0](https://github.com/algolia/react-element-to-jsx-string/compare/v3.1.2...v3.2.0) (2016-07-15) 475 | 476 | 477 | 478 | 479 | ## [3.1.2](https://github.com/algolia/react-element-to-jsx-string/compare/v3.1.1...v3.1.2) (2016-07-12) 480 | 481 | 482 | 483 | 484 | ## [3.1.1](https://github.com/algolia/react-element-to-jsx-string/compare/v3.1.0...v3.1.1) (2016-07-12) 485 | 486 | 487 | 488 | 489 | # [3.1.0](https://github.com/algolia/react-element-to-jsx-string/compare/v3.0.0...v3.1.0) (2016-06-28) 490 | 491 | 492 | 493 | 494 | # [3.0.0](https://github.com/algolia/react-element-to-jsx-string/compare/v2.6.1...v3.0.0) (2016-05-25) 495 | 496 | 497 | 498 | 499 | ## [2.6.1](https://github.com/algolia/react-element-to-jsx-string/compare/v2.6.0...v2.6.1) (2016-04-15) 500 | 501 | 502 | ### Bug Fixes 503 | 504 | * **deps:** allow react 0.14 ([7347b71](https://github.com/algolia/react-element-to-jsx-string/commit/7347b71)), closes [#24](https://github.com/algolia/react-element-to-jsx-string/issues/24) 505 | 506 | 507 | 508 | 509 | # [2.6.0](https://github.com/algolia/react-element-to-jsx-string/compare/2.5.0...v2.6.0) (2016-04-15) 510 | 511 | 512 | ### Features 513 | 514 | * **React:** React v15 compat ([37ee7b5](https://github.com/algolia/react-element-to-jsx-string/commit/37ee7b5)) 515 | 516 | 517 | 518 | 519 | # [2.4.0](https://github.com/algolia/react-element-to-jsx-string/compare/2.3.0...2.4.0) (2016-02-02) 520 | 521 | 522 | 523 | 524 | # [2.3.0](https://github.com/algolia/react-element-to-jsx-string/compare/v2.2.0...2.3.0) (2016-02-02) 525 | 526 | 527 | ### Features 528 | 529 | * **deps:** upgrade all deps ([f3e368d](https://github.com/algolia/react-element-to-jsx-string/commit/f3e368d)) 530 | 531 | 532 | 533 | 534 | # [2.2.0](https://github.com/algolia/react-element-to-jsx-string/compare/v2.1.5...v2.2.0) (2016-01-14) 535 | 536 | 537 | 538 | 539 | ## [2.1.5](https://github.com/algolia/react-element-to-jsx-string/compare/v2.1.4...v2.1.5) (2016-01-05) 540 | 541 | 542 | 543 | 544 | ## [2.1.4](https://github.com/algolia/react-element-to-jsx-string/compare/v2.1.3...v2.1.4) (2015-12-10) 545 | 546 | 547 | ### Bug Fixes 548 | 549 | * **stateless comps:** add No Display Name as default component name ([dc0f65c](https://github.com/algolia/react-element-to-jsx-string/commit/dc0f65c)), closes [#11](https://github.com/algolia/react-element-to-jsx-string/issues/11) 550 | 551 | 552 | 553 | 554 | ## [2.1.3](https://github.com/algolia/react-element-to-jsx-string/compare/v2.1.0...v2.1.3) (2015-11-30) 555 | 556 | 557 | ### Bug Fixes 558 | 559 | * handle
{123}
([609ac78](https://github.com/algolia/react-element-to-jsx-string/commit/609ac78)), closes [#8](https://github.com/algolia/react-element-to-jsx-string/issues/8) 560 | * **lodash:** just use plain lodash and import ([062b3fe](https://github.com/algolia/react-element-to-jsx-string/commit/062b3fe)) 561 | * **whitespace:** handle {true} {false} ([eaca1a2](https://github.com/algolia/react-element-to-jsx-string/commit/eaca1a2)), closes [#6](https://github.com/algolia/react-element-to-jsx-string/issues/6) [#7](https://github.com/algolia/react-element-to-jsx-string/issues/7) 562 | * **whitespace:** stop rendering it differently in SOME cases ([128aa95](https://github.com/algolia/react-element-to-jsx-string/commit/128aa95)) 563 | 564 | 565 | 566 | 567 | # [2.1.0](https://github.com/algolia/react-element-to-jsx-string/compare/v2.0.5...v2.1.0) (2015-10-25) 568 | 569 | 570 | ### Features 571 | 572 | * handle key="" ([da85281](https://github.com/algolia/react-element-to-jsx-string/commit/da85281)) 573 | * handle ref="manual-ref" ([5b18191](https://github.com/algolia/react-element-to-jsx-string/commit/5b18191)) 574 | 575 | 576 | 577 | 578 | ## [2.0.5](https://github.com/algolia/react-element-to-jsx-string/compare/v2.0.4...v2.0.5) (2015-10-21) 579 | 580 | 581 | ### Bug Fixes 582 | 583 | * merge plain strings props replacements ([7c2bf27](https://github.com/algolia/react-element-to-jsx-string/commit/7c2bf27)) 584 | 585 | 586 | 587 | 588 | ## [2.0.4](https://github.com/algolia/react-element-to-jsx-string/compare/v2.0.3...v2.0.4) (2015-10-16) 589 | 590 | 591 | ### Bug Fixes 592 | 593 | * **tagName:** fixed an edge-case with decorated component name ([9169ac7](https://github.com/algolia/react-element-to-jsx-string/commit/9169ac7)) 594 | 595 | 596 | 597 | 598 | ## [2.0.3](https://github.com/algolia/react-element-to-jsx-string/compare/v2.0.2...v2.0.3) (2015-10-16) 599 | 600 | 601 | ### Bug Fixes 602 | 603 | * handle arrays the right way ([597a910](https://github.com/algolia/react-element-to-jsx-string/commit/597a910)) 604 | 605 | 606 | 607 | 608 | ## [2.0.2](https://github.com/algolia/react-element-to-jsx-string/compare/v2.0.1...v2.0.2) (2015-10-16) 609 | 610 | 611 | ### Bug Fixes 612 | 613 | * **children:** ensure the array of children is well handled ([36b462a](https://github.com/algolia/react-element-to-jsx-string/commit/36b462a)) 614 | 615 | 616 | 617 | 618 | ## [2.0.1](https://github.com/algolia/react-element-to-jsx-string/compare/v2.0.0...v2.0.1) (2015-10-16) 619 | 620 | 621 | ### Bug Fixes 622 | 623 | * handle empty objects ([fe052bd](https://github.com/algolia/react-element-to-jsx-string/commit/fe052bd)) 624 | 625 | 626 | 627 | 628 | # [2.0.0](https://github.com/algolia/react-element-to-jsx-string/compare/v1.1.2...v2.0.0) (2015-10-16) 629 | 630 | 631 | ### Features 632 | 633 | * **deep:** handle deeply set functions ([ad21917](https://github.com/algolia/react-element-to-jsx-string/commit/ad21917)) 634 | * **deep:** handle deeply set React elements ([a06f329](https://github.com/algolia/react-element-to-jsx-string/commit/a06f329)) 635 | 636 | 637 | ### BREAKING CHANGES 638 | 639 | * deep: functions are now stringified to `function noRefCheck() 640 | {}` instead of `function () {code;}`. For various reasons AND to be 641 | specific about the fact that we do not represent the function in a 642 | realistic way. 643 | 644 | 645 | 646 | 647 | ## [1.1.2](https://github.com/algolia/react-element-to-jsx-string/compare/v1.1.1...v1.1.2) (2015-10-16) 648 | 649 | 650 | ### Bug Fixes 651 | 652 | * handle null and undefined prop values ([9a57a10](https://github.com/algolia/react-element-to-jsx-string/commit/9a57a10)), closes [#1](https://github.com/algolia/react-element-to-jsx-string/issues/1) 653 | 654 | 655 | 656 | 657 | ## [1.1.1](https://github.com/algolia/react-element-to-jsx-string/compare/v1.1.0...v1.1.1) (2015-10-15) 658 | 659 | 660 | 661 | 662 | # [1.1.0](https://github.com/algolia/react-element-to-jsx-string/compare/3e2e7b8...v1.1.0) (2015-10-15) 663 | 664 | 665 | ### Bug Fixes 666 | 667 | * **whitespace:** remove unwanted whitespace in output ([3e2e7b8](https://github.com/algolia/react-element-to-jsx-string/commit/3e2e7b8)) 668 | 669 | 670 | ### Features 671 | 672 | * sort object keys in a deterministic way ([c1ce8a6](https://github.com/algolia/react-element-to-jsx-string/commit/c1ce8a6)) 673 | -------------------------------------------------------------------------------- /src/index.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | 5 | /* @flow */ 6 | 7 | /* eslint-disable react/no-string-refs */ 8 | /* eslint-disable react/prop-types */ 9 | 10 | import React, { Fragment, Component } from 'react'; 11 | import { render, screen } from '@testing-library/react'; 12 | import reactElementToJSXString, { preserveFunctionLineBreak } from './index'; 13 | import AnonymousStatelessComponent from './AnonymousStatelessComponent'; 14 | 15 | class TestComponent extends React.Component {} 16 | 17 | function NamedStatelessComponent(props: { children: React.Children }) { 18 | const { children } = props; 19 | return
{children}
; 20 | } 21 | 22 | class DefaultPropsComponent extends React.Component {} 23 | 24 | DefaultPropsComponent.defaultProps = { 25 | test: 'test', 26 | boolean: true, 27 | number: 0, 28 | undefinedProp: undefined, 29 | }; 30 | 31 | class DisplayNamePrecedence extends React.Component {} 32 | 33 | DisplayNamePrecedence.displayName = 'This should take precedence'; 34 | 35 | describe('reactElementToJSXString(ReactElement)', () => { 36 | it('reactElementToJSXString()', () => { 37 | expect(reactElementToJSXString()).toEqual( 38 | '' 39 | ); 40 | }); 41 | 42 | it('reactElementToJSXString()', () => { 43 | expect(reactElementToJSXString()).toEqual( 44 | '' 45 | ); 46 | }); 47 | 48 | it('reactElementToJSXString()', () => { 49 | expect(reactElementToJSXString()).toEqual( 50 | '' 51 | ); 52 | }); 53 | 54 | it('reactElementToJSXString() with a displayName', () => { 55 | AnonymousStatelessComponent.displayName = 'I have a name!'; 56 | 57 | expect(reactElementToJSXString()).toEqual( 58 | '' 59 | ); 60 | 61 | delete AnonymousStatelessComponent.displayName; 62 | }); 63 | 64 | it("reactElementToJSXString(React.createElement('div'))", () => { 65 | expect(reactElementToJSXString(React.createElement('div'))).toEqual( 66 | '
' 67 | ); 68 | }); 69 | 70 | it("reactElementToJSXString(React.createElement('div', {title: 'hello \"you\"'}))", () => { 71 | expect( 72 | reactElementToJSXString( 73 | React.createElement('div', { title: 'hello "you"' }) 74 | ) 75 | ).toEqual('
'); 76 | }); 77 | 78 | it("reactElementToJSXString(React.createElement('div', {title: '<'hello' you & you>'}))", () => { 79 | expect( 80 | reactElementToJSXString( 81 | React.createElement('div', { title: "<'hello' you & you>" }) 82 | ) 83 | ).toEqual('
'); 84 | }); 85 | 86 | it("reactElementToJSXString(
}} />)", () => { 87 | /* eslint-disable react/no-unescaped-entities */ 88 | expect( 89 | reactElementToJSXString( 90 |
Hello "' you
, 93 | }} 94 | /> 95 | ) 96 | ).toEqual( 97 | `
Hello "' you
100 | }} 101 | />` 102 | ); 103 | }); 104 | 105 | it("reactElementToJSXString(
)", () => { 106 | expect( 107 | reactElementToJSXString( 108 |
117 | ) 118 | ).toEqual( 119 | `
` 131 | ); 132 | }); 133 | 134 | it("reactElementToJSXString(React.createElement('div', {title: Symbol('hello \"you\"')})", () => { 135 | expect( 136 | reactElementToJSXString( 137 | React.createElement('div', { title: Symbol('hello "you"') }) 138 | ) 139 | ).toEqual('
'); 140 | }); 141 | 142 | it('reactElementToJSXString(
)', () => { 143 | expect(reactElementToJSXString(
)).toEqual('
'); 144 | }); 145 | 146 | it('reactElementToJSXString(
{}}/>)', () => { 147 | expect(reactElementToJSXString(
{}} />)).toEqual( 148 | '
' 149 | ); 150 | }); 151 | 152 | it('reactElementToJSXString(
)', () => { 153 | // eslint-disable-next-line react/jsx-no-bind 154 | expect(reactElementToJSXString(
)).toEqual( 155 | '
' 156 | ); 157 | }); 158 | 159 | it('reactElementToJSXString(
} />)', () => { 160 | expect(reactElementToJSXString(
} />)).toEqual( 161 | '
} />' 162 | ); 163 | }); 164 | 165 | it('reactElementToJSXString(
)', () => { 166 | expect(reactElementToJSXString(
)).toEqual( 167 | '
' 168 | ); 169 | }); 170 | 171 | it('reactElementToJSXString(
)', () => { 172 | expect(reactElementToJSXString(
)).toEqual('
'); 173 | }); 174 | 175 | it("reactElementToJSXString(
)", () => { 176 | expect(reactElementToJSXString(
)).toEqual( 177 | `
` 182 | ); 183 | }); 184 | 185 | it('reactElementToJSXString(
)', () => { 186 | expect( 187 | reactElementToJSXString(
) 188 | ).toEqual( 189 | `
` 195 | ); 196 | }); 197 | 198 | it('reactElementToJSXString(
)', () => { 199 | expect( 200 | reactElementToJSXString(
) 201 | ).toEqual( 202 | `
` 208 | ); 209 | }); 210 | 211 | it('reactElementToJSXString()', () => { 212 | expect( 213 | reactElementToJSXString( 214 | 215 | ) 216 | ).toEqual( 217 | `` 220 | ); 221 | }); 222 | 223 | it('reactElementToJSXString()', () => { 224 | expect( 225 | reactElementToJSXString( 226 | 227 | ) 228 | ).toEqual( 229 | `` 232 | ); 233 | }); 234 | 235 | it('reactElementToJSXString(
)', () => { 236 | expect( 237 | reactElementToJSXString( 238 |
239 | ) 240 | ).toEqual( 241 | `
` 252 | ); 253 | }); 254 | 255 | it('reactElementToJSXString(
)', () => { 256 | expect(reactElementToJSXString(
)).toEqual('
'); 257 | }); 258 | 259 | it('reactElementToJSXString(
)', () => { 260 | /* eslint react/jsx-sort-props: 0 */ 261 | expect(reactElementToJSXString(
)).toEqual( 262 | `
` 267 | ); 268 | }); 269 | 270 | it('reactElementToJSXString(
, {sortProps: false})', () => { 271 | /* eslint react/jsx-sort-props: 0 */ 272 | expect( 273 | reactElementToJSXString(
, { 274 | sortProps: false, 275 | }) 276 | ).toEqual( 277 | `
` 282 | ); 283 | }); 284 | 285 | it('reactElementToJSXString(
Hello
)', () => { 286 | expect(reactElementToJSXString(
Hello
)).toEqual( 287 | `
288 | Hello 289 |
` 290 | ); 291 | }); 292 | 293 | it('reactElementToJSXString(
Hello
)', () => { 294 | expect( 295 | reactElementToJSXString( 296 |
297 | Hello 298 |
299 | ) 300 | ).toEqual( 301 | `
305 | Hello 306 |
` 307 | ); 308 | }); 309 | 310 | it('reactElementToJSXString(
Hello
)', () => { 311 | expect(reactElementToJSXString(
Hello
)).toEqual( 312 | `
313 | Hello 314 |
` 315 | ); 316 | }); 317 | 318 | it('reactElementToJSXString(
Hello "Jonh" and \'Mike\'
)', () => { 319 | expect(reactElementToJSXString(
Hello "Jonh" and 'Mike'
)).toEqual( 320 | `
321 | Hello "Jonh" and 'Mike' 322 |
` 323 | ); 324 | }); 325 | 326 | it('reactElementToJSXString(
{`foo\nbar`}
)', () => { 327 | expect(reactElementToJSXString(
{`foo\nbar`}
)).toEqual( 328 | `
329 | foo 330 | bar 331 |
` 332 | ); 333 | 334 | expect( 335 | reactElementToJSXString( 336 |
337 |
{`foo\nbar`}
338 |
339 | ) 340 | ).toEqual( 341 | `
342 |
343 | foo 344 | bar 345 |
346 |
` 347 | ); 348 | }); 349 | 350 | it('reactElementToJSXString(
Hello
, {tabStop: 4})', () => { 351 | expect(reactElementToJSXString(
Hello
, { tabStop: 4 })).toEqual( 352 | `
353 | Hello 354 |
` 355 | ); 356 | }); 357 | 358 | it('reactElementToJSXString(
Hello
)', () => { 359 | expect( 360 | reactElementToJSXString( 361 |
362 |
Hello
363 |
364 | ) 365 | ).toEqual( 366 | `
367 |
368 | Hello 369 |
370 |
` 371 | ); 372 | }); 373 | 374 | it('reactElementToJSXString(
Hello
, {tabStop: 4})', () => { 375 | expect( 376 | reactElementToJSXString( 377 |
378 |
Hello
379 |
, 380 | { tabStop: 4 } 381 | ) 382 | ).toEqual( 383 | `
384 |
385 | Hello 386 |
387 |
` 388 | ); 389 | }); 390 | 391 | it('reactElementToJSXString(
Hello
)', () => { 392 | expect( 393 | reactElementToJSXString( 394 |
395 |
Hello
396 |
397 | ) 398 | ).toEqual( 399 | `
403 |
404 | Hello 405 |
406 |
` 407 | ); 408 | }); 409 | 410 | it('reactElementToJSXString(
Hello
, {tabStop: 4})', () => { 411 | expect( 412 | reactElementToJSXString( 413 |
414 |
Hello
415 |
, 416 | { 417 | tabStop: 4, 418 | } 419 | ) 420 | ).toEqual( 421 | `
425 |
426 | Hello 427 |
428 |
` 429 | ); 430 | }); 431 | 432 | it('reactElementToJSXString(
Hello
)', () => { 433 | expect( 434 | reactElementToJSXString( 435 |
436 |
Hello
437 |
438 | ) 439 | ).toEqual( 440 | `
448 |
455 | Hello 456 |
457 |
` 458 | ); 459 | }); 460 | 461 | it('reactElementToJSXString()', () => { 462 | expect(() => { 463 | reactElementToJSXString(); 464 | }).toThrow('react-element-to-jsx-string: Expected a ReactElement'); 465 | }); 466 | 467 | it('reactElementToJSXString(null)', () => { 468 | expect(() => { 469 | reactElementToJSXString(null); 470 | }).toThrow('react-element-to-jsx-string: Expected a ReactElement'); 471 | }); 472 | 473 | it('ignores object keys order (sortobject)', () => { 474 | expect(reactElementToJSXString(
)).toEqual( 475 | reactElementToJSXString(
) 476 | ); 477 | }); 478 | 479 | it('reactElementToJSXString(
', () => { 480 | expect(reactElementToJSXString(
)).toEqual( 481 | reactElementToJSXString(
) 482 | ); 483 | }); 484 | 485 | it('reactElementToJSXString(
', () => { 486 | expect(reactElementToJSXString(
)).toEqual( 487 | reactElementToJSXString(
) 488 | ); 489 | }); 490 | 491 | it('reactElementToJSXString(
', () => { 492 | expect( 493 | reactElementToJSXString(
) 494 | ).toEqual( 495 | `
` 500 | ); 501 | }); 502 | 503 | it('reactElementToJSXString(
, e: null}}}} />', () => { 504 | expect( 505 | reactElementToJSXString(
, e: null } } }} />) 506 | ).toEqual( 507 | `
, 512 | e: null 513 | } 514 | } 515 | }} 516 | />` 517 | ); 518 | }); 519 | 520 | it('reactElementToJSXString(
', () => { 521 | expect(reactElementToJSXString(
)).toEqual( 522 | `
` 527 | ); 528 | }); 529 | 530 | it('reactElementToJSXString(
', () => { 531 | expect(reactElementToJSXString(
)).toEqual('
'); 532 | }); 533 | 534 | it('reactElementToJSXString(
)', () => { 535 | expect( 536 | reactElementToJSXString( 537 |
538 | 539 | 540 |
541 | ) 542 | ).toEqual( 543 | `
544 | 545 | 546 |
` 547 | ); 548 | }); 549 | 550 | it('reactElementToJSXString(
foo
)', () => { 551 | expect( 552 | reactElementToJSXString( 553 |
554 | foo 555 |
556 |
557 | ) 558 | ).toEqual( 559 | `
560 | foo 561 |
562 |
` 563 | ); 564 | }); 565 | 566 | it('reactElementToJSXString(
\nfoo bar baz qux quux\n
)', () => { 567 | expect( 568 | reactElementToJSXString( 569 |
570 | foo bar baz qux quux 571 |
572 | ) 573 | ).toEqual(`
574 | foo bar{' '} 575 | 576 | {' '}baz{' '} 577 | 578 | {' '}qux quux 579 |
`); 580 | }); 581 | 582 | it('reactElementToJSXString(
', () => { 583 | expect(reactElementToJSXString(
)).toEqual( 584 | `
` 592 | ); 593 | }); 594 | 595 | it("reactElementToJSXString(
)", () => { 596 | expect( 597 | reactElementToJSXString(
) 598 | ).toEqual( 599 | `
` 606 | ); 607 | }); 608 | 609 | it('reactElementToJSXString(
', () => { 610 | expect(reactElementToJSXString(
)).toEqual( 611 | `
` 616 | ); 617 | }); 618 | 619 | it('reactElementToJSXString(
', () => { 620 | expect(reactElementToJSXString(
)).toEqual('
'); 621 | }); 622 | 623 | it('reactElementToJSXString(
]} />', () => { 624 | expect( 625 | reactElementToJSXString( 626 |
629 | 630 |
, 631 | ]} 632 | /> 633 | ) 634 | ).toEqual( 635 | `
638 | ]} 639 | />` 640 | ); 641 | }); 642 | 643 | it('reactElementToJSXString(
)', () => { 644 | expect(reactElementToJSXString(
)).toEqual( 645 | "
" 646 | ); 647 | }); 648 | 649 | it('reactElementToJSXString(
', () => { 650 | expect(reactElementToJSXString(
)).toEqual( 651 | `
` 655 | ); 656 | }); 657 | 658 | it('reactElementToJSXString(
', () => { 659 | expect( 660 | reactElementToJSXString( 661 |
662 | 663 |
664 | ) 665 | ).toEqual( 666 | `
670 | 674 |
` 675 | ); 676 | }); 677 | 678 | it('reactElementToJSXString(
', () => { 679 | expect(reactElementToJSXString(
)).toEqual( 680 | `
` 684 | ); 685 | }); 686 | 687 | it('reactElementToJSXString(
\\n {null}\\n
', () => { 688 | const element =
{null}
; 689 | 690 | expect(reactElementToJSXString(element)).toEqual('
'); 691 | }); 692 | 693 | it('reactElementToJSXString(
{true}
)', () => { 694 | expect(reactElementToJSXString(
{true}
)).toEqual('
'); 695 | }); 696 | 697 | it('reactElementToJSXString(
{false}
)', () => { 698 | expect(reactElementToJSXString(
{false}
)).toEqual('
'); 699 | }); 700 | 701 | it('reactElementToJSXString(
\n{false}\n
)', () => { 702 | expect(reactElementToJSXString(
{false}
)).toEqual('
'); 703 | }); 704 | 705 | it('reactElementToJSXString(
{false}
)', () => { 706 | expect(reactElementToJSXString(
{false}
)).toEqual( 707 | `
708 | {' '} 709 |
` 710 | ); 711 | }); 712 | 713 | it('reactElementToJSXString(
{null}
)', () => { 714 | expect(reactElementToJSXString(
{null}
)).toEqual('
'); 715 | }); 716 | 717 | it('reactElementToJSXString(
{123}
)', () => { 718 | expect(reactElementToJSXString(
{123}
)).toEqual( 719 | `
720 | 123 721 |
` 722 | ); 723 | }); 724 | 725 | it("reactElementToJSXString(
{''}
)", () => { 726 | expect(reactElementToJSXString(
{''}
)).toEqual( 727 | reactElementToJSXString(
) 728 | ); 729 | }); 730 | 731 | it('reactElementToJSXString(
String with {1} js expression
)', () => { 732 | expect( 733 | reactElementToJSXString(
String with {1} js number
) 734 | ).toEqual( 735 | `
736 | String with 1 js number 737 |
` 738 | ); 739 | }); 740 | 741 | it('reactElementToJSXString(, { displayName: toUpper })', () => { 742 | expect( 743 | reactElementToJSXString(, { 744 | displayName: element => element.type.name.toUpperCase(), 745 | }) 746 | ).toEqual(''); 747 | }); 748 | 749 | it("reactElementToJSXString(, { filterProps: ['key', 'className'] })", () => { 750 | expect( 751 | reactElementToJSXString( 752 | , 753 | { 754 | filterProps: ['key', 'className'], 755 | } 756 | ) 757 | ).toEqual(''); 758 | }); 759 | 760 | it("reactElementToJSXString(, { filterProps: () => !key.startsWith('some')) })", () => { 761 | expect( 762 | reactElementToJSXString( 763 | , 764 | { 765 | filterProps: (val, key) => !key.startsWith('some'), 766 | } 767 | ) 768 | ).toEqual(''); 769 | }); 770 | 771 | it('reactElementToJSXString(, { useBooleanShorthandSyntax: false })', () => { 772 | expect( 773 | reactElementToJSXString( 774 | , 775 | { 776 | useBooleanShorthandSyntax: false, 777 | } 778 | ) 779 | ).toEqual( 780 | `` 784 | ); 785 | }); 786 | 787 | it('should render default props', () => { 788 | expect(reactElementToJSXString()).toEqual( 789 | `` 795 | ); 796 | }); 797 | 798 | it('should not render default props if "showDefaultProps" option is false', () => { 799 | expect( 800 | reactElementToJSXString(, { 801 | showDefaultProps: false, 802 | }) 803 | ).toEqual(''); 804 | }); 805 | 806 | it('should render props that differ from their defaults if "showDefaultProps" option is false', () => { 807 | expect( 808 | reactElementToJSXString(, { 809 | showDefaultProps: false, 810 | }) 811 | ).toEqual(''); 812 | }); 813 | 814 | it('should render boolean props if value is `false`, default is `true` and "showDefaultProps" is false', () => { 815 | expect( 816 | reactElementToJSXString(, { 817 | showDefaultProps: false, 818 | }) 819 | ).toEqual(''); 820 | }); 821 | 822 | it('reactElementToJSXString(
} />, { displayName: toUpper })', () => { 823 | expect( 824 | reactElementToJSXString(
} />, { 825 | displayName: element => element.type.toUpperCase(), 826 | }) 827 | ).toEqual('
} />'); 828 | }); 829 | 830 | it('reactElementToJSXString(
}} />, { displayName: toUpper })', () => { 831 | expect( 832 | reactElementToJSXString(
}} />, { 833 | displayName: element => element.type.toUpperCase(), 834 | }) 835 | ).toEqual( 836 | `
839 | }} 840 | />` 841 | ); 842 | }); 843 | 844 | it('should omit true as value', () => { 845 | expect( 846 | reactElementToJSXString(
) // eslint-disable-line react/jsx-boolean-value 847 | ).toEqual('
'); 848 | }); 849 | 850 | it('should omit attributes with false as value', () => { 851 | expect( 852 | reactElementToJSXString(
) // eslint-disable-line react/jsx-boolean-value 853 | ).toEqual('
'); 854 | }); 855 | 856 | it('should return the actual functions when "showFunctions" is true', () => { 857 | /* eslint-disable arrow-body-style */ 858 | const fn = () => { 859 | return 'value'; 860 | }; 861 | 862 | expect( 863 | reactElementToJSXString(
, { 864 | showFunctions: true, 865 | }) 866 | ).toEqual(`
`); 867 | }); 868 | 869 | it('should expose the multiline "functionValue" formatter', () => { 870 | /* eslint-disable arrow-body-style */ 871 | const fn = () => { 872 | return 'value'; 873 | }; 874 | 875 | expect( 876 | reactElementToJSXString(
, { 877 | showFunctions: true, 878 | functionValue: preserveFunctionLineBreak, 879 | }) 880 | ).toEqual(`
`); 885 | }); 886 | 887 | it('reactElementToJSXString()', () => { 888 | expect(reactElementToJSXString()).toEqual( 889 | '' 890 | ); 891 | }); 892 | 893 | // maxInlineAttributesLineLength tests 894 | // Validate two props will stay inline if their length is less than the option 895 | it('reactElementToJSXString(
, { maxInlineAttributesLineLength: 100 }))', () => { 896 | expect( 897 | reactElementToJSXString(
, { 898 | maxInlineAttributesLineLength: 100, 899 | }) 900 | ).toEqual('
'); 901 | }); 902 | // Validate one prop will go to new line if length is greater than option. One prop is a special case since 903 | // the old logic operated on whether or not two or more attributes were present. Making sure this overrides 904 | // that older logic 905 | it('reactElementToJSXString(
, { maxInlineAttributesLineLength: 5 }))', () => { 906 | expect( 907 | reactElementToJSXString(
, { 908 | maxInlineAttributesLineLength: 5, 909 | }) 910 | ).toEqual( 911 | `
` 914 | ); 915 | }); 916 | // Validate two props will go be multiline if their length is greater than the given option 917 | it('reactElementToJSXString(
, { maxInlineAttributesLineLength: 10 }))', () => { 918 | expect( 919 | reactElementToJSXString(
, { 920 | maxInlineAttributesLineLength: 10, 921 | }) 922 | ).toEqual( 923 | `
` 927 | ); 928 | }); 929 | 930 | // Same tests as above but with elements that have children. The closing braces for elements with children and without children 931 | // run through different code paths so we have both sets of test to specify the behavior of both when this option is present 932 | it('reactElementToJSXString(
content
, { maxInlineAttributesLineLength: 100 }))', () => { 933 | expect( 934 | reactElementToJSXString( 935 |
936 | content 937 |
, 938 | { 939 | maxInlineAttributesLineLength: 100, 940 | } 941 | ) 942 | ).toEqual( 943 | `
944 | content 945 |
` 946 | ); 947 | }); 948 | it('reactElementToJSXString(
content
, { maxInlineAttributesLineLength: 5 }))', () => { 949 | expect( 950 | reactElementToJSXString(
content
, { 951 | maxInlineAttributesLineLength: 5, 952 | }) 953 | ).toEqual( 954 | `
957 | content 958 |
` 959 | ); 960 | }); 961 | it('reactElementToJSXString(
content
, { maxInlineAttributesLineLength: 10 }))', () => { 962 | expect( 963 | reactElementToJSXString( 964 |
965 | content 966 |
, 967 | { 968 | maxInlineAttributesLineLength: 10, 969 | } 970 | ) 971 | ).toEqual( 972 | `
976 | content 977 |
` 978 | ); 979 | }); 980 | 981 | // Multi-level inline attribute test 982 | it('reactElementToJSXString(
content
, { maxInlineAttributesLineLength: 24 }))', () => { 983 | expect( 984 | reactElementToJSXString( 985 |
986 |
987 | content 988 |
989 |
, 990 | { 991 | maxInlineAttributesLineLength: 24, 992 | } 993 | ) 994 | ).toEqual( 995 | `
996 |
1000 | content 1001 |
1002 |
` 1003 | ); 1004 | }); 1005 | it('should return functionValue result when it returns a string', () => { 1006 | expect( 1007 | reactElementToJSXString(
'value'} />, { 1008 | showFunctions: true, 1009 | functionValue: () => '...', 1010 | }) 1011 | ).toEqual('
'); 1012 | }); 1013 | it('sends the original fn to functionValue', () => { 1014 | const fn = () => {}; 1015 | const functionValue = receivedFn => expect(receivedFn).toBe(fn); 1016 | reactElementToJSXString(
, { functionValue }); 1017 | }); 1018 | it('should return noRefCheck when "showFunctions" is false and "functionValue" is not provided', () => { 1019 | expect(reactElementToJSXString(
{}} />)).toEqual( 1020 | '
' 1021 | ); 1022 | }); 1023 | 1024 | it('reactElementToJSXString(

foo

bar

)', () => { 1025 | expect( 1026 | reactElementToJSXString( 1027 | 1028 |

foo

1029 |

bar

1030 |
1031 | ) 1032 | ).toEqual( 1033 | `<> 1034 |

1035 | foo 1036 |

1037 |

1038 | bar 1039 |

1040 | ` 1041 | ); 1042 | }); 1043 | 1044 | it('reactElementToJSXString(
)', () => { 1045 | expect( 1046 | reactElementToJSXString( 1047 | 1048 |
1049 |
1050 | 1051 | ) 1052 | ).toEqual( 1053 | ` 1054 |
1055 |
1056 | ` 1057 | ); 1058 | }); 1059 | 1060 | it('reactElementToJSXString()', () => { 1061 | expect(reactElementToJSXString()).toEqual(``); 1062 | }); 1063 | 1064 | it('reactElementToJSXString(
} />)', () => { 1065 | expect( 1066 | reactElementToJSXString( 1067 |
1070 |
1071 |
1072 | 1073 | } 1074 | /> 1075 | ) 1076 | ).toEqual(`
} />`); 1077 | }); 1078 | 1079 | it('should not cause recursive loop when prop object contains an element', () => { 1080 | const Test = () =>
Test
; 1081 | 1082 | const Container = ({ title: { component } }) =>
{component}
; 1083 | 1084 | class App extends Component { 1085 | render() { 1086 | const inside = }} />; 1087 | 1088 | const insideString = reactElementToJSXString(inside); 1089 | 1090 | return ( 1091 |
1092 | {insideString} 1093 | 1094 |
Hello world!
1095 | 1096 |

Start editing to see some magic happen :)

1097 |
1098 | ); 1099 | } 1100 | } 1101 | 1102 | render(); 1103 | 1104 | expect(screen.getByText('Hello world!')).toBeInTheDocument(); 1105 | }); 1106 | 1107 | it('should not cause recursive loop when an element contains a ref', () => { 1108 | expect.assertions(2); 1109 | 1110 | class App extends Component { 1111 | constructor(props) { 1112 | super(props); 1113 | this.inputRef = React.createRef(); 1114 | } 1115 | componentDidMount() { 1116 | expect(reactElementToJSXString()).toEqual( 1117 | `` 1122 | ); 1123 | } 1124 | render() { 1125 | return ( 1126 | <> 1127 | 1128 |
Hello world!
1129 | 1130 | ); 1131 | } 1132 | } 1133 | 1134 | render(); 1135 | 1136 | expect(screen.getByText('Hello world!')).toBeInTheDocument(); 1137 | }); 1138 | 1139 | it('should use inferred function name as display name for `forwardRef` element', () => { 1140 | const Tag = React.forwardRef(function Tag({ text }, ref) { 1141 | return {text}; 1142 | }); 1143 | expect(reactElementToJSXString()).toEqual( 1144 | `` 1145 | ); 1146 | }); 1147 | 1148 | it('should use `displayName` instead of inferred function name as display name for `forwardRef` element', () => { 1149 | const Tag = React.forwardRef(function Tag({ text }, ref) { 1150 | return {text}; 1151 | }); 1152 | Tag.displayName = 'MyTag'; 1153 | expect(reactElementToJSXString()).toEqual( 1154 | `` 1155 | ); 1156 | }); 1157 | 1158 | it('should use inferred function name as display name for `memo` element', () => { 1159 | const Tag = React.memo(function Tag({ text }) { 1160 | return {text}; 1161 | }); 1162 | expect(reactElementToJSXString()).toEqual( 1163 | `` 1164 | ); 1165 | }); 1166 | 1167 | it('should use `displayName` instead of inferred function name as display name for `memo` element', () => { 1168 | const Tag = React.memo(function Tag({ text }) { 1169 | return {text}; 1170 | }); 1171 | Tag.displayName = 'MyTag'; 1172 | expect(reactElementToJSXString()).toEqual( 1173 | `` 1174 | ); 1175 | }); 1176 | 1177 | it('should use inferred function name as display name for a `forwardRef` wrapped in `memo`', () => { 1178 | const Tag = React.memo( 1179 | React.forwardRef(function Tag({ text }, ref) { 1180 | return {text}; 1181 | }) 1182 | ); 1183 | expect(reactElementToJSXString()).toEqual( 1184 | `` 1185 | ); 1186 | }); 1187 | 1188 | it('should use inferred function name as display name for a component wrapped in `memo` multiple times', () => { 1189 | const Tag = React.memo( 1190 | React.memo( 1191 | React.memo(function Tag({ text }) { 1192 | return {text}; 1193 | }) 1194 | ) 1195 | ); 1196 | expect(reactElementToJSXString()).toEqual( 1197 | `` 1198 | ); 1199 | }); 1200 | 1201 | it('should stringify `StrictMode` correctly', () => { 1202 | const App = () => null; 1203 | 1204 | expect( 1205 | reactElementToJSXString( 1206 | 1207 | 1208 | 1209 | ) 1210 | ).toEqual(` 1211 | 1212 | `); 1213 | }); 1214 | 1215 | it('should stringify `Suspense` correctly', () => { 1216 | const Spinner = () => null; 1217 | const ProfilePage = () => null; 1218 | 1219 | expect( 1220 | reactElementToJSXString( 1221 | }> 1222 | 1223 | 1224 | ) 1225 | ).toEqual(`}> 1226 | 1227 | `); 1228 | }); 1229 | 1230 | it('should stringify `Profiler` correctly', () => { 1231 | const Navigation = () => null; 1232 | 1233 | expect( 1234 | reactElementToJSXString( 1235 | {}}> 1236 | 1237 | 1238 | ) 1239 | ).toEqual(` 1243 | 1244 | `); 1245 | }); 1246 | 1247 | it('should stringify `Contex.Provider` correctly', () => { 1248 | const Ctx = React.createContext(); 1249 | const App = () => {}; 1250 | 1251 | expect( 1252 | reactElementToJSXString( 1253 | 1254 | 1255 | 1256 | ) 1257 | ).toEqual(` 1258 | 1259 | `); 1260 | }); 1261 | 1262 | it('should stringify `Context` correctly', () => { 1263 | const Ctx = React.createContext(); 1264 | const App = () => {}; 1265 | 1266 | expect( 1267 | reactElementToJSXString( 1268 | 1269 | 1270 | 1271 | ) 1272 | ).toEqual(` 1273 | 1274 | `); 1275 | }); 1276 | 1277 | it('should stringify `Contex.Provider` with `displayName` correctly', () => { 1278 | const Ctx = React.createContext(); 1279 | Ctx.displayName = 'MyCtx'; 1280 | 1281 | const App = () => {}; 1282 | 1283 | expect( 1284 | reactElementToJSXString( 1285 | 1286 | 1287 | 1288 | ) 1289 | ).toEqual(` 1290 | 1291 | `); 1292 | }); 1293 | 1294 | it('should stringify `Contex.Consumer` correctly', () => { 1295 | const Ctx = React.createContext(); 1296 | const Button = () => null; 1297 | 1298 | expect( 1299 | reactElementToJSXString( 1300 | {theme =>