├── .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 | [](https://codecov.io/gh/dev-javascript/string-to-react-component) [](http://npmjs.org/package/string-to-react-component) [](http://nodejs.org/download/) [](https://react.dev/) [](LICENSE) [](https://npmjs.org/package/string-to-react-component) [](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