├── .babelrc.js ├── .eslintrc.json ├── .github └── workflows │ └── codecov.yml ├── .gitignore ├── .npmignore ├── .prettierignore ├── .prettierrc.json ├── .vscode └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── __test__ ├── __snapshots__ │ └── stringToReact.test.js.snap ├── ctx.test.js ├── mock-module.js └── stringToReact.test.js ├── example ├── index.html └── stories │ ├── data-prop │ └── README.md │ ├── env-preset │ └── README.md │ ├── filename-option │ └── README.md │ ├── styles.css │ ├── typescript │ └── README.md │ ├── usage │ └── README.md │ ├── using-react-hooks │ └── README.md │ └── using-unkown-elements │ └── README.md ├── index.d.ts ├── package-lock.json ├── package.json ├── rollup.config.js ├── src ├── ctx.tsx ├── index.ts ├── strintToReact.tsx └── types.d.ts ├── styleguide.config.js ├── tsconfig.json ├── webpack.config.js └── yarn.lock /.babelrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const output = process.env.BABEL_OUTPUT; 4 | const requirePolyfills = process.env.INCLUDE_POLYFILLS; 5 | const modules = output == null ? false : output; 6 | const options = { 7 | presets: [ 8 | ['@babel/env', {loose: true, modules}], 9 | '@babel/react', 10 | [ 11 | '@babel/preset-typescript', 12 | { 13 | isTSX: true, 14 | allExtensions: true, 15 | }, 16 | ], 17 | ], 18 | plugins: ['@babel/plugin-transform-react-jsx'], 19 | env: { 20 | test: { 21 | // extra configuration for process.env.NODE_ENV === 'test' 22 | presets: ['@babel/env'], // overwrite env-config from above with transpiled module syntax 23 | }, 24 | }, 25 | }; 26 | if (requirePolyfills) { 27 | options.plugins.push(['@babel/plugin-transform-runtime', {corejs: 3}]); 28 | } 29 | module.exports = options; 30 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true, 5 | "es6": true, 6 | "node": true, 7 | "jest": true 8 | }, 9 | "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:prettier/recommended"], 10 | "settings": { 11 | "react": { 12 | "version": "detect" 13 | } 14 | }, 15 | "parser": "@babel/eslint-parser", 16 | "parserOptions": { 17 | "ecmaFeatures": { 18 | "jsx": true 19 | }, 20 | "ecmaVersion": 12, 21 | "sourceType": "module", 22 | "allowImportExportEverywhere": false 23 | }, 24 | "plugins": ["eslint-plugin-react", "eslint-plugin-prettier"], 25 | "rules": { 26 | "prettier/prettier": [ 27 | "error", 28 | { 29 | "endOfLine": "auto" 30 | } 31 | ] 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.github/workflows/codecov.yml: -------------------------------------------------------------------------------- 1 | name: Running Code Coverage 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | permissions: 9 | contents: write 10 | pull-requests: write 11 | 12 | strategy: 13 | matrix: 14 | node-version: [18.x, 21.x] 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v2 19 | with: 20 | # Fine-grained PAT with contents:write and workflows:write 21 | # scopes 22 | token: ${{ secrets.GITHUB_TOKEN }} 23 | 24 | - name: Set up Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | 29 | - name: Install dependencies 30 | run: npm install 31 | 32 | - name: Run tests 33 | run: npm run test 34 | 35 | - name: Upload coverage to Codecov 36 | uses: codecov/codecov-action@v1 37 | with: 38 | token: ${{ secrets.CODECOV_TOKEN }} 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | #ide 2 | 3 | # dependencies 4 | /node_modules 5 | example/node_modules 6 | /.pnp 7 | .pnp.js 8 | 9 | # testing 10 | /coverage 11 | /sandbox 12 | 13 | # production 14 | /build 15 | /lib 16 | /dist 17 | 18 | # demo 19 | /demo 20 | 21 | # misc 22 | .DS_Store 23 | .env.local 24 | .env.development.local 25 | .env.test.local 26 | .env.production.local 27 | 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | #ide 2 | /.vscode 3 | 4 | # dependencies 5 | /node_modules 6 | 7 | # production 8 | /.github 9 | 10 | #development 11 | /build 12 | /sandbox 13 | 14 | #demo 15 | /example 16 | /demo 17 | 18 | # testing 19 | /coverage 20 | 21 | # misc 22 | .env.local 23 | .env.development.local 24 | .env.test.local 25 | .env.production.local 26 | 27 | npm-debug.log* 28 | yarn-debug.log* 29 | yarn-error.log* 30 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | #ide 2 | /.vscode 3 | 4 | # dependencies 5 | /node_modules 6 | example/node_modules 7 | /.pnp 8 | .pnp.js 9 | 10 | # testing 11 | /coverage 12 | 13 | # production 14 | /build 15 | /lib 16 | /dist 17 | 18 | # demo 19 | /demo 20 | 21 | # misc 22 | .DS_Store 23 | .env.local 24 | .env.development.local 25 | .env.test.local 26 | .env.production.local 27 | 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSpacing": false, 4 | "jsxBracketSameLine": true, 5 | "printWidth": 120, 6 | "semi": true, 7 | "singleQuote": true, 8 | "tabWidth": 2, 9 | "trailingComma": "all", 10 | "useTabs": false, 11 | "proseWrap": "never" 12 | } 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode" 3 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## version 4.0.1 2 | 3 | Improve documentation 4 | 5 | ## version 4.0.0 6 | 7 | - Fix source map issue 8 | 9 | - Fix next js issue 10 | 11 | - default sourceMaps value is `inline` 12 | 13 | - Now string code can be empty 14 | 15 | - Improve error handling 16 | 17 | - Remove global `React` variable 18 | 19 | - Change version of `@babel/standalone` 20 | 21 | ## version 3.1.1 22 | 23 | - Update dependencies 24 | 25 | - Provide Online Examples 26 | 27 | ## version 3.1.0 28 | 29 | - Adding `@babel/standalone` package into `peerDependencies` list 30 | 31 | - using typescript 32 | 33 | ## version 3.0.0 34 | 35 | - `data` prop should be used for passing unknown elements to the component 36 | 37 | - adding `babelOptions` prop 38 | 39 | - update peerDependencies versions 40 | 41 | ## version 2.0.0 42 | 43 | - upgrade react and react-dom peerDependencies to v18.2.0 44 | 45 | ## version 1.0.1 46 | 47 | - update README.md 48 | 49 | ## version 1.0.0 50 | 51 | - update peerDependencies : 52 | 53 | update react and react-dom to 18.1.0 54 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## Semantic Versioning 4 | 5 | string-to-react-component follows semantic versioning. We release patch versions for critical bugfixes, minor versions for new features or non-essential changes, and major versions for any breaking changes. 6 | 7 | ## Proposing a Change 8 | 9 | Patches for bugfixes are always welcome. Please accompany pull requests for bugfixes with a test case that is fixed by the PR. If you want to implement a new feature, It is advised to open an issue first in the GitHub. 10 | 11 | ## Before submitting a pull request, please make sure the following is done: 12 | 13 | - Fork the repository and create your branch from main. 14 | - If you’ve fixed a bug or added code that should be tested, add tests. 15 | - Ensure the test suite passes : `$ npm run test` 16 | - Format your code with prettier. 17 | - Make sure you don't check-in any ESLint violations : `$ npm run lint` 18 | - Update README with appropriate docs. 19 | - Commit and PR 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 js dev 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # string-to-react-component 2 | 3 | Dynamically create and render React components from strings at runtime, converting strings to React components for flexible UI generation 4 | 5 | [![Test coverage](https://codecov.io/gh/dev-javascript/string-to-react-component/graph/badge.svg?token=GT1LU074L2)](https://codecov.io/gh/dev-javascript/string-to-react-component) [![NPM version](https://img.shields.io/npm/v/string-to-react-component.svg?style=flat-square)](http://npmjs.org/package/string-to-react-component) [![node](https://img.shields.io/badge/node.js-%3E=_8.0-green.svg?style=flat-square)](http://nodejs.org/download/) [![React](https://img.shields.io/badge/React-%3E=_16.8.0-green.svg?style=flat-square)](https://react.dev/) [![License](https://img.shields.io/npm/l/string-to-react-component.svg?style=flat-square)](LICENSE) [![npm download](https://img.shields.io/npm/dm/string-to-react-component.svg?style=flat-square)](https://npmjs.org/package/string-to-react-component) [![Build Status](https://travis-ci.org/ly-components/string-to-react-component.png)](https://travis-ci.org/ly-components/string-to-react-component) 6 | 7 | ## Demo 8 | 9 | - [Online Demo](https://dev-javascript.github.io/string-to-react-component/) 10 | 11 | ## Table of Contents 12 | 13 | 14 | 15 | - [Installation](#installation) 16 | - [Basic Example](#basic-example) 17 | - [Using Unknown Elements](#using-unknown-elements) 18 | - [props](#props) 19 | - [data](#data) 20 | - [babelOptions](#babelOptions) 21 | - [Caveats](#caveats) 22 | - [Test](#test) 23 | - [License](#license) 24 | 25 | 26 | 27 | ## Installation 28 | 29 | ```js 30 | # with npm 31 | $ npm install string-to-react-component @babel/standalone --save 32 | 33 | # with yarn 34 | yarn add string-to-react-component @babel/standalone 35 | ``` 36 | 37 | ### CDN Links 38 | 39 | ```js 40 | 41 | 42 | // This will create a global function StringToReactComponent 43 | ``` 44 | 45 | ## Basic Example 46 | 47 | ```js 48 | import StringToReactComponent from 'string-to-react-component'; 49 | function App() { 50 | return ( 51 | 52 | {`(props)=>{ 53 | const [counter,setCounter]=React.useState(0); // by default your code has access to the React object 54 | const increase=()=>{ 55 | setCounter(counter+1); 56 | }; 57 | return (<> 58 | 59 | {'counter : '+ counter} 60 | ); 61 | }`} 62 | 63 | ); 64 | } 65 | ``` 66 | 67 | ### Notes 68 | 69 | - The given code inside the string should be a function. 70 | 71 | - The code inside the string has access to the `React` object and for using `useState`, `useEffect`, `useRef` and ... you should get them from `React` object or pass them as `data` prop to the component: 72 | 73 | ```js 74 | import {useState} from 'react'; 75 | import StringToReactComponent from 'string-to-react-component'; 76 | function App() { 77 | return ( 78 | 79 | {`(props)=>{ 80 | console.log(typeof useState); // undefined 81 | console.log(typeof React.useState); // function 82 | console.log(typeof props.useState); // function 83 | ... 84 | 85 | }`} 86 | 87 | ); 88 | } 89 | ``` 90 | 91 | ## Using Unknown Elements 92 | 93 | ```js 94 | import StringToReactComponent from 'string-to-react-component'; 95 | import MyFirstComponent from 'path to MyFirstComponent'; 96 | import MySecondComponent from 'path to MySecondComponent'; 97 | function App() { 98 | return ( 99 | 100 | {`(props)=>{ 101 | const {MyFirstComponent, MySecondComponent}=props; 102 | return (<> 103 | 104 | 105 | ); 106 | }`} 107 | 108 | ); 109 | } 110 | ``` 111 | 112 | ## props 113 | 114 | ### data 115 | 116 | - type : `object` 117 | - required : `No` 118 | - `data` object is passed to the component(which is generated from the string) as props 119 | - example : 120 | 121 | ```js 122 | import {useState} from 'react'; 123 | import StringToReactComponent from 'string-to-react-component'; 124 | function App() { 125 | const [counter, setCounter] = useState(0); 126 | const increase = () => { 127 | setCounter(counter + 1); 128 | }; 129 | return ( 130 | 131 | {`(props)=>{ 132 | return (<> 133 | 134 | {'counter : '+ props.counter} 135 | ); 136 | }`} 137 | 138 | ); 139 | } 140 | ``` 141 | 142 | ### babelOptions 143 | 144 | - type : `object` 145 | - required : `No` 146 | - default value : `{presets: ["react"],sourceMaps: "inline"}` 147 | - See the full option list [here](https://babeljs.io/docs/en/options) 148 | - examples : 149 | - using typescript : 150 | ```js 151 | 153 | {`()=>{ 154 | const [counter,setCounter]=React.useState(0); 155 | const increase=()=>{ 156 | setCounter(counter+1); 157 | }; 158 | return (<> 159 | 160 | {'counter : '+ counter} 161 | ); 162 | }`} 163 | 164 | ``` 165 | 166 | ## Caveats 167 | 168 | This plugin does not use `eval` function, however, suffers from security and might expose you to XSS attacks 169 | 170 | To prevent XSS attacks, You should sanitize user input before storing it. 171 | 172 | ## Test 173 | 174 | ```js 175 | $ npm run test 176 | ``` 177 | 178 | ## License 179 | 180 | MIT 181 | -------------------------------------------------------------------------------- /__test__/__snapshots__/stringToReact.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`snapshots : data props should be passed as props to generated component 1`] = ` 4 |
8 | `; 9 | 10 | exports[`snapshots : default render structure 1`] = `null`; 11 | -------------------------------------------------------------------------------- /__test__/ctx.test.js: -------------------------------------------------------------------------------- 1 | import Ctx from '../src/ctx'; 2 | import React from 'react'; 3 | import * as Babel from '@babel/standalone'; 4 | beforeEach(() => {}); 5 | afterEach(() => {}); 6 | describe('constructor :', () => { 7 | test('it should work correctly without errors', () => { 8 | new Ctx(React, Babel); 9 | expect(1).toBe(1); 10 | }); 11 | test('it should throw an error when Babel value is not passed in to it', () => { 12 | expect.assertions(1); 13 | try { 14 | new Ctx(React, undefined); 15 | } catch (er) { 16 | expect(er.message).toBe( 17 | `Package "string-to-react-component" has a missing peer dependency of "@babel/standalone" ( requires ">=7.15.8" )`, 18 | ); 19 | } 20 | }); 21 | test('check _getReact property', () => { 22 | const ins = new Ctx(React, Babel); 23 | expect(ins._getReact()).toEqual(React); 24 | }); 25 | test('check _getBabel property', () => { 26 | const ins = new Ctx(React, Babel); 27 | expect(ins._getBabel()).toEqual(Babel); 28 | }); 29 | test('it should set _rerender property', () => { 30 | const rerender = () => {}; 31 | const ins = new Ctx(React, Babel, rerender); 32 | expect(ins._rerender).toEqual(rerender); 33 | }); 34 | test('the initial value of _com prop should be a function which returns null', () => { 35 | const ins = new Ctx(React, Babel); 36 | expect(typeof ins._com).toBe('function'); 37 | expect(ins._com()).toBe(null); 38 | }); 39 | }); 40 | describe('methods : ', () => { 41 | test('_validateTemplate method ', () => { 42 | expect.assertions(3); 43 | const ins = new Ctx(React, Babel); 44 | try { 45 | ins._validateTemplate({}); 46 | } catch (er) { 47 | expect(er.message).toBe('passed child into string-to-react-component element should b a string'); 48 | } 49 | try { 50 | ins._validateTemplate(); 51 | } catch (er) { 52 | expect(er.message).toBe('passed child into string-to-react-component element should b a string'); 53 | } 54 | try { 55 | ins._validateTemplate(''); 56 | } catch (er) { 57 | expect(er.message).toBe('passed string into string-to-react-component element can not be empty'); 58 | } 59 | }); 60 | test('_validateCodeInsideTheTemp method', () => { 61 | expect.assertions(3); 62 | const ins = new Ctx(React, Babel); 63 | { 64 | ins._validateCodeInsideTheTemp(() => {}); 65 | expect(1).toBe(1); 66 | } 67 | { 68 | class c { 69 | constructor() {} 70 | } 71 | ins._validateCodeInsideTheTemp(c); 72 | expect(1).toBe(1); 73 | } 74 | try { 75 | ins._validateCodeInsideTheTemp({}); 76 | } catch (er) { 77 | expect(er.message).toBe('code inside the passed string into string-to-react-component, should be a function'); 78 | } 79 | }); 80 | test('_getBlob method', () => { 81 | const ins = new Ctx(React, Babel); 82 | const blob = ins._getBlob('()=>{}'); 83 | expect(!!blob.size).toBe(true); 84 | expect(blob.type).toBe('application/javascript'); 85 | }); 86 | test('update method should call _update', () => { 87 | const ins = new Ctx(React, Babel); 88 | ins._update = jest.fn(() => {}); 89 | const str = '()=>{}'; 90 | const babelOptions = {}; 91 | ins.update(str, babelOptions); 92 | expect(ins._update.mock.calls.length).toBe(1); 93 | expect(ins._update.mock.calls[0][0]).toBe(str); 94 | expect(ins._update.mock.calls[0][1]).toBe(babelOptions); 95 | }); 96 | test('_update method', () => { 97 | const ins = new Ctx(React, Babel); 98 | ins._updateTemplate = jest.fn((template, babelOptions) => 'transpiled string code'); 99 | ins._updateComponent = jest.fn((temp, babelOp) => {}); 100 | const str = '()=>{}'; 101 | const babelOptions = {}; 102 | ins._update(str, babelOptions); 103 | expect(ins._updateTemplate.mock.calls.length).toBe(1); 104 | expect(ins._updateTemplate.mock.calls[0][0]).toBe(str); 105 | expect(ins._updateTemplate.mock.calls[0][1]).toBe(babelOptions); 106 | expect(ins._updateComponent.mock.calls.length).toBe(1); 107 | expect(ins._updateComponent.mock.calls[0][0]).toBe('transpiled string code'); 108 | }); 109 | test('_checkBabelOptions method should set react preset and inline sourceMaps and throw an error with invalid parameter', () => { 110 | expect.assertions(6); 111 | const ins = new Ctx(React, Babel); 112 | try { 113 | ins._checkBabelOptions([]); 114 | } catch (e) { 115 | expect(e.message).toBe(`babelOptions prop of string-to-react-component element should be an object.`); 116 | } 117 | try { 118 | ins._checkBabelOptions({presets: {}}); 119 | } catch (e) { 120 | expect(e.message).toBe( 121 | `string-to-react-component Error : presets property of babelOptions prop should be an array`, 122 | ); 123 | } 124 | let babelOp = {}; 125 | ins._checkBabelOptions(babelOp); 126 | expect(babelOp.presets.indexOf('react') >= 0).toBe(true); 127 | expect(babelOp.sourceMaps).toBe('inline'); 128 | babelOp = {presets: []}; 129 | ins._checkBabelOptions(babelOp); 130 | expect(babelOp.presets.indexOf('react') >= 0).toBe(true); 131 | expect(babelOp.sourceMaps).toBe('inline'); 132 | }); 133 | test('_transpile method should override _temp to "null" when _temp is an empty string', () => { 134 | const ins = new Ctx(React, Babel); 135 | ins._temp = ''; 136 | ins._transpile({sourceMaps: false}); 137 | expect(ins._temp).toBe('null'); 138 | }); 139 | test('_transpile method should override _temp to the transpiled code', () => { 140 | const ins = new Ctx(React, Babel); 141 | ins._temp = `()=>
2
`; 142 | const code = ins._transpile({sourceMaps: false}); 143 | expect( 144 | [ 145 | '() => React.createElement("div", null, "2");', 146 | `() => /*#__PURE__*/React.createElement("div", null, "2");`, 147 | ].indexOf(ins._temp) >= 0, 148 | ).toBe(true); 149 | }); 150 | test('_import method', async () => { 151 | expect.assertions(1); 152 | const ins = new Ctx(React, Babel); 153 | await ins._import('../__test__/mock-module.js').then((res) => { 154 | expect(res.default || res).toBe('mock-module'); 155 | }); 156 | }); 157 | test('_updateComponent method', async () => { 158 | const ins = new Ctx(React, Babel); 159 | const blob = new Blob(); 160 | const com = () => 3; 161 | ins._getBlob = jest.fn(() => blob); 162 | ins._getModule = jest.fn(() => Promise.resolve(com)); 163 | ins._rerender = jest.fn(() => {}); 164 | const str = '()=>{}'; 165 | await ins._updateComponent(str); 166 | expect(ins._getBlob.mock.calls.length).toBe(1); 167 | expect(ins._getBlob.mock.calls[0][0]).toBe(str); 168 | expect(ins._com()).toBe(com()); 169 | expect(ins._rerender.mock.calls.length).toBe(1); 170 | expect(ins._rerender.mock.calls[0][0]).toEqual({}); 171 | }); 172 | }); 173 | describe('_getModule method', () => { 174 | beforeEach(() => { 175 | global.URL.createObjectURL = jest.fn(() => 'mocked-url'); 176 | global.URL.revokeObjectURL = jest.fn(() => {}); 177 | }); 178 | 179 | it('should successfully load a module and return the expected component', async () => { 180 | const instance = new Ctx(React, Babel); 181 | const mockReactComponent = jest.fn((React) => {}); 182 | instance._import = jest.fn(() => { 183 | return Promise.resolve({default: mockReactComponent}); 184 | }); 185 | const mockBlob = new Blob(); 186 | const result = await instance._getModule(mockBlob); 187 | expect(instance._import).toHaveBeenCalled(); 188 | expect(result).toBe(mockReactComponent(instance._getReact())); 189 | }); 190 | 191 | it('should handle errors during module loading', async () => { 192 | expect.assertions(4); 193 | const instance = new Ctx(React, Babel); 194 | const mockReactComponent = jest.fn((React) => {}); 195 | const mockError = new Error('Module loading failed'); 196 | instance._import = jest.fn(() => { 197 | return Promise.reject(mockError); 198 | }); 199 | const mockBlob = new Blob(); // Create a mock Blob 200 | expect(global.URL.revokeObjectURL.mock.calls.length).toBe(0); 201 | // Ensure that the error is logged to the console 202 | const consoleErrorSpy = jest.spyOn(console, 'error'); 203 | await instance._getModule(mockBlob).catch((er) => { 204 | expect(er.message).toBe('string-to-react-component loading module is failed:'); 205 | expect(global.URL.revokeObjectURL.mock.calls.length).toBe(1); 206 | expect(consoleErrorSpy).toHaveBeenCalledWith('string-to-react-component loading module is failed:', mockError); 207 | consoleErrorSpy.mockRestore(); // Restore original console.error 208 | }); 209 | }); 210 | }); 211 | describe('_updateTemplate method : ', () => { 212 | let ins; 213 | beforeEach(() => { 214 | ins = new Ctx(React, Babel); 215 | }); 216 | test('_updateTemplate should call _validateTemplate method', () => { 217 | ins._validateTemplate = jest.fn(() => {}); 218 | ins._updateTemplate('()=>{}', {}); 219 | expect(ins._validateTemplate.mock.calls.length).toBe(1); 220 | expect(ins._validateTemplate.mock.calls[0][0]).toBe('()=>{}'); 221 | }); 222 | test('_updateTemplate method should call _prependCode, _transpile and _postpendCode methods', () => { 223 | ins._prependCode = jest.fn(() => { 224 | ins._transpile = jest.fn(() => { 225 | ins._postpendCode = jest.fn(() => ins._temp); 226 | return ins; 227 | }); 228 | return ins; 229 | }); 230 | ins._updateTemplate('()=>{}', {}); 231 | expect(ins._prependCode.mock.calls.length).toBe(1); 232 | expect(ins._prependCode.mock.calls[0][0]).toBe('()=>{}'); 233 | expect(ins._transpile.mock.calls.length).toBe(1); 234 | expect(ins._transpile.mock.calls[0][0]).toEqual({}); 235 | expect(ins._postpendCode.mock.calls.length).toBe(1); 236 | }); 237 | }); 238 | -------------------------------------------------------------------------------- /__test__/mock-module.js: -------------------------------------------------------------------------------- 1 | export default 'mock-module'; 2 | -------------------------------------------------------------------------------- /__test__/stringToReact.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import * as Babel from '@babel/standalone'; 4 | import {render, unmountComponentAtNode} from 'react-dom'; 5 | import {act} from 'react-dom/test-utils'; 6 | import StrintToReact from '../src/strintToReact'; 7 | import Ctx from '../src/ctx'; 8 | const react = React; 9 | let container = document.createElement('div'); 10 | const str = `()=>

some text

`; 11 | const str2 = `()=>

some text2

`; 12 | let renderApp; 13 | beforeAll(() => { 14 | document.body.appendChild(container); 15 | }); 16 | beforeEach(() => { 17 | renderApp = (temp, babelOptions, deps, rerender, temp2, babelOptions2) => { 18 | let secondRender = false; 19 | const StrintToReactCom = StrintToReact.bind(undefined, deps); 20 | const App = function () { 21 | const template = secondRender ? temp2 || str : temp || str; 22 | const babelOp = secondRender ? babelOptions2 : babelOptions; 23 | return {template}; 24 | }; 25 | act(() => { 26 | render(, container); 27 | }); 28 | if (rerender) { 29 | secondRender = true; 30 | act(() => { 31 | render(, container); 32 | }); 33 | } 34 | }; 35 | }); 36 | afterEach(() => { 37 | unmountComponentAtNode(container); 38 | container.innerHTML = ''; 39 | renderApp = null; 40 | }); 41 | afterAll(() => { 42 | document.body.removeChild(container); 43 | container = null; 44 | }); 45 | describe('calling update method : ', () => { 46 | test('update method should be called with two parameters when props.children is changed', () => { 47 | let _ctx, _ctx2; 48 | const getCtx = function (react, Babel, rerender) { 49 | _ctx = new Ctx(react, Babel, rerender); 50 | _ctx.getComponent = jest.fn(() => _ctx._com); 51 | _ctx.update = jest.fn((template, babelOptions) => {}); 52 | return _ctx; 53 | }, 54 | getCtx2 = function (react, Babel, rerender) { 55 | _ctx2 = new Ctx(react, Babel, rerender); 56 | _ctx2.getComponent = jest.fn(() => _ctx2._com); 57 | _ctx2.update = jest.fn((template, babelOptions) => {}); 58 | return _ctx2; 59 | }; 60 | const babelOp = {}; 61 | renderApp(str, babelOp, {getCtx, react, Babel}, true, str, babelOp); 62 | expect(_ctx.update.mock.calls.length).toBe(1); 63 | expect(_ctx.update.mock.calls[0][0]).toBe(str); 64 | expect(_ctx.update.mock.calls[0][1]).toEqual(babelOp); 65 | renderApp(str, babelOp, {getCtx: getCtx2, react, Babel}, true, str2); 66 | expect(_ctx2.update.mock.calls.length).toBe(2); 67 | expect(_ctx2.update.mock.calls[0][0]).toBe(str); 68 | expect(_ctx2.update.mock.calls[0][1]).toEqual(babelOp); 69 | expect(_ctx2.update.mock.calls[1][0]).toBe(str2); 70 | expect(_ctx2.update.mock.calls[1][1]).toEqual(babelOp); 71 | }); 72 | test('update method should be called with two parameters when babelOptions is changed', () => { 73 | let _ctx, _ctx2; 74 | const getCtx = function (react, Babel, rerender) { 75 | _ctx = new Ctx(react, Babel, rerender); 76 | _ctx.getComponent = jest.fn(() => _ctx._com); 77 | _ctx.update = jest.fn((template, babelOptions) => {}); 78 | return _ctx; 79 | }, 80 | getCtx2 = function (react, Babel, rerender) { 81 | _ctx2 = new Ctx(react, Babel, rerender); 82 | _ctx2.getComponent = jest.fn(() => _ctx2._com); 83 | _ctx2.update = jest.fn((template, babelOptions) => {}); 84 | return _ctx2; 85 | }; 86 | const babelOp = {}; 87 | const babelOp2 = {presets: ['react']}; 88 | renderApp(str, babelOp, {getCtx, react, Babel}, true, str, babelOp); 89 | expect(_ctx.update.mock.calls.length).toBe(1); 90 | expect(_ctx.update.mock.calls[0][0]).toBe(str); 91 | expect(_ctx.update.mock.calls[0][1]).toEqual(babelOp); 92 | renderApp(str, babelOp, {getCtx: getCtx2, react, Babel}, true, str, babelOp2); 93 | expect(_ctx2.update.mock.calls.length).toBe(2); 94 | expect(_ctx2.update.mock.calls[0][0]).toBe(str); 95 | expect(_ctx2.update.mock.calls[0][1]).toEqual(babelOp); 96 | expect(_ctx2.update.mock.calls[1][0]).toBe(str); 97 | expect(_ctx2.update.mock.calls[1][1]).toEqual(babelOp2); 98 | }); 99 | }); 100 | describe('calling getComponent method : ', () => { 101 | test('getComponent method should be called on every render', () => { 102 | let _ctx; 103 | const getCtx = function (react, Babel, rerender) { 104 | _ctx = new Ctx(react, Babel, rerender); 105 | _ctx.getComponent = jest.fn(() => _ctx._com); 106 | _ctx.update = jest.fn((template, babelOptions) => {}); 107 | return _ctx; 108 | }; 109 | const babelOp = {}; 110 | renderApp(str, babelOp, {getCtx, react, Babel}, true, str, babelOp); 111 | expect(_ctx.getComponent.mock.calls.length).toBe(2); 112 | }); 113 | }); 114 | describe('snapshots : ', () => { 115 | test('data props should be passed as props to generated component', () => { 116 | let _ctx; 117 | const getCtx = function (react, Babel, rerender) { 118 | _ctx = new Ctx(react, Babel, rerender); 119 | _ctx.getComponent = () => (props) =>
; 120 | _ctx.update = jest.fn((template, babelOptions) => {}); 121 | return _ctx; 122 | }; 123 | const StrintToReactCom = StrintToReact.bind(undefined, {getCtx, react, Babel}); 124 | const tree = renderer 125 | .create({`string code`}) 126 | .toJSON(); 127 | expect(tree).toMatchSnapshot(); 128 | }); 129 | test('default render structure', () => { 130 | let _ctx; 131 | const getCtx = function (react, Babel, rerender) { 132 | _ctx = new Ctx(react, Babel, rerender); 133 | _ctx.update = jest.fn((template, babelOptions) => {}); 134 | return _ctx; 135 | }; 136 | const StrintToReactCom = StrintToReact.bind(undefined, {getCtx, react, Babel}); 137 | const tree = renderer 138 | .create({`string code`}) 139 | .toJSON(); 140 | expect(tree).toMatchSnapshot(); 141 | }); 142 | }); 143 | -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | myPage 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /example/stories/data-prop/README.md: -------------------------------------------------------------------------------- 1 | ```jsx 2 | import {useState} from 'react'; 3 | import StringToReactComponent from 'string-to-react-component'; 4 | function App() { 5 | const [counter, setCounter] = useState(0); 6 | const increase = () => { 7 | setCounter(counter + 1); 8 | }; 9 | return ( 10 | 11 | {`(props)=>{ 12 | return (<> 13 | 14 | {'counter : '+ props.counter} 15 | ); 16 | }`} 17 | 18 | ); 19 | } 20 | ; 21 | ``` 22 | -------------------------------------------------------------------------------- /example/stories/env-preset/README.md: -------------------------------------------------------------------------------- 1 | ```jsx 2 | import StringToReactComponent from 'string-to-react-component'; 3 | function App() { 4 | return ( 5 | 6 | {`(props)=>{ 7 | const [counter,setCounter]=React.useState(0); 8 | const increase=()=>{ 9 | setCounter(counter+1); 10 | }; 11 | return (<> 12 | 13 | {'count : '+ counter} 14 | ); 15 | }`} 16 | 17 | ); 18 | } 19 | ; 20 | ``` 21 | -------------------------------------------------------------------------------- /example/stories/filename-option/README.md: -------------------------------------------------------------------------------- 1 | ```jsx 2 | import StringToReactComponent from 'string-to-react-component'; 3 | 4 | function App() { 5 | return ( 6 | 7 | {`(props)=>{ 8 | const [counter,setCounter]=React.useState(0); 9 | const increase=()=>{ 10 | setCounter(counter+1); 11 | }; 12 | return (<> 13 | 14 | {'count : '+ counter} 15 | ); 16 | }`} 17 | 18 | ); 19 | } 20 | ; 21 | ``` 22 | -------------------------------------------------------------------------------- /example/stories/styles.css: -------------------------------------------------------------------------------- 1 | pre > span.token.tag:nth-last-child(3), 2 | pre > span.token.punctuation:nth-last-child(2) { 3 | display: none; 4 | } 5 | main > footer { 6 | display: none !important; 7 | } 8 | -------------------------------------------------------------------------------- /example/stories/typescript/README.md: -------------------------------------------------------------------------------- 1 | ```jsx 2 | import StringToReactComponent from 'string-to-react-component'; 3 | 4 | function App() { 5 | return ( 6 | 8 | {`function (props:any):React.ReactElement{ 9 | const [counter,setCounter]=React.useState(0); 10 | const increase=()=>{ 11 | setCounter(counter+1); 12 | }; 13 | return (<> 14 | 15 | {'count : '+ counter} 16 | ); 17 | }`} 18 | 19 | ); 20 | } 21 | ; 22 | ``` 23 | -------------------------------------------------------------------------------- /example/stories/usage/README.md: -------------------------------------------------------------------------------- 1 | ```jsx 2 | import StringToReactComponent from 'string-to-react-component'; 3 | function App() { 4 | return ( 5 | 6 | {`(props)=>{ 7 | const [counter,setCounter]=React.useState(0);//by default your code has access to the React object 8 | const increase=()=>{ 9 | setCounter(counter+1); 10 | }; 11 | return (<> 12 | 13 | {'count : '+ counter} 14 | ); 15 | }`} 16 | 17 | ); 18 | } 19 | ; 20 | ``` 21 | -------------------------------------------------------------------------------- /example/stories/using-react-hooks/README.md: -------------------------------------------------------------------------------- 1 | The code inside the string has access to the `React` object and for using `useState`, `useEffect`, `useRef` and ... you should get them from `React` object or pass them as `data` prop to the component 2 | 3 | ```jsx 4 | import {useState} from 'react'; 5 | import StringToReactComponent from 'string-to-react-component'; 6 | 7 | function App() { 8 | return ( 9 | 10 | {`(props)=>{ 11 | return (<> 12 |

type of imported useState is {typeof useState}

13 |

type of React is {typeof React}

14 |

type of React.useState is {typeof React.useState}

15 |

type of props.useState is {typeof props.useState}

16 | ); 17 | }`} 18 |
19 | ); 20 | } 21 | ; 22 | ``` 23 | -------------------------------------------------------------------------------- /example/stories/using-unkown-elements/README.md: -------------------------------------------------------------------------------- 1 | ```jsx 2 | import StringToReactComponent from 'string-to-react-component'; 3 | 4 | function MyFirstComponent() { 5 | return

This is my first component.

; 6 | } 7 | function MySecondComponent() { 8 | return

This is my second component.

; 9 | } 10 | 11 | function App() { 12 | return ( 13 | 14 | {`(props)=>{ 15 | const {MyFirstComponent, MySecondComponent}=props; 16 | return (<> 17 | 18 | 19 | ); 20 | }`} 21 | 22 | ); 23 | } 24 | ; 25 | ``` 26 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import type { FC, PropsWithChildren } from 'react'; 2 | import type { StringToReactComponentProps } from './src/types.d'; 3 | export { StringToReactComponentProps } from './src/types.d'; 4 | declare const StringToReactComponent: FC>; 5 | export default StringToReactComponent; 6 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "string-to-react-component", 3 | "version": "4.0.1", 4 | "private": false, 5 | "description": "Dynamically create and render React components from strings at runtime, converting strings to React components for flexible UI generation.", 6 | "keywords": [ 7 | "react", 8 | "component", 9 | "string-to-react", 10 | "convert-string-to-react", 11 | "render-react-from-string", 12 | "react-parser", 13 | "string", 14 | "element", 15 | "jsx", 16 | "string-to-jsx", 17 | "converter", 18 | "parser", 19 | "dynamic-components", 20 | "runtime-components" 21 | ], 22 | "author": { 23 | "name": "dev-javascript", 24 | "email": "javascript.code.dev@gmail.com" 25 | }, 26 | "types": "./index.d.ts", 27 | "main": "lib/cjs/index.js", 28 | "module": "lib/esm/index.js", 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/dev-javascript/string-to-react-component.git" 32 | }, 33 | "homepage": "https://github.com/dev-javascript/string-to-react-component/", 34 | "bugs": { 35 | "url": "https://github.com/dev-javascript/string-to-react-component/issues" 36 | }, 37 | "scripts": { 38 | "watch": "set NODE_OPTIONS=--openssl-legacy-provider & cross-env BABEL_OUTPUT=umd INCLUDE_POLYFILLS=true webpack --config webpack.config.js --env=development --watch", 39 | "build": "set NODE_OPTIONS=--openssl-legacy-provider & npm-run-all clean:* --parallel build:lib:* & npm run build:dist", 40 | "build:dist": "rollup -c", 41 | "build:lib:cjs": "cross-env BABEL_OUTPUT=cjs babel src/ --out-dir lib/cjs/ --extensions .ts,.tsx --ignore **/__tests__,**/__mocks__,**/*.test.js,**/*.js.snap,**/*.d.ts", 42 | "build:lib:esm": "babel src/ --out-dir lib/esm/ --extensions .ts,.tsx --ignore **/__tests__,**/__mocks__,**/*.test.js,**/*.js.snap,**/*.d.ts", 43 | "build:lib:esm-pf": "cross-env INCLUDE_POLYFILLS=true babel src/ --out-dir lib/esm-including-polyfills/ --extensions .ts,.tsx --ignore **/__tests__,**/__mocks__,**/*.test.js,**/*.js.snap,**/*.d.ts", 44 | "clean:lib": "rimraf lib", 45 | "clean:dist": "rimraf dist", 46 | "prepublishOnly": "npm run build", 47 | "test": "jest", 48 | "lint": "eslint src", 49 | "deploy": "gh-pages -d demo", 50 | "styleguide": "styleguidist server", 51 | "styleguide:build": "styleguidist build" 52 | }, 53 | "peerDependencies": { 54 | "@babel/standalone": ">=7.15.8", 55 | "react": ">=16.8.0", 56 | "react-dom": ">=16.8.0" 57 | }, 58 | "devDependencies": { 59 | "@babel/cli": "^7.24.8", 60 | "@babel/core": "^7.24.9", 61 | "@babel/eslint-parser": "^7.25.0", 62 | "@babel/plugin-transform-react-jsx": "^7.24.7", 63 | "@babel/plugin-transform-react-jsx-self": "^7.24.7", 64 | "@babel/plugin-transform-runtime": "^7.24.7", 65 | "@babel/preset-env": "^7.25.0", 66 | "@babel/preset-react": "^7.24.7", 67 | "@babel/preset-typescript": "^7.24.7", 68 | "@babel/runtime-corejs3": "^7.25.0", 69 | "@babel/standalone": "7.15.8", 70 | "@rollup/plugin-commonjs": "^26.0.1", 71 | "@rollup/plugin-node-resolve": "^15.2.3", 72 | "@rollup/plugin-terser": "^0.4.4", 73 | "@types/babel__standalone": "^7.1.7", 74 | "@types/react": "^18.3.3", 75 | "@types/react-dom": "^18.3.0", 76 | "babel-loader": "^9.1.3", 77 | "cross-env": "^7.0.3", 78 | "css-loader": "^7.1.2", 79 | "eslint": "^9.8.0", 80 | "eslint-config-prettier": "^9.1.0", 81 | "eslint-plugin-prettier": "^5.2.1", 82 | "eslint-plugin-react": "^7.35.0", 83 | "gh-pages": "^6.1.1", 84 | "jest": "^29.7.0", 85 | "jest-environment-jsdom": "^29.7.0", 86 | "jest-extended": "^4.0.2", 87 | "npm-run-all": "^4.1.5", 88 | "prettier": "3.3.3", 89 | "react": "16.9.0", 90 | "react-dom": "16.9.0", 91 | "react-styleguidist": "^12.0.1", 92 | "react-test-renderer": "16.9.0", 93 | "rollup": "^4.19.1", 94 | "style-loader": "^4.0.0", 95 | "webpack": "^5.93.0", 96 | "webpack-cli": "^5.1.4" 97 | }, 98 | "license": "MIT", 99 | "directories": { 100 | "lib": "lib" 101 | }, 102 | "jest": { 103 | "testEnvironment": "jsdom", 104 | "setupFilesAfterEnv": [ 105 | "jest-extended" 106 | ], 107 | "collectCoverage": true 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | const pkg = require('./package.json'); 2 | const terser = require('@rollup/plugin-terser'); 3 | const commonjs = require('@rollup/plugin-commonjs'); 4 | const nodeResolve = require('@rollup/plugin-node-resolve'); 5 | const name = pkg.name 6 | .split('-') 7 | .map((s) => s.charAt(0).toUpperCase() + s.slice(1)) 8 | .join(''); 9 | const Config = ({en, inputPath = '', outputFile = 'stringToReactComponent', pf = false}) => { 10 | var pfName = pf ? '.including-polyfills' : ''; 11 | return { 12 | input: `lib/${pf ? 'esm-including-polyfills' : 'esm'}/${inputPath}index.js`, 13 | output: { 14 | file: `dist/${outputFile}${pfName}.umd${en === 'dev' ? '' : '.min'}.js`, 15 | format: 'umd', 16 | name, 17 | banner: 18 | '' + 19 | `/** 20 | * ${pkg.name} - ${pkg.description} 21 | * 22 | * @version v${pkg.version} 23 | * @homepage ${pkg.homepage} 24 | * @author ${pkg.author.name} ${pkg.author.email} 25 | * @license ${pkg.license} 26 | */`, 27 | globals: { 28 | 'react-dom': 'ReactDOM', 29 | react: 'React', 30 | '@babel/standalone': 'Babel', 31 | }, 32 | sourcemap: true, 33 | }, 34 | plugins: (function () { 35 | const _plugins = [nodeResolve({preferBuiltins: false}), commonjs()]; 36 | if (en === 'prod') { 37 | _plugins.push(terser()); 38 | } 39 | return _plugins; 40 | })(), 41 | external: function (id) { 42 | return /prop-types$|react$|\@babel\/standalone$|react-dom$|.test.js$|.js.snap$|.css$/g.test(id); 43 | }, 44 | }; 45 | }, 46 | ConfigFactory = (op) => [ 47 | Config({en: 'dev', ...op}), 48 | Config({en: 'prod', ...op}), 49 | Config({en: 'dev', pf: true, ...op}), 50 | Config({en: 'prod', pf: true, ...op}), 51 | ]; 52 | module.exports = ConfigFactory(); 53 | -------------------------------------------------------------------------------- /src/ctx.tsx: -------------------------------------------------------------------------------- 1 | import type {TransformOptions} from '@babel/core'; 2 | import type {TBabel, TReact, IStringToReactApi} from './types.d'; 3 | import {FC} from 'react'; 4 | class Ctx implements IStringToReactApi { 5 | _temp: string = ''; 6 | _blob: Blob | undefined = undefined; 7 | _rerender: (state: {}) => void = () => {}; 8 | _com: FC = function () { 9 | return null; 10 | }; 11 | _getBabel: () => TBabel; 12 | _getReact: () => TReact; 13 | constructor(React: TReact, Babel: TBabel, rerender: (state: {}) => void) { 14 | this._rerender = rerender; 15 | this._getReact = () => React; 16 | if (!Babel) { 17 | throw new Error( 18 | `Package "string-to-react-component" has a missing peer dependency of "@babel/standalone" ( requires ">=7.15.8" )`, 19 | ); 20 | } 21 | this._getBabel = () => Babel; 22 | } 23 | _checkBabelOptions(babelOptions: TransformOptions) { 24 | if (Object.prototype.toString.call(babelOptions) !== '[object Object]') { 25 | throw new Error(`babelOptions prop of string-to-react-component element should be an object.`); 26 | } 27 | if (Object.prototype.hasOwnProperty.call(babelOptions, 'sourceMaps') === false) { 28 | babelOptions.sourceMaps = 'inline'; 29 | } 30 | if (Object.prototype.hasOwnProperty.call(babelOptions, 'presets') === false) { 31 | babelOptions.presets = ['react']; 32 | } else { 33 | //check if babelOptions.presets is not type of Array 34 | if (!(typeof babelOptions.presets === 'object' && babelOptions.presets?.constructor == Array)) { 35 | throw new Error(`string-to-react-component Error : presets property of babelOptions prop should be an array`); 36 | } 37 | if (babelOptions.presets.indexOf('react') === -1) { 38 | babelOptions.presets.push('react'); 39 | } 40 | } 41 | } 42 | _prependCode(template: string): IStringToReactApi { 43 | this._temp = `import React from "react";\nexport default ${template}`; 44 | return this; 45 | } 46 | _postpendCode(): string { 47 | return this._temp 48 | .replace('export default', 'export default (React)=>') 49 | .replace('import React from "react";', '//import React from "react";'); 50 | } 51 | _getBlob(temp: string): Blob { 52 | return new Blob([temp], {type: 'application/javascript'}); 53 | } 54 | _import(url: string): Promise { 55 | return import(/* webpackIgnore: true */ url); 56 | } 57 | _getModule(blob: Blob): Promise { 58 | const moduleUrl = URL.createObjectURL(blob); 59 | return this._import(moduleUrl) 60 | .then((module) => { 61 | URL.revokeObjectURL(moduleUrl); 62 | return Promise.resolve((module?.default || module)(this._getReact())); 63 | }) 64 | .catch((error) => { 65 | URL.revokeObjectURL(moduleUrl); 66 | const errorTitle: string = 'string-to-react-component loading module is failed:'; 67 | console.error(errorTitle, error); 68 | throw new Error(errorTitle); 69 | }); 70 | } 71 | _transpile(babelOptions: TransformOptions): IStringToReactApi { 72 | this._checkBabelOptions(babelOptions); 73 | const resultObj = this._getBabel().transform(this._temp, babelOptions); 74 | let code = resultObj.code; 75 | // if (babelOptions.filename) { 76 | // code = resultObj.code + `\n//# sourceURL=${babelOptions.filename}`; 77 | // } 78 | this._temp = code || 'null'; 79 | return this; 80 | } 81 | _validateCodeInsideTheTemp(com: any): void { 82 | if (typeof com !== 'function') { 83 | throw new Error(`code inside the passed string into string-to-react-component, should be a function`); 84 | } 85 | } 86 | _validateTemplate(temp: any) { 87 | if (typeof temp !== 'string') { 88 | throw new Error(`passed child into string-to-react-component element should b a string`); 89 | } 90 | if (temp === '') { 91 | throw new Error(`passed string into string-to-react-component element can not be empty`); 92 | } 93 | } 94 | /** update transpiled code */ 95 | _updateTemplate(template: string, babelOptions: TransformOptions): string { 96 | this._validateTemplate(template); 97 | return this._prependCode(template)._transpile(babelOptions)._postpendCode(); 98 | } 99 | update(template: string, babelOptions: TransformOptions): void { 100 | this._update(template, babelOptions); 101 | } 102 | _update(template: string, babelOptions: TransformOptions): void { 103 | this._updateComponent(this._updateTemplate(template, babelOptions)); 104 | } 105 | _onChangeComponent(): void { 106 | this._rerender({}); 107 | } 108 | _updateComponent(template: string): void { 109 | this._getModule(this._getBlob(template)).then((com: FC) => { 110 | this._validateCodeInsideTheTemp(com); 111 | this._com = com; 112 | this._onChangeComponent(); 113 | }); 114 | } 115 | getComponent(): FC { 116 | return this._com; 117 | } 118 | } 119 | export default Ctx; 120 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Ctx from './ctx'; 3 | import * as Babel from '@babel/standalone'; 4 | import type { TBabel, TReact, IStringToReactApi } from './types.d'; 5 | import StringToReact from './strintToReact'; 6 | const getCtx: (React: TReact, Babel: TBabel, rerender: (state: {}) => void) => IStringToReactApi = (React: TReact, Babel: TBabel, rerender: (state: {}) => void) => new Ctx(React, Babel, rerender); 7 | export default StringToReact.bind(null, { getCtx: getCtx, Babel: Babel, react: React }); -------------------------------------------------------------------------------- /src/strintToReact.tsx: -------------------------------------------------------------------------------- 1 | import React, {useRef, useState, useEffect, type FC} from 'react'; 2 | import type {StringToReactComponentProps, IStringToReactApi, TBabel, TReact} from './types.d'; 3 | function StringToReactComponent( 4 | deps: { 5 | getCtx: (react: TReact, Babel: TBabel, rerender: (state: {}) => void) => IStringToReactApi; 6 | react: TReact; 7 | Babel: TBabel; 8 | }, 9 | props: StringToReactComponentProps, 10 | ) { 11 | const {getCtx, Babel, react} = deps; 12 | const [, rerender] = useState({}); 13 | let ref = useRef(null); 14 | ref.current = ref.current || getCtx(react, Babel, rerender); 15 | const api = ref.current as IStringToReactApi; 16 | const data = props.data || {}; 17 | useEffect(() => { 18 | api.update(props.children || '()=>null', props.babelOptions || {}); 19 | }, [props.children, props.babelOptions]); 20 | const Com: FC = api.getComponent(); 21 | return ; 22 | } 23 | 24 | export default StringToReactComponent; 25 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | import * as Babel from '@types/babel__standalone'; 2 | import * as React from '@types/react'; 3 | import { TransformOptions } from "@babel/core"; 4 | import React, { FunctionComponent, FC, PropsWithChildren } from 'react'; 5 | import { TransformOptions } from "@babel/core"; 6 | export interface IStringToReactApi { 7 | update: (template: string, babelOptions: TransformOptions) => void; 8 | getComponent: () => FC>; 9 | [x: string]: any; 10 | } 11 | export type TReact = typeof React; 12 | export type TBabel = typeof Babel; 13 | export interface StringToReactComponentProps { 14 | data?: object, 15 | babelOptions?: TransformOptions, 16 | children?: string, 17 | } 18 | -------------------------------------------------------------------------------- /styleguide.config.js: -------------------------------------------------------------------------------- 1 | const webpack = require('webpack'); 2 | const {version, name} = require('./package'); 3 | const path = require('path'); 4 | 5 | module.exports = { 6 | title: name, 7 | template: { 8 | head: { 9 | meta: [ 10 | { 11 | name: 'description', 12 | content: 13 | 'Convert string to react component dynamically. Rendering React Components from String. Create Dynamic React Components with String', 14 | }, 15 | ], 16 | }, 17 | }, 18 | getComponentPathLine(componentPath) { 19 | return ``; 20 | }, 21 | components: 'example/stories/**/*.{jsx,js,tsx}', 22 | moduleAliases: { 23 | 'string-to-react-component': path.resolve(__dirname, './'), 24 | }, 25 | ribbon: { 26 | // Link to open on the ribbon click (required) 27 | url: 'https://github.com/dev-javascript/string-to-react-component', 28 | // Text to show on the ribbon (optional) 29 | text: 'Fork me on GitHub', 30 | }, 31 | styleguideDir: 'demo', 32 | require: [path.join(__dirname, './example/stories/styles.css')], 33 | // assetsDir: "example/stories/assets", 34 | sections: [ 35 | {name: 'Minimal Usage', content: 'example/stories/usage/README.md', sectionDepth: 1}, 36 | {name: 'Using Unknown Elements', content: 'example/stories/using-unkown-elements/README.md', sectionDepth: 1}, 37 | {name: 'data prop', content: 'example/stories/data-prop/README.md', sectionDepth: 1}, 38 | {name: 'Using React Hooks', content: 'example/stories/using-react-hooks/README.md'}, 39 | {name: 'filename option', content: 'example/stories/filename-option/README.md', sectionDepth: 1}, 40 | {name: 'Using Typescript', content: 'example/stories/typescript/README.md', sectionDepth: 1}, 41 | {name: 'Using env preset', content: 'example/stories/env-preset/README.md', sectionDepth: 1}, 42 | ], 43 | styleguideComponents: {}, 44 | pagePerSection: true, 45 | defaultExample: true, 46 | usageMode: 'expand', 47 | version, 48 | webpackConfig: { 49 | plugins: [ 50 | new webpack.DefinePlugin({ 51 | process: { 52 | env: JSON.stringify({ 53 | ...process.env, 54 | }), 55 | }, 56 | }), 57 | ], 58 | module: { 59 | rules: [ 60 | { 61 | test: /\.(js|ts)x?$/, 62 | exclude: /node_modules/, 63 | loader: 'babel-loader', 64 | }, 65 | { 66 | test: /\.css$/, 67 | use: ['style-loader', 'css-loader'], 68 | }, 69 | ], 70 | noParse: /\.(scss)/, 71 | }, 72 | resolve: { 73 | extensions: ['.ts', '.tsx', '.json'], 74 | }, 75 | }, 76 | }; 77 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "allowJs": true, 8 | "skipLibCheck": true, 9 | 10 | "allowSyntheticDefaultImports": true, 11 | // Ensure that .d.ts files are created by tsc, but not .js files 12 | "declaration": true, 13 | "emitDeclarationOnly": true, 14 | 15 | /* Bundler mode */ 16 | "moduleResolution": "node", 17 | "allowImportingTsExtensions": true, 18 | /* Advanced Options */ 19 | "resolveJsonModule": true /* Include modules imported with '.json' extension */, 20 | // Ensure that Babel can safely transpile files in the TypeScript project 21 | "isolatedModules": true, 22 | //"noEmit": true, 23 | "jsx": "react-jsx", 24 | 25 | /* Linting */ 26 | "strict": true, 27 | "noUnusedLocals": true, 28 | "noUnusedParameters": true, 29 | "noFallthroughCasesInSwitch": true 30 | }, 31 | "include": ["src", "__test__/ctx.test.js", "__test__/stringToReact.test.js", "__test__/mock-module.js"], 32 | "exclude": ["node_modules"] 33 | } 34 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const pkg = require('./package.json'); 3 | const library = pkg.name 4 | .split('-') 5 | .map((s) => s.charAt(0).toUpperCase() + s.slice(1)) 6 | .join(''); 7 | module.exports = (env) => { 8 | const isProduction = env === 'production'; 9 | return { 10 | entry: './src/index.ts', 11 | output: { 12 | filename: isProduction ? 'stringToReactComponent.umd.min.js' : 'stringToReactComponent.umd.js', 13 | path: path.resolve(__dirname, 'build'), 14 | library, 15 | libraryTarget: 'umd', 16 | publicPath: '/build/', 17 | umdNamedDefine: true, 18 | }, 19 | devtool: isProduction ? 'source-map' : 'inline-source-map', 20 | mode: 'development', 21 | module: { 22 | rules: [ 23 | { 24 | test: /\.(js|jsx|tsx|ts)$/, 25 | exclude: /(node_modules|bower_components)/, 26 | use: { 27 | loader: 'babel-loader', 28 | }, 29 | }, 30 | { 31 | test: /\.css$/i, 32 | use: ['style-loader', 'css-loader'], 33 | }, 34 | ], 35 | }, 36 | resolve: { 37 | extensions: ['*', '.js', '.jsx', '.tsx', '.ts'], 38 | alias: { 39 | assets: path.resolve(__dirname, 'assets'), 40 | }, 41 | }, 42 | externals: { 43 | react: { 44 | commonjs: 'react', 45 | commonjs2: 'react', 46 | amd: 'React', 47 | root: 'React', 48 | }, 49 | 'react-dom': { 50 | commonjs: 'react-dom', 51 | commonjs2: 'react-dom', 52 | amd: 'ReactDOM', 53 | root: 'ReactDOM', 54 | }, 55 | '@babel/standalone': 'Babel', 56 | }, 57 | }; 58 | }; 59 | --------------------------------------------------------------------------------