├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ ├── build.yml │ ├── latest.yml │ ├── lint.yml │ ├── nightly.yml │ └── test.yml ├── .gitignore ├── .node-version ├── .npmignore ├── .parcelrc ├── LICENSE ├── README.md ├── doc ├── document.tsx ├── header.tsx ├── index.html ├── index.scss ├── index.tsx ├── jsxEditor.tsx ├── liveDemo.tsx ├── objectEditor.tsx └── readme.tsx ├── jest.config.js ├── package.json ├── prettier.config.js ├── script ├── build.mjs ├── jest │ └── jsdom-polyfill.js ├── nightly.mjs ├── package.json.cjs └── react-version.mjs ├── src ├── evaluate │ ├── bind.ts │ ├── class.ts │ ├── context.ts │ ├── definition.ts │ ├── error.ts │ ├── evaluate.test.tsx │ ├── evaluate.ts │ ├── expression.test.ts │ ├── expression.ts │ ├── function.ts │ ├── index.ts │ ├── options.ts │ ├── program.ts │ ├── statement.test.ts │ └── statement.ts ├── index.ts ├── renderer │ ├── __snapshots__ │ │ └── renderer.test.tsx.snap │ ├── filter.ts │ ├── index.ts │ ├── isUnknownElementTagName.tsx │ ├── options.ts │ ├── render.ts │ ├── renderer.test.tsx │ └── renderer.tsx └── types │ ├── binding.ts │ ├── index.ts │ ├── node.ts │ └── options.ts ├── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: ['@typescript-eslint', 'jest', 'prettier'], 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:import/errors', 7 | 'plugin:import/warnings', 8 | 'plugin:import/typescript', 9 | 'plugin:prettier/recommended', 10 | 'prettier', 11 | ], 12 | parser: require.resolve('@typescript-eslint/parser'), 13 | parserOptions: { 14 | sourceType: 'module', 15 | ecmaFeatures: { 16 | jsx: true, 17 | }, 18 | }, 19 | env: { browser: true, node: true, es6: true, 'jest/globals': true }, 20 | rules: { 21 | 'prettier/prettier': 'error', 22 | 'import/no-unresolved': 'off', 23 | '@typescript-eslint/no-empty-interface': 'off', 24 | '@typescript-eslint/no-empty-function': 'off', 25 | '@typescript-eslint/no-explicit-any': 'off', 26 | '@typescript-eslint/explicit-module-boundary-types': 'off', 27 | '@typescript-eslint/no-unused-vars': [ 28 | 2, 29 | { 30 | argsIgnorePattern: '^_', 31 | varsIgnorePattern: '^_', 32 | }, 33 | ], 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: 7 | - main 8 | paths: 9 | - package.json 10 | - yarn.lock 11 | - doc/**/* 12 | - src/**/* 13 | 14 | jobs: 15 | doc: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v2 20 | - name: Detect node version 21 | id: node-version 22 | run: echo "::set-output name=NODE_VERSION::$(cat .node-version)" 23 | - name: Setup node.js 24 | uses: actions/setup-node@v2 25 | with: 26 | node-version: "${{ steps.node-version.outputs.NODE_VERSION }}" 27 | - name: Cache dependencies 28 | uses: actions/cache@v2 29 | with: 30 | path: | 31 | ./node_modules 32 | key: build-yarn-${{ runner.os }}-${{ steps.node-version.outputs.NODE_VERSION }}-${{ hashFiles('yarn.lock') }} 33 | restore-keys: | 34 | build-yarn-${{ runner.os }}-${{ steps.node-version.outputs.NODE_VERSION }}-${{ hashFiles('yarn.lock') }} 35 | build-yarn-${{ runner.os }}-${{ steps.node-version.outputs.NODE_VERSION }}- 36 | build-yarn-${{ runner.os }}- 37 | - name: Install dependencies 38 | run: yarn install --frozen-lockfile 39 | - name: Build 40 | run: yarn doc:build --public-url=/react-jsx-renderer/ 41 | env: 42 | NODE_ENV: production 43 | - name: Deploy 44 | uses: peaceiris/actions-gh-pages@v3 45 | if: github.ref == 'refs/heads/main' 46 | with: 47 | github_token: ${{ secrets.GITHUB_TOKEN }} 48 | publish_dir: ./dist 49 | -------------------------------------------------------------------------------- /.github/workflows/latest.yml: -------------------------------------------------------------------------------- 1 | name: latest 2 | 3 | on: 4 | release: 5 | types: 6 | - created 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Detect node version 15 | id: node-version 16 | run: echo "::set-output name=NODE_VERSION::$(cat .node-version)" 17 | - name: Setup node.js 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: "${{ steps.node-version.outputs.NODE_VERSION }}" 21 | registry-url: 'https://registry.npmjs.org' 22 | - name: Cache dependencies 23 | uses: actions/cache@v2 24 | with: 25 | path: | 26 | ./node_modules 27 | key: release-yarn-${{ runner.os }}-${{ steps.node-version.outputs.NODE_VERSION }}-${{ hashFiles('yarn.lock') }} 28 | restore-keys: | 29 | release-yarn-${{ runner.os }}-${{ steps.node-version.outputs.NODE_VERSION }}-${{ hashFiles('yarn.lock') }} 30 | release-yarn-${{ runner.os }}-${{ steps.node-version.outputs.NODE_VERSION }}- 31 | release-yarn-${{ runner.os }}- 32 | - name: Install dependencies 33 | run: yarn install --frozen-lockfile 34 | - name: Publish package 35 | run: npm publish --access public --tag latest 36 | env: 37 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 38 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: lint 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - '**/*.js' 7 | - '**/*.jsx' 8 | - '**/*.ts' 9 | - '**/*.tsx' 10 | - '**/*.mjs' 11 | push: 12 | branches: 13 | - main 14 | paths: 15 | - '**/*.js' 16 | - '**/*.jsx' 17 | - '**/*.ts' 18 | - '**/*.tsx' 19 | - '**/*.mjs' 20 | 21 | jobs: 22 | lint: 23 | runs-on: ubuntu-latest 24 | steps: 25 | - name: Checkout 26 | uses: actions/checkout@v2 27 | - name: Detect node version 28 | id: node-version 29 | run: echo "::set-output name=NODE_VERSION::$(cat .node-version)" 30 | - name: Setup node.js 31 | uses: actions/setup-node@v2 32 | with: 33 | node-version: "${{ steps.node-version.outputs.NODE_VERSION }}" 34 | - name: Cache dependencies 35 | uses: actions/cache@v2 36 | with: 37 | path: | 38 | ./node_modules 39 | key: lint-yarn-${{ runner.os }}-${{ steps.node-version.outputs.NODE_VERSION }}-${{ hashFiles('yarn.lock') }} 40 | restore-keys: | 41 | lint-yarn-${{ runner.os }}-${{ steps.node-version.outputs.NODE_VERSION }}-${{ hashFiles('yarn.lock') }} 42 | lint-yarn-${{ runner.os }}-${{ steps.node-version.outputs.NODE_VERSION }}- 43 | lint-yarn-${{ runner.os }}- 44 | - name: Install dependencies 45 | run: yarn install 46 | - name: Run lint 47 | run: yarn lint 48 | -------------------------------------------------------------------------------- /.github/workflows/nightly.yml: -------------------------------------------------------------------------------- 1 | name: nightly 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | - name: Detect node version 15 | id: node-version 16 | run: echo "::set-output name=NODE_VERSION::$(cat .node-version)" 17 | - name: Setup node.js 18 | uses: actions/setup-node@v2 19 | with: 20 | node-version: "${{ steps.node-version.outputs.NODE_VERSION }}" 21 | registry-url: 'https://registry.npmjs.org' 22 | - name: Cache dependencies 23 | uses: actions/cache@v2 24 | with: 25 | path: | 26 | ./node_modules 27 | key: release-yarn-${{ runner.os }}-${{ steps.node-version.outputs.NODE_VERSION }}-${{ hashFiles('yarn.lock') }} 28 | restore-keys: | 29 | release-yarn-${{ runner.os }}-${{ steps.node-version.outputs.NODE_VERSION }}-${{ hashFiles('yarn.lock') }} 30 | release-yarn-${{ runner.os }}-${{ steps.node-version.outputs.NODE_VERSION }}- 31 | release-yarn-${{ runner.os }}- 32 | - name: Install dependencies 33 | run: yarn install --frozen-lockfile 34 | - name: Set nightly version 35 | run: node script/nightly.mjs 36 | env: 37 | TZ: 'Asia/Tokyo' 38 | - name: Publish package 39 | run: npm publish --access public --tag nightly 40 | env: 41 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 42 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | pull_request: {} 5 | push: 6 | branches: 7 | - main 8 | paths: 9 | - '**/*.js' 10 | - '**/*.jsx' 11 | - '**/*.ts' 12 | - '**/*.tsx' 13 | - '**/*.mjs' 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | env: ['jsdom', 'node'] 21 | react-version: ['16.0.0', '17.0.0'] 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v2 25 | - name: Detect node version 26 | id: node-version 27 | run: echo "::set-output name=NODE_VERSION::$(cat .node-version)" 28 | - name: Setup node.js 29 | uses: actions/setup-node@v2 30 | with: 31 | node-version: "${{ steps.node-version.outputs.NODE_VERSION }}" 32 | - name: Cache dependencies 33 | uses: actions/cache@v2 34 | with: 35 | path: | 36 | ./node_modules 37 | key: yarn-${{ runner.os }}-${{ steps.node-version.outputs.NODE_VERSION }}-${{ matrix.react-version }}-${{ hashFiles('yarn.lock') }} 38 | restore-keys: | 39 | yarn-${{ runner.os }}-${{ steps.node-version.outputs.NODE_VERSION }}-${{ matrix.react-version }}-${{ hashFiles('yarn.lock') }} 40 | yarn-${{ runner.os }}-${{ steps.node-version.outputs.NODE_VERSION }}-${{ matrix.react-version }}- 41 | yarn-${{ runner.os }}- 42 | - name: Install dependencies - React ${{ matrix.react-version }} 43 | run: | 44 | node script/react-version.mjs ^${{ matrix.react-version }} 45 | yarn install 46 | - name: Run test with React@${{ matrix.react-version }} on ${{ matrix.env }} 47 | run: yarn test --env ${{ matrix.env }} 48 | - name: Send coverage 49 | uses: codecov/codecov-action@v1 50 | with: 51 | files: ./coverage/lcov.info 52 | flags: react-${{ matrix.react-version }},env-${{ matrix.env }} 53 | fail_ci_if_error: true 54 | verbose: true 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | .parcel-cache 4 | .vscode 5 | coverage 6 | dist 7 | node_modules 8 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 16.2.0 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | **/__snapshots__ 2 | *.log 3 | .vscode 4 | /.git 5 | /.github 6 | /.parcel-cache 7 | /coverage 8 | /doc 9 | /example 10 | /jest.config.js 11 | /node_modules 12 | /script 13 | /src/**/*.test.ts 14 | /src/**/*.test.tsx 15 | /tsconfig.json 16 | /webpack.config.mjs 17 | /yarn.lock 18 | -------------------------------------------------------------------------------- /.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@parcel/config-default" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Sho Kusano 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React JSX Renderer 2 | 3 | [![npm (latest)](https://img.shields.io/npm/v/react-jsx-renderer/latest)](https://www.npmjs.org/package/react-jsx-renderer) 4 | [![npm (nightlyt)](https://img.shields.io/npm/v/react-jsx-renderer/nightly)](https://www.npmjs.com/package/react-jsx-renderer/v/nightly) 5 | 6 | [![demo](https://img.shields.io/badge/LIVE%20DEMO-available-success)](https://aduca.org/react-jsx-renderer/) 7 | [![Coverage Status](https://codecov.io/gh/rosylilly/react-jsx-renderer/branch/main/graph/badge.svg?token=notleiwHZC)](https://codecov.io/gh/rosylilly/react-jsx-renderer) 8 | [![Dependencies Status](https://status.david-dm.org/gh/rosylilly/react-jsx-renderer.svg)](https://david-dm.org/rosylilly/react-jsx-renderer) 9 | [![Install size](https://packagephobia.com/badge?p=react-jsx-renderer)](https://packagephobia.com/result?p=react-jsx-renderer) 10 | [![License: MIT](https://img.shields.io/npm/l/react-jsx-renderer)](https://opensource.org/licenses/MIT) 11 | 12 | [![latest](https://github.com/rosylilly/react-jsx-renderer/actions/workflows/latest.yml/badge.svg)](https://github.com/rosylilly/react-jsx-renderer/actions/workflows/latest.yml) 13 | [![nightly](https://github.com/rosylilly/react-jsx-renderer/actions/workflows/nightly.yml/badge.svg)](https://github.com/rosylilly/react-jsx-renderer/actions/workflows/nightly.yml) 14 | [![build](https://github.com/rosylilly/react-jsx-renderer/actions/workflows/build.yml/badge.svg)](https://github.com/rosylilly/react-jsx-renderer/actions/workflows/build.yml) 15 | [![test](https://github.com/rosylilly/react-jsx-renderer/actions/workflows/test.yml/badge.svg)](https://github.com/rosylilly/react-jsx-renderer/actions/workflows/test.yml) 16 | [![lint](https://github.com/rosylilly/react-jsx-renderer/actions/workflows/lint.yml/badge.svg)](https://github.com/rosylilly/react-jsx-renderer/actions/workflows/lint.yml) 17 | 18 | A React Component for Rendering JSX. 19 | 20 | ## Description 21 | 22 | React JSX Renderer is a React Component for rendering JSX to React nodes. 23 | 24 | It has a JavaScript Runtime inside, and can execute the user's JSX with controlled behavior. 25 | 26 | [Launch Demo](https://aduca.org/react-jsx-renderer/) 27 | 28 | ## Features 29 | 30 | - [x] Rendering JSX as React nodes 31 | - [x] TypeScritpt ready 32 | - [x] Provides CommonJS and ES Modules 33 | - [x] JavaScript syntax and featues 34 | - without async, await and generator 35 | - [x] Injectable custom React components 36 | - [x] Pass binding variables 37 | - [x] Applicable filters to parsed nodes 38 | - You can create allowlist / denylist filters to tagName, attributes or properties 39 | - [x] Avoid user's call expressions 40 | - [x] Avoid user's new expressions 41 | - [x] Parse with [meriyah](https://github.com/meriyah/meriyah) 42 | 43 | ## Installation 44 | 45 | 1. `npm install -s react-jsx-renderer` (or `yarn add react-jsx-renderer`) 46 | 2. Add `import { JSXRenderer } from 'react-jsx-renderer';` 47 | 3. `` to render `Hello, World` 48 | 49 | ## Requirements 50 | 51 | - **React**: >= 16.0.0 52 | 53 | ## Options 54 | 55 | ```typescript 56 | interface ParseOptions { 57 | /** 58 | * Options of parser 59 | */ 60 | meriyah?: meriyah.Options; 61 | 62 | /** 63 | * When this option is enabled, always parse as an expression. 64 | */ 65 | forceExpression?: boolean; 66 | } 67 | 68 | interface EvaluateOptions { 69 | /** 70 | * binding 71 | */ 72 | binding?: Binding; 73 | 74 | /** 75 | * components 76 | */ 77 | components?: ComponentsBinding; 78 | 79 | /** 80 | * Prefix of generated keys. 81 | */ 82 | keyPrefix?: string; 83 | 84 | /** 85 | * When this option is enabled, no key will be generated 86 | */ 87 | disableKeyGeneration?: boolean; 88 | 89 | /** 90 | * When this option is enabled, bindings will be excluded from the component search. 91 | */ 92 | disableSearchCompontsByBinding?: boolean; 93 | 94 | /** 95 | * When this option is enabled, Call Expression and New Expression will always return undefined. 96 | */ 97 | disableCall?: boolean; 98 | 99 | /** 100 | * When this option is enabled, New Expression will always return undefined. 101 | */ 102 | disableNew?: boolean; 103 | 104 | /** 105 | * When this option is enabled, access to undefined variables will raise an exception. 106 | */ 107 | raiseReferenceError?: boolean; 108 | 109 | /** 110 | * List of functions allowed to be executed. 111 | * 112 | * If empty, all functions will be allowed to execute. 113 | */ 114 | allowedFunctions?: AnyFunction[]; 115 | 116 | /** 117 | * Add user-defined functions to the allowed list. 118 | */ 119 | allowUserDefinedFunction?: boolean; 120 | 121 | /** 122 | * List of functions denied to be executed. 123 | * 124 | * If empty, all functions will be allowed to execute. 125 | */ 126 | deniedFunctions?: AnyFunction[]; 127 | } 128 | 129 | interface RenderingOptions { 130 | /** 131 | * List of filters to be applied to elements. 132 | */ 133 | elementFilters?: JSXElementFilter[]; 134 | 135 | /** 136 | * List of filters to be applied to fragments. 137 | */ 138 | fragmentFilters?: JSXFragmentFilter[]; 139 | 140 | /** 141 | * List of filters to be applied to text nodes. 142 | */ 143 | textFilters?: JSXTextFilter[]; 144 | 145 | /** 146 | * When this option is enabled, non-existent HTML elements will not be rendered. 147 | */ 148 | disableUnknownHTMLElement?: boolean; 149 | 150 | /** 151 | * Function to determine Unknown HTML Element 152 | */ 153 | isUnknownHTMLElementTagName?: UnknownHTMLElementTagNameFunction; 154 | } 155 | 156 | interface RendererOptions extends { 157 | /** 158 | * JSX code 159 | */ 160 | code?: string; 161 | 162 | /** 163 | * The component that will be displayed instead when an error occurs. 164 | */ 165 | fallbackComponent?: JSXFallbackComponent; 166 | 167 | /** 168 | * If you want to receive the parsed result, set a Ref object to this option. 169 | */ 170 | refNodes?: Ref; 171 | } 172 | ``` 173 | 174 | ## Usage 175 | 176 | ### Using like a simple HTML template engine 177 | 178 | input: 179 | 180 | ```javascript 181 | import { render } from 'react-dom'; 182 | import { JSXRenderer } from 'react-jsx-renderer'; 183 | 184 | const root = document.getElementById('root'); 185 | 186 | render( 187 | Hello, {name}

'} 190 | />, 191 | root 192 | ); 193 | ``` 194 | 195 | to: 196 | 197 | ```html 198 |

Hello, Sho Kusano

199 | ``` 200 | 201 | ### Using JSX with JavaScript expressions 202 | 203 | input: 204 | 205 | ```javascript 206 | render( 207 | + {three + seven}

' + 214 | '

- {three - seven}

' + 215 | '

bitwise shift {three << seven}

' 216 | } 217 | />, 218 | root 219 | ); 220 | ``` 221 | 222 | to: 223 | 224 | ```html 225 |

+ 10

226 |

- -4

227 |

bitwise shift 384

228 | ``` 229 | 230 | ### Using JSX with your favorite custom components 231 | 232 | ```javascript 233 | const Red = ({ children }) => {children} 234 | 235 | render( 236 | red

'} 239 | />, 240 | root 241 | ); 242 | ``` 243 | 244 | to: 245 | 246 | ```html 247 |

red

248 | ``` 249 | 250 | ### Convert JSX with filters 251 | 252 | ```javascript 253 | const hrefFilter = (element: JSXElement) => { 254 | const { props, component, children } = element; 255 | if (component !== 'a') return element; 256 | 257 | let href = props.href || ''; 258 | if (href.includes('//')) { 259 | href = secureURLConvert(href); // Add prefix https://secure.url/redirect?url= 260 | } 261 | const filteredProps = { ...props, href }; 262 | return { component, children, props: filteredProps }; 263 | } 264 | 265 | render( 266 | root

' + 270 | '

upper directory

' + 271 | '

sub directory

' + 272 | '

github

' + 273 | } 274 | />, 275 | root 276 | ); 277 | ``` 278 | 279 | to: 280 | 281 | ```html 282 |

root

283 |

upper directory

284 |

sub directory

285 |

github

286 | ``` 287 | 288 | ### Provide options by context 289 | 290 | ex: Server side rendering. 291 | 292 | ```javascript 293 | import { JSDOM } from 'jsdom'; 294 | 295 | render( 296 | { 297 | const { window } = new JSDOM(); 298 | return window.document.createElement(tagName) instanceof window.HTMLUnknownElement; 299 | }}> 300 | Avoid

' 303 | } 304 | /> 305 |
, 306 | root 307 | ); 308 | ``` 309 | 310 | to: 311 | 312 | ```html 313 |

314 | ``` 315 | 316 | ## License 317 | 318 | [MIT License](https://github.com/rosylilly/react-jsx-renderer/blob/main/LICENSE) 319 | 320 | ## Related projects 321 | 322 | - [react-jsx-parser](https://github.com/TroyAlford/react-jsx-parser) 323 | -------------------------------------------------------------------------------- /doc/document.tsx: -------------------------------------------------------------------------------- 1 | import React, { VFC } from 'react'; 2 | import { Link, Route, Switch, useLocation } from 'react-router-dom'; 3 | import { Header } from './header'; 4 | import { LiveDemo, LiveDemos } from './liveDemo'; 5 | import { README } from './readme'; 6 | 7 | export const Document: VFC = () => { 8 | const location = useLocation(); 9 | 10 | return ( 11 | <> 12 |
13 | 14 |
15 |
16 |
17 |

React JSX Renderer

18 |

A React component for Rendering JSX

19 |
20 |
21 |
22 | 23 |
24 |
25 |
26 |
27 |
    28 |
  • 29 | 30 | README 31 | 32 |
  • 33 |
34 |

DEMO

35 |
    36 | {Object.entries(LiveDemos).map(([name, _]) => ( 37 |
  • 38 | 39 | {name} 40 | 41 |
  • 42 | ))} 43 |
44 |
45 |
46 | 47 |
48 | 49 | {Object.entries(LiveDemos).map(([name, props]) => ( 50 | 51 | 52 | 53 | ))} 54 | 55 | 56 | 57 | 58 |
59 |
60 |
61 | 62 | ); 63 | }; 64 | -------------------------------------------------------------------------------- /doc/header.tsx: -------------------------------------------------------------------------------- 1 | import React, { VFC } from 'react'; 2 | import GitHubButton from 'react-github-btn'; 3 | import packageJSON from '../package.json'; 4 | 5 | export const Header: VFC = () => { 6 | return ( 7 |
8 |
9 | 14 | 15 |
16 |
17 | 24 | Star 25 | 26 |
27 |
28 | 29 | View on GitHub 30 | 31 |
32 |
33 |
34 |
35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /doc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | React JSX Renderer 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /doc/index.scss: -------------------------------------------------------------------------------- 1 | @import 'bulma/bulma.sass'; 2 | 3 | .table .table { 4 | tr { 5 | th { 6 | padding-left: 0; 7 | } 8 | td { 9 | padding-right: 0; 10 | } 11 | } 12 | 13 | &.is-array, tfoot { 14 | td { 15 | padding-left: 0; 16 | } 17 | } 18 | 19 | tbody { 20 | tr:first-child { 21 | td, th { 22 | padding-top: 0; 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /doc/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { render } from 'react-dom'; 3 | import { HashRouter } from 'react-router-dom'; 4 | import { Document } from './document'; 5 | import './index.scss'; 6 | 7 | const main = () => { 8 | const root = document.getElementById('root'); 9 | 10 | render( 11 | 12 | 13 | , 14 | root, 15 | ); 16 | }; 17 | 18 | export default main(); 19 | -------------------------------------------------------------------------------- /doc/jsxEditor.tsx: -------------------------------------------------------------------------------- 1 | import CodeMirror from 'codemirror'; 2 | import 'codemirror/mode/jsx/jsx'; 3 | import React, { useEffect, useRef, VFC } from 'react'; 4 | 5 | export const JSXEditor: VFC<{ code: string; onChange: (code: string) => void }> = ({ code, onChange }) => { 6 | const wrapper = useRef(null); 7 | 8 | useEffect(() => { 9 | if (!wrapper.current) return () => {}; 10 | 11 | const editor = CodeMirror(wrapper.current, { mode: 'jsx', theme: 'monokai', lineNumbers: true, value: code }); 12 | editor.on('change', (editor) => { 13 | onChange(editor.getValue()); 14 | }); 15 | 16 | return () => { 17 | if (wrapper.current) { 18 | wrapper.current.removeChild(editor.getWrapperElement()); 19 | } 20 | }; 21 | }, [wrapper.current]); 22 | 23 | return
; 24 | }; 25 | -------------------------------------------------------------------------------- /doc/liveDemo.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, MouseEventHandler, useCallback, useState, VFC } from 'react'; 2 | import { Binding, JSXElementFilter, JSXNode, JSXRenderer, JSXRendererProps, JSXTextFilter } from '../src'; 3 | import { JSXEditor } from './jsxEditor'; 4 | import { ObjectEditor } from './objectEditor'; 5 | 6 | const defaultOptions: JSXRendererProps = { 7 | debug: true, 8 | disableCall: false, 9 | disableNew: false, 10 | disableUnknownHTMLElement: false, 11 | disableKeyGeneration: false, 12 | }; 13 | 14 | export const LiveDemo: VFC = ({ code, ...props }) => { 15 | const { binding, components, textFilters, elementFilters, ...options } = Object.assign({}, defaultOptions, props); 16 | const [state, update] = useState<{ code: string | undefined; binding: Binding | undefined; nodes: JSXNode[]; options: JSXRendererProps }>({ code, binding, options, nodes: [] }); 17 | const refNodes = useCallback( 18 | (nodes: JSXNode[]) => { 19 | update((state) => ({ ...state, nodes })); 20 | }, 21 | [update], 22 | ); 23 | 24 | return ( 25 |
26 |
27 |
28 |

JSX

29 | update((props) => ({ ...props, code }))} /> 30 |
31 | 32 |
33 |
BINDING
34 | update((s) => ({ ...s, binding }))} expanded={true} /> 35 |
36 | 37 |
38 |
OPTIONS
39 | update((s) => ({ ...s, options }))} expanded={true} /> 40 |
41 |
42 |
43 |
44 |

Preview

45 |
46 | 55 |
56 |
57 |
58 |

Nodes

59 |
 60 |             {JSON.stringify(state.nodes || [], null, 2)}
 61 |           
62 |
63 |
64 |
65 | ); 66 | }; 67 | 68 | const OnClickAlert: FC<{ message: string }> = ({ message, children }) => { 69 | const onClick = useCallback( 70 | (e) => { 71 | e.preventDefault(); 72 | alert(message); 73 | }, 74 | [message], 75 | ); 76 | 77 | return ( 78 | 81 | ); 82 | }; 83 | 84 | const BanWordFilter: JSXTextFilter = (node) => { 85 | const str = `${node}`; 86 | return str.replaceAll('BAN', '***'); 87 | }; 88 | 89 | const ClassNameFilter: JSXElementFilter = (node) => { 90 | const { props } = node; 91 | if (props['class'] && !props['className']) { 92 | props['className'] = props['class']; 93 | delete props['class']; 94 | } 95 | 96 | return { ...node, props }; 97 | }; 98 | 99 | export const LiveDemos: Record = { 100 | 'Hello, World': { code: '

React JSX Renderer

\n

Hello, World

' }, 101 | Binding: { 102 | code: 'Hello, {name}', 103 | binding: { 104 | name: 'React', 105 | number: 1, 106 | string: 'string', 107 | boolean: true, 108 | array: ['one', 'two', 'three'], 109 | object: { key: 'value' }, 110 | }, 111 | }, 112 | 'Custom Component': { 113 | code: 'Click Me', 114 | components: { OnClickAlert }, 115 | }, 116 | 'List Rendering': { 117 | code: '

Members:

\n
    {members.map((member) =>
  • {member}
  • )}
', 118 | binding: { 119 | members: ['Ada', 'Bob', 'Chris'], 120 | }, 121 | }, 122 | 'Event Handler': { 123 | code: ``, 124 | binding: { 125 | alert, 126 | }, 127 | }, 128 | 'Text Filter': { 129 | code: '

Ban Word filter is BAN replace to ***

', 130 | textFilters: [BanWordFilter], 131 | }, 132 | 'Element Filter': { 133 | code: '

Support class attribute.

', 134 | elementFilters: [ClassNameFilter], 135 | }, 136 | 'Disable Call': { 137 | code: `

Toogle below disableCall option

\n

{(() => 'Called')()}

\n

{UPPER_CASE.toLowerCase()}

`, 138 | binding: { 139 | UPPER_CASE: 'UPPER_CASE', 140 | }, 141 | disableCall: true, 142 | }, 143 | 'Disable New': { 144 | code: `

Toogle below disableNew option

\n

{(new Date())?.toISOString()}

`, 145 | binding: { 146 | Date, 147 | }, 148 | disableNew: true, 149 | }, 150 | 'Disable Unknown HTMLElement': { 151 | code: '

before

\nUnknown HTML Element\n

after

', 152 | disableUnknownHTMLElement: true, 153 | }, 154 | 'Allowed Functions': { 155 | code: [ 156 | '

Allowed functions: String.toLowerCase(), Array.map(), Array.join()

', 157 | '

{names.map((name) => name.toLowerCase()).join(", ")}

', 158 | '

{names.map((name) => name.toUpperCase()).join(", ")}

', 159 | '

{(() => "John".toLowerCase())()}

', 160 | ].join('\n'), 161 | binding: { 162 | names: ['John', 'Gonbe', 'Richard', 'Alan'], 163 | }, 164 | allowedFunctions: [String.prototype.toLowerCase, Array.prototype.map, Array.prototype.join], 165 | allowUserDefinedFunction: true, 166 | }, 167 | 'Denied Functions': { 168 | code: [ 169 | '

Denied functions: String.toLowerCase()

', 170 | '

{names.map((name) => name.toLowerCase()).join(", ")}

', 171 | '

{names.map((name) => name.toUpperCase()).join(", ")}

', 172 | '

{(() => "John".toUpperCase())()}

', 173 | ].join('\n'), 174 | binding: { 175 | names: ['John', 'Gonbe', 'Richard', 'Alan'], 176 | }, 177 | deniedFunctions: [String.prototype.toLowerCase], 178 | allowUserDefinedFunction: true, 179 | }, 180 | 'Named Component': { 181 | code: [ 182 | 'const ListItem = ({ item }) => {', 183 | ' console.log(`this is list item: ${item}`);', 184 | ' return
  • item: {item}
  • ;', 185 | '};', 186 | '', 187 | 'const List = ({ items }) => {', 188 | ' console.log("this is list");', 189 | ' return
      {items.map((item, idx) => )}
    ;', 190 | '};', 191 | '', 192 | 'export const Main = () => {', 193 | ' console.log("this is main");', 194 | ' const items = ["a", "b", "c"];', 195 | ' return ;', 196 | '};', 197 | ].join('\n'), 198 | binding: { 199 | console, 200 | }, 201 | component: 'Main', 202 | }, 203 | }; 204 | -------------------------------------------------------------------------------- /doc/objectEditor.tsx: -------------------------------------------------------------------------------- 1 | import React, { MouseEventHandler, useCallback, useState, VFC } from 'react'; 2 | 3 | export const ObjectEditor: VFC<{ object: any; onChange: (obj: any) => void; expanded?: boolean }> = ({ object, expanded, onChange }) => { 4 | const [isExpanded, updateExpanded] = useState(!!expanded); 5 | const [error, updateError] = useState(undefined); 6 | const [obj, update] = useState(() => object); 7 | 8 | const change = useCallback( 9 | (o: any) => { 10 | onChange(o); 11 | update(() => o); 12 | }, 13 | [onChange, update], 14 | ); 15 | 16 | const toggleExpand = useCallback( 17 | (e) => { 18 | e.preventDefault(); 19 | updateExpanded((now) => !now); 20 | }, 21 | [updateExpanded], 22 | ); 23 | 24 | if (Array.isArray(obj)) { 25 | return isExpanded ? ( 26 | 27 | 28 | {obj.map((val, idx) => ( 29 | 30 | 33 | 34 | ))} 35 | 36 | 37 | 38 | 43 | 44 | 45 |
    31 | change([...obj.splice(0, idx), o, ...obj.slice(idx + 1)])} /> 32 |
    39 | 42 |
    46 | ) : ( 47 | 48 | {'[ ... ]'} 49 | 50 | ); 51 | } 52 | 53 | let type = typeof obj; 54 | if (obj === null) type = 'undefined'; 55 | 56 | switch (type) { 57 | case 'function': { 58 | const name = obj.name ? `: ${obj.name}` : ''; 59 | return ; 60 | } 61 | case 'boolean': 62 | return ( 63 | 72 | ); 73 | case 'string': 74 | return ( 75 | { 81 | change(String(e.target.value)); 82 | }} 83 | /> 84 | ); 85 | case 'number': 86 | return ( 87 | { 93 | change(parseInt(e.target.value, 10)); 94 | }} 95 | /> 96 | ); 97 | case 'object': 98 | return isExpanded ? ( 99 |
    100 | 101 | 102 | {Object.entries(obj as Record) 103 | .sort(([a], [b]) => (a < b ? -1 : 1)) 104 | .map(([key, val], idx) => ( 105 | 106 | 120 | 123 | 124 | ))} 125 | 126 | 127 | 128 | 141 | 142 | {expanded ? null : ( 143 | 144 | 149 | 150 | )} 151 | 152 |
    107 | { 112 | const newObj = { ...obj }; 113 | const newKey = String(e.target.value); 114 | delete newObj[key]; 115 | newObj[newKey] = val; 116 | change(newObj); 117 | }} 118 | /> 119 | 121 | change({ ...obj, [key]: o })} /> 122 |
    129 | 140 |
    145 | 148 |
    153 |
    154 | ) : ( 155 | 156 | {'{ ... }'} 157 | 158 | ); 159 | default: 160 | return ( 161 | <> 162 | { 168 | try { 169 | const val = JSON.parse(e.target.value); 170 | change(val); 171 | updateError(undefined); 172 | } catch (err) { 173 | updateError(err); 174 | } 175 | }} 176 | /> 177 |

    Input JSON value

    178 | 179 | ); 180 | } 181 | }; 182 | -------------------------------------------------------------------------------- /doc/readme.tsx: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 2 | // @ts-ignore 3 | import text from 'bundle-text:../README.md'; 4 | import React, { VFC } from 'react'; 5 | import MarkdownIt from 'markdown-it'; 6 | import taskLists from 'markdown-it-task-lists'; 7 | 8 | const markdown = new MarkdownIt().use(taskLists); 9 | 10 | export const README: VFC = () => { 11 | const html = markdown.render(text); 12 | return
    ; 13 | }; 14 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testEnvironment: 'jsdom', 3 | collectCoverage: true, 4 | transform: { 5 | '^.+\\.(ts|tsx)$': 'ts-jest', 6 | }, 7 | globals: { 8 | 'ts-jest': { 9 | useESM: true, 10 | diagnostics: false, 11 | isolatedModules: true, 12 | }, 13 | }, 14 | moduleFileExtensions: ['tsx', 'ts', 'jsx', 'js', 'mjs'], 15 | testMatch: ['**/?(*.)+(spec|test).+(ts|tsx|js)'], 16 | setupFilesAfterEnv: ['./script/jest/jsdom-polyfill.js'], 17 | }; 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-jsx-renderer", 3 | "version": "1.3.1", 4 | "description": "A React component for Rendering JSX", 5 | "main": "dist/index.cjs", 6 | "module": "dist/index.mjs", 7 | "types": "dist/index.d.ts", 8 | "typings": "dist/index.d.ts", 9 | "source": [ 10 | "./doc/index.html" 11 | ], 12 | "doc": "./dist/index.html", 13 | "targets": { 14 | "doc": {}, 15 | "main": false, 16 | "module": false, 17 | "types": false 18 | }, 19 | "homepage": "https://github.com/rosylilly/react-jsx-renderer", 20 | "repository": { 21 | "type": "git", 22 | "url": "https://github.com/rosylilly/react-jsx-renderer.git" 23 | }, 24 | "keywords": [ 25 | "react", 26 | "jsx", 27 | "template", 28 | "renderer" 29 | ], 30 | "author": "Sho Kusano ", 31 | "license": "MIT", 32 | "files": [ 33 | "dist/**/*.{js,mjs,d.ts,map}", 34 | "src/**/*.{ts,tsx}", 35 | "package.json" 36 | ], 37 | "scripts": { 38 | "build": "./script/build.mjs", 39 | "doc:build": "parcel build ./doc/index.html", 40 | "doc:serve": "parcel serve ./doc/index.html", 41 | "test": "jest", 42 | "lint": "eslint --ext .js,.mjs,.ts,.tsx .", 43 | "clean": "rimraf dist", 44 | "prepack": "yarn clean && cross-env NODE_ENV=production yarn build" 45 | }, 46 | "resolutions": { 47 | "@types/react": ">=16.0.0", 48 | "@types/react-dom": ">=16.0.0", 49 | "react": ">=16.0.0", 50 | "react-dom": ">=16.0.0" 51 | }, 52 | "peerDependencies": { 53 | "react": ">=16.0.0" 54 | }, 55 | "dependencies": { 56 | "meriyah": "^4.1.5" 57 | }, 58 | "devDependencies": { 59 | "@parcel/transformer-inline-string": "^2.1.1", 60 | "@parcel/transformer-sass": "^2.1.1", 61 | "@types/codemirror": "^5.60.0", 62 | "@types/jest": "^27.0.0", 63 | "@types/jsdom": "^16.2.10", 64 | "@types/markdown-it": "^12.0.1", 65 | "@types/prismjs": "^1.16.5", 66 | "@types/react": ">=16.0.0", 67 | "@types/react-dom": ">=16.0.0", 68 | "@types/react-router-dom": "^5.1.7", 69 | "@types/react-test-renderer": "^17.0.1", 70 | "@typescript-eslint/eslint-plugin": "^4.25.0", 71 | "@typescript-eslint/parser": "^4.25.0", 72 | "bulma": "^0.9.2", 73 | "codemirror": "^5.61.1", 74 | "cross-env": "^7.0.3", 75 | "date-fns": "^2.22.1", 76 | "esbuild": "^0.12.5", 77 | "esbuild-plugin-d.ts": "^1.0.3", 78 | "eslint": "^7.27.0", 79 | "eslint-config-prettier": "^8.3.0", 80 | "eslint-plugin-import": "^2.23.3", 81 | "eslint-plugin-jest": "^24.3.6", 82 | "eslint-plugin-json": "^3.0.0", 83 | "eslint-plugin-prettier": "^4.0.0", 84 | "jest": "^27.0.0", 85 | "jsdom": "^17.0.0", 86 | "markdown-it": "^12.0.6", 87 | "markdown-it-task-lists": "^2.1.1", 88 | "parcel": "^2.1.1", 89 | "prettier": "^2.3.0", 90 | "prismjs": "^1.23.0", 91 | "react": ">=16.0.0", 92 | "react-dom": ">=16.0.0", 93 | "react-github-btn": "^1.2.0", 94 | "react-router-dom": "^5.2.0", 95 | "react-test-renderer": "^17.0.2", 96 | "rimraf": "^3.0.2", 97 | "ts-jest": "^27.0.0", 98 | "typescript": "^4.2.4" 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | printWidth: 180, 3 | tabWidth: 2, 4 | useTabs: false, 5 | semi: true, 6 | singleQuote: true, 7 | quoteProps: 'as-needed', 8 | jsxSingleQuote: false, 9 | trailingComma: 'all', 10 | bracketSpacing: true, 11 | jsxBracketSameLine: false, 12 | arrowParens: 'always', 13 | }; 14 | -------------------------------------------------------------------------------- /script/build.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { build } from 'esbuild'; 4 | import { dtsPlugin } from 'esbuild-plugin-d.ts'; 5 | import { rm, stat } from 'fs/promises'; 6 | import packageJSON from './package.json.cjs'; 7 | 8 | const mode = process.env.NODE_ENV === 'production' ? 'production' : 'development'; 9 | const isProduction = mode === 'production'; 10 | const dependencies = Object.keys(packageJSON.dependencies); 11 | const peerDependencies = Object.keys(packageJSON.peerDependencies); 12 | 13 | const size = (path) => stat(path).then((s) => s.size); 14 | 15 | const minify = isProduction; 16 | const external = [...dependencies, ...peerDependencies]; 17 | 18 | /** @type {import('esbuild').BuildOptions} */ 19 | const baseOptions = { 20 | entryPoints: ['./src/index.ts'], 21 | mainFields: ['module', 'main'], 22 | bundle: true, 23 | platform: 'node', 24 | sourcemap: true, 25 | external, 26 | minify, 27 | }; 28 | 29 | const compile = async (/** @type string */ outfile, /** @type {import('esbuild').BuildOptions} */ options) => { 30 | const start = Date.now(); 31 | return build({ 32 | ...baseOptions, 33 | ...options, 34 | outfile, 35 | }).then(async () => { 36 | const complete = Date.now(); 37 | const time = complete - start; 38 | const fileSize = await size(outfile); 39 | console.log(`===> Complete build: ${outfile}`, { fileSize, minify, time }); 40 | }); 41 | }; 42 | 43 | const cjs = async () => { 44 | return compile('dist/index.cjs', { target: ['es6', 'node14'], format: 'cjs' }); 45 | }; 46 | 47 | const esm = async () => { 48 | return compile('dist/index.mjs', { 49 | target: ['es2020', 'node16'], 50 | format: 'esm', 51 | plugins: [isProduction ? dtsPlugin({ outDir: './dist' }) : null].filter(Boolean), 52 | }); 53 | }; 54 | 55 | const run = async () => { 56 | await rm('dist', { recursive: true, force: true }); 57 | return Promise.all([cjs(), esm()]); 58 | }; 59 | 60 | export default run(); 61 | -------------------------------------------------------------------------------- /script/jest/jsdom-polyfill.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Polyfill for https://github.com/jsdom/jsdom/issues/2524 3 | */ 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-var-requires 6 | const { TextEncoder, TextDecoder } = require('util'); 7 | global.TextEncoder = TextEncoder; 8 | global.TextDecoder = TextDecoder; 9 | -------------------------------------------------------------------------------- /script/nightly.mjs: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'fs/promises'; 2 | import { dirname, resolve } from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | import { format } from 'date-fns'; 5 | import pkgJson from './package.json.cjs'; 6 | 7 | const __dirname = dirname(fileURLToPath(import.meta.url)); 8 | 9 | const main = async () => { 10 | const date = format(new Date(), 'yyyyMMddHHmm'); 11 | const version = `${pkgJson.version}-nightly.${date}`; 12 | console.log(version); 13 | 14 | const newPackageJson = { 15 | ...pkgJson, 16 | version, 17 | }; 18 | 19 | const pkgJsonPath = resolve(__dirname, '..', 'package.json'); 20 | 21 | await writeFile(pkgJsonPath, JSON.stringify(newPackageJson, undefined, 2)); 22 | }; 23 | 24 | export default main(); 25 | -------------------------------------------------------------------------------- /script/package.json.cjs: -------------------------------------------------------------------------------- 1 | module.exports = require('../package.json'); 2 | -------------------------------------------------------------------------------- /script/react-version.mjs: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'fs/promises'; 2 | import { dirname, resolve } from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | import pkgJson from './package.json.cjs'; 5 | 6 | const [reactVersion] = process.argv.slice(2); 7 | 8 | const __dirname = dirname(fileURLToPath(import.meta.url)); 9 | 10 | const main = async () => { 11 | const reactVersions = { 12 | '@types/react': `${reactVersion}`, 13 | '@types/react-dom': `${reactVersion}`, 14 | react: `${reactVersion}`, 15 | 'react-dom': `${reactVersion}`, 16 | 'react-test-renderer': `${reactVersion}`, 17 | }; 18 | 19 | const newPackageJson = { 20 | ...pkgJson, 21 | devDependencies: { 22 | ...(pkgJson.devDependencies || {}), 23 | ...reactVersions, 24 | }, 25 | resolutions: { 26 | ...(pkgJson.resolutions || {}), 27 | ...reactVersions, 28 | }, 29 | }; 30 | 31 | const pkgJsonPath = resolve(__dirname, '..', 'package.json'); 32 | 33 | await writeFile(pkgJsonPath, JSON.stringify(newPackageJson, undefined, 2)); 34 | }; 35 | 36 | export default main(); 37 | -------------------------------------------------------------------------------- /src/evaluate/bind.ts: -------------------------------------------------------------------------------- 1 | import { ESTree } from 'meriyah'; 2 | import { JSXContext } from './context'; 3 | import { evalExpression } from './expression'; 4 | 5 | export interface IdentifierBinding { 6 | type: 'Identifier'; 7 | name: string; 8 | default?: any; 9 | } 10 | 11 | export interface PathBinding { 12 | [key: string]: Binding; 13 | } 14 | 15 | export interface ObjectBinding { 16 | type: 'Object'; 17 | binds: PathBinding; 18 | rest: RestBinding | null; 19 | default?: any; 20 | } 21 | 22 | export interface ArrayBinding { 23 | type: 'Array'; 24 | binds: (Binding | null)[]; 25 | rest: RestBinding | null; 26 | default?: any; 27 | } 28 | 29 | export interface RestBinding { 30 | type: 'Rest'; 31 | bind: Binding; 32 | default?: any; 33 | } 34 | 35 | export interface MemberBinding { 36 | type: 'Member'; 37 | object: any; 38 | property: string; 39 | default?: any; 40 | } 41 | 42 | export type Binding = IdentifierBinding | ObjectBinding | ArrayBinding | RestBinding | MemberBinding; 43 | 44 | export const evalBindingPattern = (bind: ESTree.BindingPattern | ESTree.AssignmentPattern | ESTree.Expression, context: JSXContext): Binding => { 45 | switch (bind.type) { 46 | case 'Identifier': 47 | return evalIdentifierBinding(bind, context); 48 | case 'AssignmentPattern': 49 | return evalAssignmentPattern(bind, context); 50 | case 'MemberExpression': 51 | return evalMemberBinding(bind, context); 52 | default: 53 | return evalExpression(bind, context); 54 | } 55 | }; 56 | 57 | export const evalIdentifierBinding = (bind: ESTree.Identifier, _: JSXContext): IdentifierBinding => { 58 | return { 59 | type: 'Identifier', 60 | name: bind.name, 61 | }; 62 | }; 63 | 64 | export const evalObjectPattern = (bind: ESTree.ObjectPattern, context: JSXContext): ObjectBinding => { 65 | const binding: ObjectBinding = { 66 | type: 'Object', 67 | binds: {}, 68 | rest: null, 69 | }; 70 | 71 | bind.properties.forEach((prop) => { 72 | switch (prop.type) { 73 | case 'Property': { 74 | const key = prop.key.type === 'Identifier' ? prop.key.name : evalExpression(prop.key, context); 75 | const val = evalBindingPattern(prop.value, context); 76 | binding.binds[key] = val; 77 | break; 78 | } 79 | case 'RestElement': { 80 | binding.rest = evalRestElement(prop, context); 81 | } 82 | } 83 | }); 84 | 85 | return binding; 86 | }; 87 | 88 | export const evalArrayPattern = (bind: ESTree.ArrayPattern, context: JSXContext): ArrayBinding => { 89 | const binding: ArrayBinding = { 90 | type: 'Array', 91 | binds: [], 92 | rest: null, 93 | }; 94 | 95 | bind.elements.forEach((element) => { 96 | if (element === null) { 97 | binding.binds.push(null); 98 | return; 99 | } 100 | 101 | switch (element.type) { 102 | case 'RestElement': 103 | binding.rest = evalRestElement(element, context); 104 | break; 105 | default: 106 | binding.binds.push(evalBindingPattern(element, context)); 107 | } 108 | }); 109 | 110 | return binding; 111 | }; 112 | 113 | export const evalAssignmentPattern = (bind: ESTree.AssignmentPattern, context: JSXContext) => { 114 | const binding = evalBindingPattern(bind.left, context); 115 | binding.default = bind.right ? evalExpression(bind.right, context) : undefined; 116 | return binding; 117 | }; 118 | 119 | export const evalRestElement = (bind: ESTree.RestElement, context: JSXContext): RestBinding => { 120 | return { 121 | type: 'Rest', 122 | bind: evalBindingPattern(bind.argument, context), 123 | default: undefined, 124 | }; 125 | }; 126 | 127 | export const evalMemberBinding = (bind: ESTree.MemberExpression, context: JSXContext): MemberBinding => { 128 | const property = 129 | bind.property.type === 'Identifier' ? bind.property.name : bind.property.type === 'PrivateIdentifier' ? bind.property.name : evalExpression(bind.property, context); 130 | 131 | return { 132 | type: 'Member', 133 | object: evalExpression(bind.object, context), 134 | property, 135 | }; 136 | }; 137 | 138 | type DefineKind = ESTree.VariableDeclaration['kind']; 139 | 140 | export const setBinding = (bind: Binding, val: any, context: JSXContext, define?: DefineKind) => { 141 | switch (bind.type) { 142 | case 'Identifier': 143 | return setIdentifierBinding(bind, val, context, define); 144 | case 'Object': 145 | return setObjectBinding(bind, val, context, define); 146 | case 'Array': 147 | return setArrayBinding(bind, val, context, define); 148 | case 'Rest': 149 | return setBinding(bind.bind, val, context, define); 150 | case 'Member': 151 | return setMemberBinding(bind, val, context, define); 152 | } 153 | }; 154 | 155 | const setIdentifierBinding = (id: IdentifierBinding, val: any, context: JSXContext, define?: DefineKind) => { 156 | if (define) context.defineVariable(define, id.name); 157 | val !== undefined && context.setVariable(id.name, val); 158 | return val; 159 | }; 160 | 161 | const setObjectBinding = (obj: ObjectBinding, val: any, context: JSXContext, define?: DefineKind) => { 162 | val = Object.assign({}, val); 163 | for (const [key, bind] of Object.entries(obj.binds)) { 164 | const value = val[key] === undefined ? bind.default : val[key]; 165 | setBinding(bind, value, context, define); 166 | delete val[key]; 167 | } 168 | if (obj.rest) { 169 | setBinding(obj.rest, val, context, define); 170 | } 171 | return val; 172 | }; 173 | 174 | const setArrayBinding = (ary: ArrayBinding, val: any, context: JSXContext, define?: DefineKind) => { 175 | const values = [...val] as any[]; 176 | for (let idx = 0; idx < ary.binds.length; idx++) { 177 | const bind = ary.binds[idx]; 178 | const value = values.shift(); 179 | if (!bind) continue; 180 | 181 | setBinding(bind, value === undefined ? bind.default : value, context, define); 182 | } 183 | 184 | if (ary.rest) { 185 | setBinding(ary.rest, values, context, define); 186 | } 187 | return val; 188 | }; 189 | 190 | const setMemberBinding = (binding: MemberBinding, val: any, context: JSXContext, _define?: DefineKind) => { 191 | context.pushStack(binding.object); 192 | binding.object[binding.property] = val; 193 | context.popStack(); 194 | return val; 195 | }; 196 | -------------------------------------------------------------------------------- /src/evaluate/class.ts: -------------------------------------------------------------------------------- 1 | import { ESTree } from 'meriyah'; 2 | import { evalIdentifierBinding, setBinding } from './bind'; 3 | import { JSXContext } from './context'; 4 | import { Definition, evalMethodDefinition, evalPropertyDefinition } from './definition'; 5 | import { evalExpression, evalFunctionExpression } from './expression'; 6 | import { AnyFunction } from './options'; 7 | 8 | export const evalClassDeclaration = (declaration: ESTree.ClassDeclaration, context: JSXContext) => { 9 | const constructor = evalClassDeclarationBase(declaration, context); 10 | 11 | if (declaration.id) { 12 | const binding = evalIdentifierBinding(declaration.id, context); 13 | setBinding(binding, constructor, context, 'const'); 14 | } 15 | return constructor; 16 | }; 17 | 18 | export const evalClassExpression = (expression: ESTree.ClassExpression, context: JSXContext) => { 19 | return evalClassDeclarationBase(expression, context); 20 | }; 21 | 22 | export const evalClassDeclarationBase = (base: ESTree.ClassDeclaration | ESTree.ClassExpression, context: JSXContext) => { 23 | let constructor: AnyFunction = function () {}; 24 | const klass = function (...args: any[]) { 25 | context.pushStack(this); 26 | constructor.call(this, ...args); 27 | context.popStack(); 28 | }; 29 | 30 | for (const stmt of base.body.body) { 31 | let definition: Definition | AnyFunction | undefined; 32 | switch (stmt.type) { 33 | case 'PropertyDefinition': 34 | definition = evalPropertyDefinition(stmt, context); 35 | break; 36 | default: { 37 | definition = evalClassElement(stmt, context); 38 | } 39 | } 40 | 41 | if (!definition) break; 42 | if (typeof definition === 'function') { 43 | // noop 44 | } else { 45 | const target = definition.static ? klass : klass.prototype; 46 | const descripter = Object.getOwnPropertyDescriptor(target, definition.key); 47 | switch (definition.kind) { 48 | case 'constructor': 49 | constructor = definition.value; 50 | break; 51 | case 'method': 52 | Object.defineProperty(target, definition.key, { configurable: false, enumerable: false, value: definition.value }); 53 | break; 54 | case 'get': 55 | Object.defineProperty(target, definition.key, { ...(descripter || {}), configurable: true, enumerable: true, get: definition.value }); 56 | break; 57 | case 'set': 58 | Object.defineProperty(target, definition.key, { ...(descripter || {}), configurable: true, enumerable: true, set: definition.value }); 59 | break; 60 | case 'property': 61 | Object.defineProperty(target, definition.key, { ...(descripter || {}), configurable: true, enumerable: true, writable: true, value: definition.value }); 62 | break; 63 | } 64 | } 65 | } 66 | 67 | if (base.superClass) { 68 | const superClass = evalExpression(base.superClass, context); 69 | Object.setPrototypeOf(klass, Object.getPrototypeOf(superClass)); 70 | 71 | Object.defineProperty(klass, 'super', { enumerable: false, writable: true, value: superClass }); 72 | } 73 | Object.defineProperty(klass.prototype, 'constructor', { enumerable: false, writable: true, value: klass }); 74 | 75 | if (base.id) { 76 | Object.defineProperty(klass, 'name', { enumerable: false, configurable: true, writable: false, value: base.id.name }); 77 | } 78 | 79 | return klass; 80 | }; 81 | 82 | export const evalClassElement = (element: ESTree.ClassElement, context: JSXContext) => { 83 | switch (element.type) { 84 | case 'FunctionExpression': { 85 | return evalFunctionExpression(element, context); 86 | } 87 | case 'MethodDefinition': 88 | return evalMethodDefinition(element, context); 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /src/evaluate/context.ts: -------------------------------------------------------------------------------- 1 | import { ESTree } from 'meriyah'; 2 | import { Binding, ComponentsBinding } from '../types/binding'; 3 | import { AnyFunction, EvaluateOptions } from './options'; 4 | 5 | class KeyGenerator { 6 | private readonly prefix: string; 7 | private readonly counter: number[]; 8 | 9 | constructor(prefix?: string) { 10 | this.prefix = prefix || ''; 11 | this.counter = [0]; 12 | } 13 | 14 | public increment() { 15 | this.counter[this.counter.length - 1]++; 16 | } 17 | 18 | public openingElement() { 19 | this.counter.push(0); 20 | } 21 | 22 | public closingElement() { 23 | this.counter.pop(); 24 | } 25 | 26 | public generate(): string { 27 | this.increment(); 28 | const key = this.counter.map((counter) => counter.toFixed(0)).join('-'); 29 | return this.prefix ? `${this.prefix}-${key}` : key; 30 | } 31 | } 32 | 33 | type VariableKind = ESTree.VariableDeclaration['kind']; 34 | 35 | class Variable { 36 | public readonly kind: VariableKind; 37 | private init = false; 38 | private stored: any = undefined; 39 | 40 | constructor(kind: VariableKind) { 41 | this.kind = kind; 42 | } 43 | 44 | get value(): any { 45 | return this.stored; 46 | } 47 | 48 | set value(val: any) { 49 | if (this.init && this.kind === 'const') return; 50 | this.stored = val; 51 | this.init = true; 52 | } 53 | } 54 | 55 | class Stack { 56 | public readonly parent: Stack | undefined; 57 | public readonly self: any; 58 | private variables: Map; 59 | 60 | constructor(parent: Stack | undefined, self: any, init: Record) { 61 | this.parent = parent; 62 | this.self = self; 63 | this.variables = new Map(); 64 | for (const [key, value] of Object.entries(init)) { 65 | const variable = new Variable('const'); 66 | variable.value = value; 67 | this.variables.set(key, variable); 68 | } 69 | } 70 | 71 | public get(name: string): Variable | undefined { 72 | return this.variables.get(name) || (this.parent ? this.parent.get(name) : undefined); 73 | } 74 | 75 | public define(kind: VariableKind, name: string) { 76 | this.variables.set(name, new Variable(kind)); 77 | } 78 | 79 | public set(name: string, value: any) { 80 | const variable = this.variables.get(name); 81 | if (!variable) return this.parent ? this.parent.set(name, value) : undefined; 82 | variable.value = value; 83 | } 84 | } 85 | 86 | const systemVariables = { 87 | undefined: undefined, 88 | null: null, 89 | true: true, 90 | false: false, 91 | } as const; 92 | 93 | export class JSXContext { 94 | public readonly options: EvaluateOptions; 95 | public readonly keyGenerator: KeyGenerator; 96 | public readonly binding: Binding; 97 | public readonly components: ComponentsBinding; 98 | public readonly allowedFunctions: AnyFunction[]; 99 | public readonly deniedFunctions: AnyFunction[]; 100 | public readonly exports: Record; 101 | 102 | public stack: Stack; 103 | 104 | constructor(options: EvaluateOptions) { 105 | this.options = options; 106 | this.keyGenerator = new KeyGenerator(options.keyPrefix); 107 | 108 | this.binding = options.binding || {}; 109 | this.components = options.components || {}; 110 | 111 | this.allowedFunctions = [...(options.allowedFunctions || [])]; 112 | this.deniedFunctions = [...(options.deniedFunctions || [])]; 113 | 114 | this.stack = new Stack(new Stack(undefined, undefined, systemVariables), undefined, this.binding); 115 | this.exports = {}; 116 | } 117 | 118 | public get stackSize(): number { 119 | const getStackSize = (stack: Stack, num: number): number => (stack.parent ? getStackSize(stack.parent, num) + 1 : num); 120 | 121 | return getStackSize(this.stack, 1); 122 | } 123 | 124 | public pushStack(self: any) { 125 | this.stack = new Stack(this.stack, self, {}); 126 | } 127 | 128 | public popStack() { 129 | this.stack = this.stack.parent as Stack; 130 | } 131 | 132 | public defineVariable(kind: VariableKind, name: string) { 133 | this.stack.define(kind, name); 134 | } 135 | 136 | public setVariable(name: string, value: any) { 137 | this.stack.set(name, value); 138 | } 139 | 140 | public resolveThis(): any { 141 | return this.stack ? this.stack.self : undefined; 142 | } 143 | 144 | public resolveIdentifier(name: string): Variable | undefined { 145 | return this.stack.get(name); 146 | } 147 | 148 | public resolveComponent(name: string): any { 149 | const component = this.resolveIdentifier(name); 150 | if (component) return component.value; 151 | 152 | const allComponents = Object.assign({}, this.options.disableSearchCompontsByBinding ? {} : this.binding, this.components); 153 | 154 | return name.split('.').reduce((components, part) => { 155 | return components[part] || part; 156 | }, allComponents); 157 | } 158 | 159 | public export(name: string, value: any) { 160 | this.exports[name] = value; 161 | } 162 | 163 | private _label: string | undefined; 164 | public get label(): string | undefined { 165 | const label = this._label; 166 | this._label = undefined; 167 | return label; 168 | } 169 | 170 | public set label(l: string | undefined) { 171 | this._label = l; 172 | } 173 | 174 | public isAllowedFunction(func: AnyFunction): boolean { 175 | return !this.isDeniedFunc(func) && this.isAllowedFunc(func); 176 | } 177 | 178 | private isAllowedFunc(func: AnyFunction): boolean { 179 | if (!this.hasAllowedFunctions) return true; 180 | 181 | const match = this.allowedFunctions.reduce((match, f) => match || f === func, false); 182 | return match; 183 | } 184 | 185 | private isDeniedFunc(func: AnyFunction): boolean { 186 | const match = this.deniedFunctions.reduce((match, f) => match || f === func, false); 187 | return match; 188 | } 189 | 190 | public get hasAllowedFunctions(): boolean { 191 | return !!this.options.allowedFunctions; 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/evaluate/definition.ts: -------------------------------------------------------------------------------- 1 | import { ESTree } from 'meriyah'; 2 | import { JSXContext } from './context'; 3 | import { evalExpression, evalFunctionExpression } from './expression'; 4 | import { AnyFunction } from './options'; 5 | 6 | export interface MethodDefinition { 7 | type: 'Method'; 8 | kind: ESTree.MethodDefinition['kind']; 9 | key: string; 10 | value: AnyFunction; 11 | static: boolean; 12 | } 13 | 14 | export interface PropertyDefinition { 15 | type: 'Property'; 16 | kind: 'property'; 17 | key: string; 18 | value: AnyFunction; 19 | static: boolean; 20 | } 21 | 22 | export type Definition = MethodDefinition | PropertyDefinition; 23 | 24 | export const evalMethodDefinition = (exp: ESTree.MethodDefinition, context: JSXContext): MethodDefinition | undefined => { 25 | if (!exp.key) return undefined; 26 | 27 | let key: string; 28 | switch (exp.key.type) { 29 | case 'Identifier': 30 | key = exp.key.name; 31 | break; 32 | case 'PrivateIdentifier': 33 | key = exp.key.name; 34 | break; 35 | default: 36 | key = evalExpression(exp.key, context); 37 | break; 38 | } 39 | const value = evalFunctionExpression(exp.value, context); 40 | 41 | return { 42 | type: 'Method', 43 | kind: exp.kind, 44 | static: exp.static, 45 | key, 46 | value, 47 | }; 48 | }; 49 | 50 | export const evalPropertyDefinition = (exp: ESTree.PropertyDefinition, context: JSXContext): PropertyDefinition => { 51 | let key: string; 52 | switch (exp.key.type) { 53 | case 'Identifier': 54 | key = exp.key.name; 55 | break; 56 | case 'PrivateIdentifier': 57 | key = exp.key.name; 58 | break; 59 | default: 60 | key = evalExpression(exp.key, context); 61 | break; 62 | } 63 | const value = evalExpression(exp.value, context); 64 | 65 | return { 66 | type: 'Property', 67 | kind: 'property', 68 | static: exp.static, 69 | key, 70 | value, 71 | }; 72 | }; 73 | -------------------------------------------------------------------------------- /src/evaluate/error.ts: -------------------------------------------------------------------------------- 1 | import { ESTree } from 'meriyah'; 2 | import { JSXContext } from './context'; 3 | 4 | class JSXError extends Error { 5 | constructor(message: string) { 6 | super(message); 7 | 8 | Object.defineProperty(this, 'name', { configurable: true, enumerable: false, value: this.constructor.name, writable: false }); 9 | Object.setPrototypeOf(this, new.target.prototype); 10 | 11 | if (Error.captureStackTrace) { 12 | Error.captureStackTrace(this, JSXError); 13 | } 14 | } 15 | } 16 | 17 | export class JSXEvaluateError extends JSXError { 18 | public readonly source: Error | undefined; 19 | public readonly node: ESTree.Node; 20 | public readonly context: JSXContext; 21 | 22 | constructor(source: Error | string, node: ESTree.Node, context: JSXContext) { 23 | const loc = node?.loc?.start; 24 | const message = source instanceof Error ? source.message : source; 25 | super([loc ? `[${loc.line}:${loc.column}] ` : '', `${message}`].join('')); 26 | 27 | if (source instanceof Error) this.source = source; 28 | this.node = node; 29 | this.context = context; 30 | 31 | Object.defineProperty(this, 'name', { configurable: true, enumerable: false, value: this.constructor.name, writable: false }); 32 | Object.setPrototypeOf(this, new.target.prototype); 33 | 34 | if (this.source) { 35 | this.stack = this.source.stack; 36 | } else if (Error.captureStackTrace) { 37 | Error.captureStackTrace(this, JSXEvaluateError); 38 | } 39 | } 40 | } 41 | 42 | export class JSXBreak extends JSXError { 43 | public readonly label: string | undefined; 44 | 45 | constructor(label?: string) { 46 | super(`break${label ? ` ${label}` : ''}`); 47 | this.label = label; 48 | 49 | Object.defineProperty(this, 'name', { configurable: true, enumerable: false, value: this.constructor.name, writable: false }); 50 | Object.setPrototypeOf(this, new.target.prototype); 51 | 52 | if (Error.captureStackTrace) { 53 | Error.captureStackTrace(this, JSXBreak); 54 | } 55 | } 56 | 57 | public get isLabeled(): boolean { 58 | return this.label !== undefined; 59 | } 60 | } 61 | 62 | export class JSXContinue extends JSXError { 63 | public readonly label: string | undefined; 64 | 65 | constructor(label?: string) { 66 | super(`continue${label ? ` ${label}` : ''}`); 67 | this.label = label; 68 | 69 | Object.defineProperty(this, 'name', { configurable: true, enumerable: false, value: this.constructor.name, writable: false }); 70 | Object.setPrototypeOf(this, new.target.prototype); 71 | 72 | if (Error.captureStackTrace) { 73 | Error.captureStackTrace(this, JSXContinue); 74 | } 75 | } 76 | 77 | public get isLabeled(): boolean { 78 | return this.label !== undefined; 79 | } 80 | } 81 | 82 | export class JSXReturn extends JSXError { 83 | public readonly value: any; 84 | 85 | constructor(value: any) { 86 | super('return'); 87 | this.value = value; 88 | 89 | Object.defineProperty(this, 'name', { configurable: true, enumerable: false, value: this.constructor.name, writable: false }); 90 | Object.setPrototypeOf(this, new.target.prototype); 91 | 92 | if (Error.captureStackTrace) { 93 | Error.captureStackTrace(this, JSXReturn); 94 | } 95 | } 96 | } 97 | 98 | export const wrapJSXError = (e: any, node: ESTree.Node, context: JSXContext): JSXError => { 99 | if (e instanceof JSXError) return e; 100 | const error = e instanceof Error ? e : new Error(e); 101 | const jsxError = new JSXEvaluateError(error, node, context); 102 | return jsxError; 103 | }; 104 | -------------------------------------------------------------------------------- /src/evaluate/evaluate.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC } from 'react'; 2 | import { JSXNode } from '..'; 3 | import { Binding, ComponentsBinding } from '../types/binding'; 4 | import { evaluate, evaluateJSX } from './evaluate'; 5 | 6 | describe('evaluateJSX', () => { 7 | const mockConsoleTime = jest.spyOn(global.console, 'time').mockImplementation(); 8 | const mockConsoleTimeEnd = jest.spyOn(global.console, 'timeEnd').mockImplementation(); 9 | const ComponentA: FC = ({ children }) => <>{children}; 10 | const ComponentB: FC = ({ children }) => <>{children}; 11 | 12 | const components: ComponentsBinding = { 13 | A: ComponentA, 14 | B: ComponentB, 15 | }; 16 | const binding: Binding = { 17 | string: 'string', 18 | number: 1, 19 | boolean: true, 20 | regexp: /.*/, 21 | undefined: undefined, 22 | null: null, 23 | array: [1, 2, 3], 24 | object: { 25 | member: 'object-member', 26 | properties: { 27 | member: 'properties-member', 28 | }, 29 | }, 30 | components, 31 | }; 32 | 33 | const removeLoc = (node: JSXNode): JSXNode => { 34 | if (typeof node !== 'object') return node; 35 | const { loc: _loc, children, ...rest } = node; 36 | 37 | if (!children) return rest as JSXNode; 38 | return { ...rest, children: children.map((node) => removeLoc(node)) }; 39 | }; 40 | 41 | const test = (code: string, results: any[]) => { 42 | it(`should evaluate ${code}`, () => { 43 | const actual = evaluateJSX(code, { binding, components }).map((node) => removeLoc(node)); 44 | expect(actual).toStrictEqual(results); 45 | }); 46 | }; 47 | 48 | afterEach(() => { 49 | mockConsoleTime.mockClear(); 50 | mockConsoleTimeEnd.mockClear(); 51 | }); 52 | 53 | test('hello', ['hello']); 54 | test('{}', [undefined]); 55 | test('{1}', [1]); 56 | test('
    ', [{ type: 'element', component: 'hr', props: { key: '1' }, children: [] }]); 57 | test('

    test

    ', [{ type: 'element', component: 'p', props: { key: '1', foo: 'string' }, children: ['test'] }]); 58 | test('{...[1, 2, 3]}', [{ type: 'fragment', props: { key: '1' }, children: [1, 2, 3] }]); 59 | test('{{ a: 1 }}', [{ a: 1 }]); 60 | 61 | test('test', [{ type: 'element', component: 'xs:test', props: { key: '1' }, children: ['test'] }]); 62 | test('test', [{ type: 'element', component: ComponentA, props: { key: '1' }, children: ['test'] }]); 63 | test('test', [{ type: 'element', component: ComponentA, props: { key: '1' }, children: ['test'] }]); 64 | 65 | it('should call console.time on debug mode', () => { 66 | evaluate('const a = 1', { debug: true }); 67 | 68 | expect(mockConsoleTime).toHaveBeenCalledTimes(2); 69 | expect(mockConsoleTimeEnd).toHaveBeenCalledTimes(2); 70 | }); 71 | 72 | it('should call console.time on debug mode(JSX)', () => { 73 | evaluateJSX('

    Hello

    ', { debug: true }); 74 | 75 | expect(mockConsoleTime).toHaveBeenCalledTimes(2); 76 | expect(mockConsoleTimeEnd).toHaveBeenCalledTimes(2); 77 | }); 78 | }); 79 | -------------------------------------------------------------------------------- /src/evaluate/evaluate.ts: -------------------------------------------------------------------------------- 1 | import { ESTree, Options, parseModule } from 'meriyah'; 2 | import { JSXNode } from '../types/node'; 3 | import { JSXContext } from './context'; 4 | import { evalJSXChild } from './expression'; 5 | import { EvaluateOptions, ParseOptions } from './options'; 6 | import { evalProgram } from './program'; 7 | 8 | const meriyahForceOptions: Options = { 9 | module: true, 10 | jsx: true, 11 | loc: true, 12 | }; 13 | 14 | export const parse = (code: string, options: ParseOptions): ESTree.Program => { 15 | const { meriyah, debug, forceExpression } = options; 16 | 17 | try { 18 | const parserOptions = Object.assign({}, meriyah || {}, meriyahForceOptions); 19 | debug && console.time('JSX parse'); 20 | const program = parseModule(forceExpression ? `<>${code}` : code, parserOptions); 21 | return program; 22 | } finally { 23 | debug && console.timeEnd('JSX parse'); 24 | } 25 | }; 26 | 27 | type EvaluateFunction = { 28 | (program: ESTree.Program, options?: EvaluateOptions): T; 29 | (program: string, options?: ParseOptions & EvaluateOptions): T; 30 | }; 31 | 32 | export const evaluate: EvaluateFunction = (program: ESTree.Program | string, options: ParseOptions & EvaluateOptions = {}): JSXContext => { 33 | if (typeof program === 'string') program = parse(program, options); 34 | 35 | const context = new JSXContext(options); 36 | try { 37 | options.debug && console.time('JSX eval '); 38 | evalProgram(program, context); 39 | return context; 40 | } finally { 41 | options.debug && console.timeEnd('JSX eval '); 42 | } 43 | }; 44 | 45 | export const evaluateJSX: EvaluateFunction = (program: ESTree.Program | string, options: ParseOptions & EvaluateOptions = {}): JSXNode[] => { 46 | if (typeof program === 'string') program = parse(program, { ...options, forceExpression: true }); 47 | 48 | const [fragmentExpression] = program.body; 49 | if (!fragmentExpression || fragmentExpression.type !== 'ExpressionStatement') { 50 | return []; 51 | } 52 | 53 | const fragment = fragmentExpression.expression; 54 | if (!fragment || fragment.type !== 'JSXFragment') { 55 | return []; 56 | } 57 | 58 | const context = new JSXContext(options); 59 | 60 | try { 61 | options.debug && console.time('JSX eval '); 62 | const nodes = fragment.children.map((child) => evalJSXChild(child, context)); 63 | return nodes; 64 | } finally { 65 | options.debug && console.timeEnd('JSX eval '); 66 | } 67 | }; 68 | -------------------------------------------------------------------------------- /src/evaluate/expression.test.ts: -------------------------------------------------------------------------------- 1 | import { ESTree } from 'meriyah'; 2 | import { Binding } from '../types'; 3 | import { JSXEvaluateError } from './error'; 4 | import { evaluateJSX } from './evaluate'; 5 | import { AnyFunction, EvaluateOptions, ParseOptions } from './options'; 6 | 7 | describe('Expression', () => { 8 | const binding: Binding = { 9 | name: 'rosylilly', 10 | object: { 11 | foo: 'foo', 12 | bar: 'bar', 13 | }, 14 | Array, 15 | Object, 16 | JSON, 17 | tag: (parts: string[]) => { 18 | return parts.join(' + '); 19 | }, 20 | nop: (...args) => args, 21 | }; 22 | 23 | const supported = (name: ESTree.Expression['type'], code: string, result: any, options: ParseOptions & EvaluateOptions = {}) => { 24 | it(`should be supported: ${name}`, () => { 25 | expect(evaluateJSX(`{${code}}`, { ...options, binding })[0]).toStrictEqual(result); 26 | }); 27 | }; 28 | 29 | const notSupported = (name: ESTree.Expression['type'], code: string, options: ParseOptions & EvaluateOptions = {}) => { 30 | it(`should not be supported: ${name}`, () => { 31 | expect(() => evaluateJSX(`{${code}}`, { ...options, binding })).toThrowError(JSXEvaluateError); 32 | }); 33 | }; 34 | 35 | supported('ArrayExpression', '[1, 2, 3, , 5]', [1, 2, 3, null, 5]); 36 | // supported('ArrayPattern') 37 | // supported('ArrowFunctionExpression', '() => {}', Function); 38 | supported('AssignmentExpression', 'a += 1', NaN); 39 | // notSupported('AwaitExpression', 'await promise()') 40 | supported('BinaryExpression', '1 + 1', 2); 41 | supported('CallExpression', '[1,2].join("-")', '1-2'); 42 | supported('ChainExpression', 'test?.foo', undefined); 43 | supported('ChainExpression', 'test?.()', undefined); 44 | // notSupported('ClassDeclaration', 'class {}'); 45 | // supported('ClassExpression', 'class {}'); 46 | supported('ConditionalExpression', 'true ? false ? 1 : 2 : 3', 2); 47 | // supported('FunctionExpression', 'function() { }'); 48 | supported('Identifier', 'name', 'rosylilly'); 49 | // notSupported('Import', 'import test as "test";'); 50 | notSupported('ImportExpression', 'import("test")'); 51 | supported('JSXElement', '

    test

    ', { type: 'element', component: 'p', props: { key: '1' }, children: ['test'], loc: { column: 3, line: 1 } }); 52 | supported('JSXFragment', '<>test', { type: 'fragment', props: { key: '1' }, children: ['test'], loc: { column: 3, line: 1 } }); 53 | supported('JSXSpreadChild', '...[1, 2, 3]', { children: [1, 2, 3], props: { key: '1' }, type: 'fragment', loc: undefined }); 54 | supported('Literal', '1.45', 1.45); 55 | supported('LogicalExpression', 'true && false || true', true); 56 | supported('MemberExpression', 'object.foo', 'foo'); 57 | notSupported('MetaProperty', 'import.meta', { meriyah: { module: true } }); 58 | supported('NewExpression', 'new Array()', []); 59 | supported('ObjectExpression', '{ a: 1 }', { a: 1 }); 60 | // supported('RestElement') 61 | // supported('SequenceExpression') 62 | supported('SpreadElement', 'nop({ ...object })', [binding.object]); 63 | // notSupported('Super', '() => { super() }()') 64 | supported('TaggedTemplateExpression', 'tag`this ${is} a ${pen}`', 'this + a + '); 65 | supported('TemplateLiteral', '`${object.foo} san`', 'foo san'); 66 | notSupported('ThisExpression', 'this.name'); 67 | supported('UnaryExpression', '~7.8', -8); 68 | supported('UpdateExpression', 'i++', NaN); 69 | // notSupported('YieldExpression', 'yield index') 70 | 71 | it('should raise exception undefined identifier', () => { 72 | expect(() => { 73 | evaluateJSX('{test}', { raiseReferenceError: true }); 74 | }).toThrowError('test is not defined'); 75 | 76 | expect(() => { 77 | evaluateJSX('{undefined}', { raiseReferenceError: true }); 78 | }).not.toThrowError('undefined is not defined'); 79 | 80 | expect(() => { 81 | evaluateJSX('{null}', { raiseReferenceError: true }); 82 | }).not.toThrowError('null is not defined'); 83 | }); 84 | 85 | it('should evaluate binary expression', () => { 86 | const sample = { 87 | '+': 3 + 7, 88 | '-': 3 - 7, 89 | '/': 3 / 7, 90 | '*': 3 * 7, 91 | '%': 3 % 7, 92 | '**': 3 ** 7, 93 | '<': 3 < 7, 94 | '>': 3 > 7, 95 | '<=': 3 <= 7, 96 | '>=': 3 >= 7, 97 | '==': false, // 3 == 7, 98 | '!=': true, // 3 != 7, 99 | '===': false, // 3 === 7, 100 | '!==': true, // 3 !== 7, 101 | '<<': 3 << 7, 102 | '>>': 3 >> 7, 103 | '>>>': 3 >>> 7, 104 | '&': 3 & 7, 105 | '|': 3 | 7, 106 | '^': 3 ^ 7, 107 | '&&': 3 && 7, 108 | '||': 3 || 7, 109 | '??': 3 ?? 7, 110 | }; 111 | for (const [op, val] of Object.entries(sample)) { 112 | expect(evaluateJSX(`{3 ${op} 7}`)).toStrictEqual([val]); 113 | } 114 | 115 | expect(evaluateJSX('{"stringify" in JSON}', { binding })).toStrictEqual([true]); 116 | expect(evaluateJSX('{object instanceof Object}', { binding })).toStrictEqual([true]); 117 | }); 118 | 119 | it('should evaluate unary expression', () => { 120 | const sample = { 121 | '+': +7, 122 | '-': -7, 123 | '~': ~7, 124 | '!': !7, 125 | void: void 7, 126 | }; 127 | for (const [op, val] of Object.entries(sample)) { 128 | expect(evaluateJSX(`{${op}(7)}`, { binding })).toStrictEqual([val]); 129 | } 130 | 131 | expect(evaluateJSX('{typeof "hello"}')).toStrictEqual(['string']); 132 | }); 133 | 134 | it('should evaluate arrow function', () => { 135 | const func = evaluateJSX('{() => {}}')[0]; 136 | expect(func).toBeInstanceOf(Function); 137 | 138 | const expRet = evaluateJSX('{(() => 100)()}')[0]; 139 | expect(expRet).toStrictEqual(100); 140 | 141 | const argRet = evaluateJSX('{((a) => a)(200)}')[0]; 142 | expect(argRet).toStrictEqual(200); 143 | 144 | const zeroRet = evaluateJSX('{((a) => a)(0)}')[0]; 145 | expect(zeroRet).toStrictEqual(0); 146 | 147 | const undefinedRet = evaluateJSX('{((a) => a)()}')[0]; 148 | expect(undefinedRet).toStrictEqual(undefined); 149 | 150 | const stmtRet = evaluateJSX('{((a) => { return a + 100 })(200)}')[0]; 151 | expect(stmtRet).toStrictEqual(300); 152 | 153 | const defaultParamRet = evaluateJSX('{((a, b = 2) => { return a + b + 100 })(200)}')[0]; 154 | expect(defaultParamRet).toStrictEqual(302); 155 | }); 156 | 157 | it('should evaluate function', () => { 158 | const func = evaluateJSX('{function() {}}')[0]; 159 | expect(func).toBeInstanceOf(Function); 160 | 161 | const expRet = evaluateJSX('{(function() { return 100 })()}')[0]; 162 | expect(expRet).toStrictEqual(100); 163 | 164 | const zeroRet = evaluateJSX('{(function (a) { return a })(0)}')[0]; 165 | expect(zeroRet).toStrictEqual(0); 166 | 167 | const undefinedRet = evaluateJSX('{(function (a) { return a })()}')[0]; 168 | expect(undefinedRet).toStrictEqual(undefined); 169 | 170 | const argRet = evaluateJSX('{(function (a) { return a })(200)}')[0]; 171 | expect(argRet).toStrictEqual(200); 172 | 173 | const stmtRet = evaluateJSX('{(function (a) { return a + 100 })(200)}')[0]; 174 | expect(stmtRet).toStrictEqual(300); 175 | 176 | const defaultParamRet = evaluateJSX('{(function (a, b = 2) { return a + b + 100 })(200)}')[0]; 177 | expect(defaultParamRet).toStrictEqual(302); 178 | }); 179 | 180 | it('should be activatable disable call', () => { 181 | expect(evaluateJSX('{(() => 1)()}', { disableCall: true })[0]).toBeUndefined(); 182 | expect(evaluateJSX('{"hello".toString()}', { disableCall: true })[0]).toBeUndefined(); 183 | expect(evaluateJSX('{new Date()}', { disableCall: true })[0]).toBeUndefined(); 184 | }); 185 | 186 | it('should avoid function call with allowed list', () => { 187 | const allowedFunctions: AnyFunction[] = [''.toUpperCase]; 188 | expect(evaluateJSX('{"Hello"}', { allowedFunctions })[0]).toStrictEqual('Hello'); 189 | expect(evaluateJSX('{"Hello".toUpperCase()}', { allowedFunctions })[0]).toStrictEqual('HELLO'); 190 | expect(() => evaluateJSX('{"Hello".toLowerCase()}', { allowedFunctions })[0]).toThrowError('toLowerCase is not allowed function'); 191 | }); 192 | 193 | it('should avoid function call with denied list', () => { 194 | const deniedFunctions: AnyFunction[] = [''.toLowerCase]; 195 | expect(evaluateJSX('{"Hello"}', { deniedFunctions })[0]).toStrictEqual('Hello'); 196 | expect(evaluateJSX('{"Hello".toUpperCase()}', { deniedFunctions })[0]).toStrictEqual('HELLO'); 197 | expect(() => evaluateJSX('{"Hello".toLowerCase()}', { deniedFunctions })[0]).toThrowError('toLowerCase is not allowed function'); 198 | }); 199 | 200 | it('should automated allow function', () => { 201 | const allowedFunctions = [String.prototype.toLowerCase, Array.prototype.map]; 202 | expect(evaluateJSX('{"Hello".toLowerCase()}', { allowedFunctions, allowUserDefinedFunction: true })[0]).toStrictEqual('hello'); 203 | expect(() => evaluateJSX('{"Hello".toUpperCase()}', { allowedFunctions, allowUserDefinedFunction: true })[0]).toThrowError('toUpperCase is not allowed function'); 204 | expect(evaluateJSX('{(() => "Hello")()}', { allowedFunctions, allowUserDefinedFunction: true })[0]).toStrictEqual('Hello'); 205 | expect(() => evaluateJSX('{(() => "Hello")()}', { allowedFunctions, allowUserDefinedFunction: false })[0]).toThrowError('f is not allowed function'); 206 | expect(evaluateJSX('{(function() { return "Hello" })()}', { allowedFunctions, allowUserDefinedFunction: true })[0]).toStrictEqual('Hello'); 207 | expect(() => evaluateJSX('{(function() { return "Hello" })()}', { allowedFunctions, allowUserDefinedFunction: false })[0]).toThrowError('f is not allowed function'); 208 | expect(evaluateJSX('{["A", "B", "C"].map((char) => char.toLowerCase())}', { allowedFunctions, allowUserDefinedFunction: true })[0]).toStrictEqual(['a', 'b', 'c']); 209 | expect(evaluateJSX('{["A", "B", "C"].map(function(char) { return char.toLowerCase() })}', { allowedFunctions, allowUserDefinedFunction: true })[0]).toStrictEqual([ 210 | 'a', 211 | 'b', 212 | 'c', 213 | ]); 214 | }); 215 | 216 | it('should valid template literal', () => { 217 | expect(evaluateJSX('{((a, b) => `template: ${b} and ${a}`)("Hello", "World")}')[0]).toStrictEqual('template: World and Hello'); 218 | }); 219 | }); 220 | -------------------------------------------------------------------------------- /src/evaluate/expression.ts: -------------------------------------------------------------------------------- 1 | import { ESTree } from 'meriyah'; 2 | import { ReactNode } from 'react'; 3 | import { JSXComponent, JSXElement, JSXFragment, JSXNode, JSXProperties, JSXText } from '../types/node'; 4 | import { evalArrayPattern, evalBindingPattern, evalObjectPattern, evalRestElement, setBinding } from './bind'; 5 | import { evalClassDeclaration, evalClassExpression } from './class'; 6 | import { JSXContext } from './context'; 7 | import { evalMethodDefinition } from './definition'; 8 | import { JSXEvaluateError, wrapJSXError } from './error'; 9 | import { bindFunction, evalFunction } from './function'; 10 | 11 | export const evalExpression = (exp: ESTree.Expression, context: JSXContext): any => { 12 | try { 13 | switch (exp.type) { 14 | case 'ArrayExpression': 15 | return evalArrayExpression(exp, context); 16 | case 'ArrayPattern': 17 | return evalArrayPattern(exp, context); 18 | case 'ArrowFunctionExpression': 19 | return evalArrowFunctionExpression(exp, context); 20 | case 'AssignmentExpression': 21 | return evalAssignmentExpression(exp, context); 22 | case 'AwaitExpression': 23 | return evalAwaitExpression(exp, context); 24 | case 'BinaryExpression': 25 | return evalBinaryExpression(exp, context); 26 | case 'CallExpression': 27 | return evalCallExpression(exp, context); 28 | case 'ChainExpression': 29 | return evalChainExpression(exp, context); 30 | case 'ClassDeclaration': 31 | return evalClassDeclaration(exp, context); 32 | case 'ClassExpression': 33 | return evalClassExpression(exp, context); 34 | case 'ConditionalExpression': 35 | return evalConditionalExpression(exp, context); 36 | case 'FunctionExpression': 37 | return evalFunctionExpression(exp, context); 38 | case 'Identifier': 39 | return evalIdentifier(exp, context); 40 | case 'Import': 41 | return evalImport(exp, context); 42 | case 'ImportExpression': 43 | return evalImportExpression(exp, context); 44 | case 'JSXClosingElement': 45 | return evalJSXClosingElement(exp, context); 46 | case 'JSXClosingFragment': 47 | return evalJSXClosingFragment(exp, context); 48 | case 'JSXElement': 49 | return evalJSXElement(exp, context); 50 | case 'JSXExpressionContainer': 51 | return evalJSXExpressionContainer(exp, context); 52 | case 'JSXFragment': 53 | return evalJSXFragment(exp, context); 54 | case 'JSXOpeningElement': 55 | return evalJSXOpeningElement(exp, context); 56 | case 'JSXOpeningFragment': 57 | return evalJSXOpeningFragment(exp, context); 58 | case 'JSXSpreadChild': 59 | return evalJSXSpreadChild(exp, context); 60 | case 'Literal': 61 | return evalLiteral(exp, context); 62 | case 'LogicalExpression': 63 | return evalLogicalExpression(exp, context); 64 | case 'MemberExpression': 65 | return evalMemberExpression(exp, context); 66 | case 'MetaProperty': 67 | return evalMetaProperty(exp, context); 68 | case 'NewExpression': 69 | return evalNewExpression(exp, context); 70 | case 'ObjectExpression': 71 | return evalObjectExpression(exp, context); 72 | case 'ObjectPattern': 73 | return evalObjectPattern(exp, context); 74 | case 'RestElement': 75 | return evalRestElement(exp, context); 76 | case 'SequenceExpression': 77 | return evalSequenceExpression(exp, context); 78 | case 'SpreadElement': 79 | return evalSpreadElement(exp, context); 80 | case 'Super': 81 | return evalSuper(exp, context); 82 | case 'TaggedTemplateExpression': 83 | return evalTaggedTemplateExpression(exp, context); 84 | case 'TemplateLiteral': 85 | return evalTemplateLiteral(exp, context); 86 | case 'ThisExpression': 87 | return evalThisExpression(exp, context); 88 | case 'UnaryExpression': 89 | return evalUnaryExpression(exp, context); 90 | case 'UpdateExpression': 91 | return evalUpdateExpression(exp, context); 92 | case 'YieldExpression': 93 | return evalYieldExpression(exp, context); 94 | default: 95 | throw new JSXEvaluateError('Not implemented expression', exp, context); 96 | } 97 | } catch (e) { 98 | throw wrapJSXError(e, exp, context); 99 | } 100 | }; 101 | 102 | export const evalArrayExpression = (exp: ESTree.ArrayExpression, context: JSXContext): Array => { 103 | return exp.elements.map((element) => (element ? evalExpression(element, context) : null)); 104 | }; 105 | 106 | export const evalArrowFunctionExpression = (exp: ESTree.ArrowFunctionExpression, context: JSXContext) => { 107 | const self = context.resolveThis(); 108 | const func = bindFunction(evalFunction(exp, context)[1], self, context); 109 | 110 | if (context.options.allowUserDefinedFunction && context.hasAllowedFunctions) { 111 | context.allowedFunctions.push(func); 112 | } 113 | 114 | return func; 115 | }; 116 | 117 | export const evalAssignmentExpression = (exp: ESTree.AssignmentExpression, context: JSXContext) => { 118 | const binding = evalBindingPattern(exp.left, context); 119 | 120 | const { operator } = exp; 121 | if (operator === '=') { 122 | const val = evalExpression(exp.right, context); 123 | setBinding(binding, val, context); 124 | return val; 125 | } else { 126 | const val = evalBinaryExpression( 127 | { 128 | type: 'BinaryExpression', 129 | operator: operator.slice(0, operator.length - 1), 130 | left: exp.left, 131 | right: exp.right, 132 | }, 133 | context, 134 | ); 135 | setBinding(binding, val, context); 136 | return val; 137 | } 138 | }; 139 | 140 | export const evalAwaitExpression = (exp: ESTree.AwaitExpression, context: JSXContext) => { 141 | throw new JSXEvaluateError('await is not supported', exp, context); 142 | }; 143 | 144 | export const evalBinaryExpression = (exp: ESTree.BinaryExpression, context: JSXContext) => { 145 | const left = () => evalExpression(exp.left, context); 146 | const right = () => evalExpression(exp.right, context); 147 | switch (exp.operator) { 148 | case '+': 149 | return left() + right(); 150 | case '-': 151 | return left() - right(); 152 | case '/': 153 | return left() / right(); 154 | case '*': 155 | return left() * right(); 156 | case '%': 157 | return left() % right(); 158 | case '**': 159 | return left() ** right(); 160 | // relational operators 161 | case 'in': 162 | return left() in right(); 163 | case 'instanceof': 164 | return left() instanceof right(); 165 | case '<': 166 | return left() < right(); 167 | case '>': 168 | return left() > right(); 169 | case '<=': 170 | return left() <= right(); 171 | case '>=': 172 | return left() >= right(); 173 | // equality operators 174 | case '==': 175 | return left() == right(); 176 | case '!=': 177 | return left() != right(); 178 | case '===': 179 | return left() === right(); 180 | case '!==': 181 | return left() !== right(); 182 | // bitwise shift operators 183 | case '<<': 184 | return left() << right(); 185 | case '>>': 186 | return left() >> right(); 187 | case '>>>': 188 | return left() >>> right(); 189 | // binary bitwise operators 190 | case '&': 191 | return left() & right(); 192 | case '|': 193 | return left() | right(); 194 | case '^': 195 | return left() ^ right(); 196 | default: 197 | throw new JSXEvaluateError(`Unknown binary operator: ${exp.operator}`, exp, context); 198 | } 199 | }; 200 | 201 | export const evalCallExpression = (exp: ESTree.CallExpression, context: JSXContext) => { 202 | if (context.options.disableCall) return undefined; 203 | 204 | try { 205 | const callee = exp.callee as ESTree.Expression; 206 | const receiver = callee.type === 'MemberExpression' ? evalExpression(callee.object, context) : context.resolveThis(); 207 | const getName = (callee: ESTree.Expression | ESTree.PrivateIdentifier) => { 208 | return callee.type === 'Identifier' ? callee.name : callee.type === 'MemberExpression' ? getName(callee.property) : null; 209 | }; 210 | 211 | if (exp.optional && receiver === undefined) return undefined; 212 | 213 | const method = evalExpression(callee, context) as (...args: any[]) => any; 214 | 215 | if (typeof method !== 'function') { 216 | throw new JSXEvaluateError(`${getName(callee) || 'f'} is not a function`, exp, context); 217 | } 218 | 219 | if (!context.isAllowedFunction(method)) { 220 | throw new JSXEvaluateError(`${getName(callee) || 'f'} is not allowed function`, exp, context); 221 | } 222 | 223 | const args = exp.arguments.map((arg) => evalExpression(arg, context)); 224 | 225 | context.pushStack(receiver); 226 | const retval = method.call(receiver, ...args); 227 | context.popStack(); 228 | return retval; 229 | } catch (e) { 230 | throw wrapJSXError(e, exp, context); 231 | } 232 | }; 233 | 234 | export const evalChainExpression = (exp: ESTree.ChainExpression, context: JSXContext) => { 235 | return evalExpression(exp.expression, context); 236 | }; 237 | 238 | export const evalConditionalExpression = (exp: ESTree.ConditionalExpression, context: JSXContext) => { 239 | return evalExpression(exp.test, context) ? evalExpression(exp.consequent, context) : evalExpression(exp.alternate, context); 240 | }; 241 | 242 | export const evalFunctionExpression = (exp: ESTree.FunctionExpression, context: JSXContext) => { 243 | const func = evalFunction(exp, context)[1]; 244 | 245 | if (context.options.allowUserDefinedFunction && context.hasAllowedFunctions) { 246 | context.allowedFunctions.push(func); 247 | } 248 | 249 | return func; 250 | }; 251 | 252 | export const evalIdentifier = (exp: ESTree.Identifier, context: JSXContext) => { 253 | const variable = context.resolveIdentifier(exp.name); 254 | if (!variable) { 255 | if (context.options.raiseReferenceError) { 256 | throw new JSXEvaluateError(`${exp.name} is not defined`, exp, context); 257 | } else { 258 | return undefined; 259 | } 260 | } 261 | return variable.value; 262 | }; 263 | 264 | export const evalImport = (exp: ESTree.Import, context: JSXContext) => { 265 | throw new JSXEvaluateError('import is not supported', exp, context); 266 | }; 267 | 268 | export const evalImportExpression = (exp: ESTree.ImportExpression, context: JSXContext) => { 269 | throw new JSXEvaluateError('import is not supported', exp, context); 270 | }; 271 | 272 | export const evalLiteral = (exp: ESTree.Literal, _context: JSXContext): ESTree.Literal['value'] => { 273 | return exp.value; 274 | }; 275 | 276 | export const evalLogicalExpression = (exp: ESTree.LogicalExpression, context: JSXContext) => { 277 | const left = () => evalExpression(exp.left, context); 278 | const right = () => evalExpression(exp.right, context); 279 | switch (exp.operator) { 280 | case '&&': 281 | return left() && right(); 282 | case '||': 283 | return left() || right(); 284 | case '??': 285 | return left() ?? right(); 286 | default: 287 | throw new JSXEvaluateError(`Unknown logical operator: ${exp.operator}`, exp, context); 288 | } 289 | }; 290 | 291 | export const evalMemberExpression = (exp: ESTree.MemberExpression, context: JSXContext) => { 292 | try { 293 | const { object, property } = exp; 294 | 295 | const receiver = evalExpression(object, context); 296 | const key = property.type === 'Identifier' ? property.name : property.type === 'PrivateIdentifier' ? property.name : evalExpression(property, context); 297 | 298 | if (exp.optional && receiver === undefined) return undefined; 299 | 300 | context.pushStack(receiver); 301 | const retval = receiver[key]; 302 | context.popStack(); 303 | return retval; 304 | } catch (e) { 305 | throw wrapJSXError(e, exp, context); 306 | } 307 | }; 308 | 309 | export const evalMetaProperty = (exp: ESTree.MetaProperty, context: JSXContext) => { 310 | throw new JSXEvaluateError('meta property is not supported', exp, context); 311 | }; 312 | 313 | export const evalNewExpression = (exp: ESTree.NewExpression, context: JSXContext) => { 314 | try { 315 | if (context.options.disableCall || context.options.disableNew) return undefined; 316 | 317 | const callee = evalExpression(exp.callee, context); 318 | const arugments = exp.arguments.map((arg) => evalExpression(arg, context)); 319 | return new callee(...arugments); 320 | } catch (e) { 321 | throw wrapJSXError(e, exp, context); 322 | } 323 | }; 324 | 325 | export const evalObjectExpression = (exp: ESTree.ObjectExpression, context: JSXContext) => { 326 | const object: Record = {}; 327 | exp.properties.forEach((property) => { 328 | evalObjectLiteralElementLike(object, property, context); 329 | }); 330 | return object; 331 | }; 332 | 333 | export const evalSequenceExpression = (exp: ESTree.SequenceExpression, context: JSXContext) => { 334 | return exp.expressions.reduce((_, e) => evalExpression(e, context), undefined); 335 | }; 336 | 337 | export const evalSpreadElement = (exp: ESTree.SpreadElement, context: JSXContext) => { 338 | return evalExpression(exp.argument, context); 339 | }; 340 | 341 | export const evalSuper = (_: ESTree.Super, context: JSXContext) => { 342 | const ctor = context.resolveThis().constructor; 343 | return ctor.super; 344 | }; 345 | 346 | export const evalTaggedTemplateExpression = (exp: ESTree.TaggedTemplateExpression, context: JSXContext) => { 347 | const { quasi } = exp; 348 | const tag = evalExpression(exp.tag, context); 349 | const quasis = quasi.quasis.map((q) => q.value.cooked); 350 | const expressions = quasi.expressions.map((e) => evalExpression(e, context)); 351 | return tag(quasis, ...expressions); 352 | }; 353 | 354 | const getLocStart = (node: ESTree.Node) => { 355 | if (node.loc) return node.loc.start; 356 | return { line: 0, column: 0 }; 357 | }; 358 | 359 | export const evalTemplateLiteral = (exp: ESTree.TemplateLiteral, context: JSXContext) => { 360 | return [...exp.expressions, ...exp.quasis] 361 | .sort((a, b) => { 362 | const aLoc = getLocStart(a); 363 | const bLoc = getLocStart(b); 364 | if (aLoc.line === bLoc.line) return aLoc.column - bLoc.column; 365 | return aLoc.line - bLoc.line; 366 | }) 367 | .map((e) => { 368 | switch (e.type) { 369 | case 'TemplateElement': 370 | return e.value.cooked; 371 | default: 372 | return evalExpression(e, context); 373 | } 374 | }) 375 | .join(''); 376 | }; 377 | 378 | export const evalThisExpression = (_: ESTree.ThisExpression, context: JSXContext) => { 379 | return context.resolveThis(); 380 | }; 381 | 382 | export const evalUnaryExpression = (exp: ESTree.UnaryExpression, context: JSXContext) => { 383 | switch (exp.operator) { 384 | case '+': 385 | return +evalExpression(exp.argument, context); 386 | case '-': 387 | return -evalExpression(exp.argument, context); 388 | case '~': 389 | return ~evalExpression(exp.argument, context); 390 | case '!': 391 | return !evalExpression(exp.argument, context); 392 | case 'void': 393 | return void evalExpression(exp.argument, context); 394 | // case 'delete': return delete this.evalExpression(expression.argument); 395 | case 'typeof': 396 | return typeof evalExpression(exp.argument, context); 397 | default: 398 | throw new JSXEvaluateError(`Unknown unary operator: ${exp.operator}`, exp, context); 399 | } 400 | }; 401 | 402 | export const evalUpdateExpression = (exp: ESTree.UpdateExpression, context: JSXContext) => { 403 | const binding = evalBindingPattern(exp.argument, context); 404 | const current = evalExpression(exp.argument, context); 405 | switch (exp.operator) { 406 | case '++': 407 | return setBinding(binding, current + 1, context); 408 | case '--': 409 | return setBinding(binding, current - 1, context); 410 | default: 411 | throw new JSXEvaluateError(`Unknown update operator: ${exp.operator}`, exp, context); 412 | } 413 | }; 414 | 415 | export const evalYieldExpression = (exp: ESTree.YieldExpression, context: JSXContext) => { 416 | throw new JSXEvaluateError('yield is not supported', exp, context); 417 | }; 418 | 419 | // ObjectLiteralElementLike 420 | 421 | const evalObjectLiteralElementLike = (object: any, exp: ESTree.ObjectLiteralElementLike, context: JSXContext) => { 422 | switch (exp.type) { 423 | case 'MethodDefinition': 424 | evalMethodDefinition(exp, context); 425 | break; 426 | case 'Property': { 427 | evalProperty(object, exp, context); 428 | break; 429 | } 430 | case 'SpreadElement': { 431 | Object.assign(object, evalSpreadElement(exp, context)); 432 | break; 433 | } 434 | } 435 | }; 436 | 437 | export const evalProperty = (object: any, exp: ESTree.Property, context: JSXContext) => { 438 | let key: any; 439 | if (exp.computed) { 440 | key = evalExpression(exp.key, context); 441 | } else { 442 | switch (exp.key.type) { 443 | case 'Literal': 444 | key = evalLiteral(exp.key, context); 445 | break; 446 | case 'Identifier': 447 | key = exp.key.name; 448 | break; 449 | } 450 | } 451 | 452 | const value = ((exp: ESTree.Property['value']) => { 453 | switch (exp.type) { 454 | case 'AssignmentPattern': 455 | case 'ArrayPattern': 456 | case 'ObjectPattern': 457 | return undefined; 458 | default: 459 | return evalExpression(exp, context); 460 | } 461 | })(exp.value); 462 | 463 | switch (exp.kind) { 464 | case 'init': 465 | object[key] = value; 466 | break; 467 | case 'get': 468 | Object.defineProperty(object, key, { get: bindFunction(value, object, context) }); 469 | break; 470 | case 'set': 471 | Object.defineProperty(object, key, { set: bindFunction(value, object, context) }); 472 | break; 473 | } 474 | }; 475 | 476 | /// JSXChild 477 | 478 | export const evalJSXChild = (jsx: ESTree.JSXChild, context: JSXContext): JSXNode => { 479 | switch (jsx.type) { 480 | case 'JSXEmptyExpression': 481 | return evalJSXEmptyExpression(jsx, context); 482 | case 'JSXText': 483 | return evalJSXText(jsx, context); 484 | // case 'JSXElement': return evalJSXElement(jsx, context); 485 | // case 'JSXExpressionContainer': return evalJSXExpressionContainer(jsx, context); 486 | // case 'JSXFragment': return evalJSXFragment(jsx, context); 487 | // case 'JSXSpreadChild': return evalJSXSpreadChild(jsx, context); 488 | default: 489 | return evalExpression(jsx, context); 490 | } 491 | }; 492 | 493 | export const evalJSXElement = (jsx: ESTree.JSXElement, context: JSXContext): JSXElement | ReactNode => { 494 | const { openingElement } = jsx; 495 | const [component, properties] = evalExpression(openingElement, context); 496 | const children = jsx.children.map((child) => evalJSXChild(child, context)); 497 | 498 | jsx.closingElement && evalExpression(jsx.closingElement, context); 499 | 500 | const { start: loc } = Object.assign({}, { start: undefined }, jsx.loc); 501 | 502 | return { 503 | type: 'element', 504 | component, 505 | props: properties, 506 | children, 507 | loc, 508 | }; 509 | }; 510 | 511 | export const evalJSXEmptyExpression = (_jsx: ESTree.JSXEmptyExpression, _context: JSXContext): JSXNode => { 512 | return undefined; 513 | }; 514 | 515 | export const evalJSXSpreadChild = (jsx: ESTree.JSXSpreadChild, context: JSXContext): JSXFragment | ReactNode | undefined => { 516 | const { expression } = jsx; 517 | const fragment = evalJSXFragment( 518 | { 519 | type: 'JSXFragment', 520 | openingFragment: { 521 | type: 'JSXOpeningFragment', 522 | }, 523 | closingFragment: { 524 | type: 'JSXClosingFragment', 525 | }, 526 | children: [], 527 | }, 528 | context, 529 | ); 530 | 531 | fragment.children = Array.from(evalJSXExpressionContainer({ type: 'JSXExpressionContainer', expression }, context)); 532 | return fragment; 533 | }; 534 | 535 | export const evalJSXExpressionContainer = (jsx: ESTree.JSXExpressionContainer, context: JSXContext): any => { 536 | const { expression } = jsx; 537 | switch (expression.type) { 538 | case 'JSXEmptyExpression': 539 | return evalJSXEmptyExpression(expression, context); 540 | default: 541 | return evalExpression(expression, context); 542 | } 543 | }; 544 | 545 | export const evalJSXFragment = (jsx: ESTree.JSXFragment, context: JSXContext): JSXFragment => { 546 | const { openingFragment } = jsx; 547 | const [, properties] = evalExpression(openingFragment, context); 548 | const children = jsx.children.map((child) => evalJSXChild(child, context)); 549 | 550 | evalExpression(jsx.closingFragment, context); 551 | 552 | const { start: loc } = Object.assign({}, { start: undefined }, jsx.loc); 553 | 554 | return { 555 | type: 'fragment', 556 | props: properties, 557 | children, 558 | loc, 559 | }; 560 | }; 561 | 562 | export const evalJSXText = (jsx: ESTree.JSXText, _context: JSXContext): JSXText => { 563 | return jsx.value; 564 | }; 565 | 566 | export const evalJSXClosingElement = (_jsx: ESTree.JSXClosingElement, context: JSXContext) => { 567 | context.keyGenerator.closingElement(); 568 | return undefined; 569 | }; 570 | 571 | export const evalJSXClosingFragment = (_jsx: ESTree.JSXClosingFragment, context: JSXContext) => { 572 | context.keyGenerator.closingElement(); 573 | return undefined; 574 | }; 575 | 576 | export const evalJSXOpeningElement = (jsx: ESTree.JSXOpeningElement, context: JSXContext): [JSXComponent, JSXProperties] => { 577 | const { attributes } = jsx; 578 | 579 | const name = evalJSXTagNameExpression(jsx.name, context); 580 | const component = context.resolveComponent(name); 581 | 582 | const properties: JSXProperties = {}; 583 | attributes.forEach((attribute) => { 584 | switch (attribute.type) { 585 | case 'JSXAttribute': { 586 | const [key, value] = evalJSXAttribute(attribute, context); 587 | properties[key] = value; 588 | break; 589 | } 590 | case 'JSXSpreadAttribute': { 591 | Object.assign(properties, evalJSXSpreadAttribute(attribute, context)); 592 | break; 593 | } 594 | } 595 | }); 596 | if (!context.options.disableKeyGeneration && properties['key'] === undefined) { 597 | const key = context.keyGenerator.generate(); 598 | properties['key'] = key; 599 | } 600 | 601 | context.keyGenerator.openingElement(); 602 | if (jsx.selfClosing) context.keyGenerator.closingElement(); 603 | 604 | return [component, properties]; 605 | }; 606 | 607 | export const evalJSXOpeningFragment = (_jsx: ESTree.JSXOpeningFragment, context: JSXContext): [undefined, JSXProperties] => { 608 | const properties: JSXProperties = {}; 609 | 610 | if (!context.options.disableKeyGeneration && properties['key'] === undefined) { 611 | properties['key'] = context.keyGenerator.generate(); 612 | } 613 | 614 | context.keyGenerator.openingElement(); 615 | return [undefined, properties]; 616 | }; 617 | 618 | /// JSXTagNameExpression 619 | 620 | export const evalJSXTagNameExpression = (jsx: ESTree.JSXTagNameExpression, context: JSXContext): string => { 621 | switch (jsx.type) { 622 | case 'JSXIdentifier': 623 | return evalJSXIdentifier(jsx, context); 624 | case 'JSXMemberExpression': 625 | return evalJSXMemberExpression(jsx, context); 626 | case 'JSXNamespacedName': 627 | return evalJSXNamespacedName(jsx, context); 628 | } 629 | }; 630 | 631 | export const evalJSXIdentifier = (jsx: ESTree.JSXIdentifier, _context: JSXContext): string => { 632 | const { name } = jsx; 633 | return name; 634 | }; 635 | 636 | export const evalJSXMemberExpression = (jsx: ESTree.JSXMemberExpression, context: JSXContext): string => { 637 | const { object, property } = jsx; 638 | return `${evalJSXTagNameExpression(object, context)}.${evalJSXIdentifier(property, context)}`; 639 | }; 640 | 641 | export const evalJSXNamespacedName = (jsx: ESTree.JSXNamespacedName, context: JSXContext): string => { 642 | const { namespace, name } = jsx; 643 | return `${evalJSXTagNameExpression(namespace, context)}:${evalJSXIdentifier(name, context)}`; 644 | }; 645 | 646 | /// JSXAttribute 647 | 648 | export const evalJSXAttribute = (jsx: ESTree.JSXAttribute, context: JSXContext): [string, any] => { 649 | const name = evalJSXTagNameExpression(jsx.name, context); 650 | const value = evalJSXAttributeValue(jsx.value, context); 651 | return [name, value]; 652 | }; 653 | 654 | export const evalJSXSpreadAttribute = (jsx: ESTree.JSXSpreadAttribute, context: JSXContext) => { 655 | return evalExpression(jsx.argument, context); 656 | }; 657 | 658 | /// JSXAttributeValue 659 | 660 | export const evalJSXAttributeValue = (jsx: ESTree.JSXAttributeValue, context: JSXContext) => { 661 | if (!jsx) return true; 662 | 663 | switch (jsx.type) { 664 | case 'JSXIdentifier': 665 | return evalJSXIdentifier(jsx, context); 666 | case 'Literal': 667 | return evalLiteral(jsx, context); 668 | case 'JSXElement': 669 | return evalJSXElement(jsx, context); 670 | case 'JSXFragment': 671 | return evalJSXFragment(jsx, context); 672 | case 'JSXExpressionContainer': 673 | return evalJSXExpressionContainer(jsx, context); 674 | case 'JSXSpreadChild': 675 | return evalJSXSpreadChild(jsx, context); 676 | } 677 | }; 678 | -------------------------------------------------------------------------------- /src/evaluate/function.ts: -------------------------------------------------------------------------------- 1 | import { ESTree } from 'meriyah'; 2 | import { evalBindingPattern, IdentifierBinding, setBinding } from './bind'; 3 | import { JSXContext } from './context'; 4 | import { JSXEvaluateError, JSXReturn } from './error'; 5 | import { evalExpression } from './expression'; 6 | import { AnyFunction } from './options'; 7 | import { evalStatement } from './statement'; 8 | 9 | export const evalFunction = ( 10 | exp: ESTree.FunctionDeclaration | ESTree.FunctionExpression | ESTree.ArrowFunctionExpression, 11 | context: JSXContext, 12 | ): [IdentifierBinding | undefined, AnyFunction] => { 13 | if (exp.async) { 14 | throw new JSXEvaluateError('async function not supported', exp, context); 15 | } 16 | 17 | const func = function (...args: any[]): any { 18 | let retval: any; 19 | context.pushStack(context.resolveThis()); 20 | exp.params.forEach((param) => { 21 | const bind = evalBindingPattern(param, context); 22 | setBinding(bind, bind.type === 'Rest' ? args : args.shift() ?? bind.default, context, 'let'); 23 | }); 24 | 25 | try { 26 | if (exp.body) { 27 | switch (exp.body.type) { 28 | case 'BlockStatement': 29 | evalStatement(exp.body, context); 30 | break; 31 | default: 32 | retval = evalExpression(exp.body, context); 33 | } 34 | } 35 | } catch (err) { 36 | if (err instanceof JSXReturn) { 37 | retval = err.value; 38 | } else { 39 | throw err; 40 | } 41 | } 42 | 43 | context.popStack(); 44 | return retval; 45 | }; 46 | 47 | let bind: IdentifierBinding | undefined = undefined; 48 | if (exp.type === 'FunctionDeclaration' || exp.type === 'FunctionExpression') { 49 | if (exp.id) { 50 | bind = evalBindingPattern(exp.id, context) as IdentifierBinding; 51 | setBinding(bind, func, context, 'let'); 52 | } 53 | } 54 | 55 | return [bind, func]; 56 | }; 57 | 58 | export const bindFunction = (func: AnyFunction, self: any, context: JSXContext): AnyFunction => { 59 | return (...args: any[]) => { 60 | context.pushStack(self); 61 | const retval = func(...args); 62 | context.popStack(); 63 | return retval; 64 | }; 65 | }; 66 | -------------------------------------------------------------------------------- /src/evaluate/index.ts: -------------------------------------------------------------------------------- 1 | export * from './error'; 2 | export * from './options'; 3 | export * from './context'; 4 | export * from './evaluate'; 5 | -------------------------------------------------------------------------------- /src/evaluate/options.ts: -------------------------------------------------------------------------------- 1 | import * as meriyah from 'meriyah'; 2 | import { Options } from '../types'; 3 | import { Binding, ComponentsBinding } from '../types/binding'; 4 | 5 | export type AnyFunction = (...args: any[]) => any; 6 | 7 | export interface ParseOptions extends Options { 8 | /** 9 | * Options of parser 10 | */ 11 | meriyah?: meriyah.Options; 12 | 13 | /** 14 | * When this option is enabled, always parse as an expression. 15 | */ 16 | forceExpression?: boolean; 17 | } 18 | 19 | export interface EvaluateOptions extends Options { 20 | /** 21 | * binding 22 | */ 23 | binding?: Binding; 24 | 25 | /** 26 | * components 27 | */ 28 | components?: ComponentsBinding; 29 | 30 | /** 31 | * Prefix of generated keys. 32 | */ 33 | keyPrefix?: string; 34 | 35 | /** 36 | * When this option is enabled, no key will be generated 37 | */ 38 | disableKeyGeneration?: boolean; 39 | 40 | /** 41 | * When this option is enabled, bindings will be excluded from the component search. 42 | */ 43 | disableSearchCompontsByBinding?: boolean; 44 | 45 | /** 46 | * When this option is enabled, Call Expression and New Expression will always return undefined. 47 | */ 48 | disableCall?: boolean; 49 | 50 | /** 51 | * When this option is enabled, New Expression will always return undefined. 52 | */ 53 | disableNew?: boolean; 54 | 55 | /** 56 | * When this option is enabled, access to undefined variables will raise an exception. 57 | */ 58 | raiseReferenceError?: boolean; 59 | 60 | /** 61 | * List of functions allowed to be executed. 62 | * 63 | * If empty, all functions will be allowed to execute. 64 | */ 65 | allowedFunctions?: AnyFunction[]; 66 | 67 | /** 68 | * Add user-defined functions to the allowed list. 69 | */ 70 | allowUserDefinedFunction?: boolean; 71 | 72 | /** 73 | * List of functions denied to be executed. 74 | * 75 | * If empty, all functions will be allowed to execute. 76 | */ 77 | deniedFunctions?: AnyFunction[]; 78 | } 79 | -------------------------------------------------------------------------------- /src/evaluate/program.ts: -------------------------------------------------------------------------------- 1 | import { ESTree } from 'meriyah'; 2 | import { JSXContext } from './context'; 3 | import { evalStatement } from './statement'; 4 | 5 | export const evalProgram = (prog: ESTree.Program, context: JSXContext) => { 6 | prog.body.forEach((stmt) => { 7 | evalStatement(stmt, context); 8 | }); 9 | }; 10 | -------------------------------------------------------------------------------- /src/evaluate/statement.test.ts: -------------------------------------------------------------------------------- 1 | import { ESTree } from 'meriyah'; 2 | import { Binding } from '../types'; 3 | import { JSXContext } from './context'; 4 | import { JSXEvaluateError } from './error'; 5 | import { evaluate } from './evaluate'; 6 | import { EvaluateOptions } from './options'; 7 | 8 | describe('Statement', () => { 9 | const binding: Binding = { 10 | name: 'rosylilly', 11 | object: { 12 | foo: 'foo', 13 | bar: 'bar', 14 | }, 15 | Array, 16 | tag: (parts: string[]) => { 17 | return parts.join(' + '); 18 | }, 19 | nop: (...args) => args, 20 | }; 21 | 22 | const supported = (name: ESTree.Statement['type'], code: string, options: EvaluateOptions = {}, f?: (matcher: jest.Matchers) => void) => { 23 | it(`should be supported: ${name}`, () => { 24 | const e = expect(() => evaluate(code, { ...options, binding })); 25 | f ? f(e) : e.not.toThrow(); 26 | }); 27 | }; 28 | 29 | const notSupported = (name: ESTree.Statement['type'], code: string, options: EvaluateOptions = {}) => { 30 | it(`should not be supported: ${name}`, () => { 31 | expect(() => evaluate(code, { ...options, binding })).toThrowError(JSXEvaluateError); 32 | }); 33 | }; 34 | 35 | supported('BlockStatement', '{ 1 }'); 36 | supported('BreakStatement', 'for (;;) { break; }'); 37 | supported('ClassDeclaration', 'class Foo {}'); 38 | // notSupported('ClassExpression', 'export default class {}', { meriyah: { module: true }}); 39 | supported('ContinueStatement', 'let i = 0; while (i < 1) { i++; continue }'); 40 | supported('DebuggerStatement', 'debugger;'); 41 | supported('DoWhileStatement', 'do { break; } while(true)'); 42 | supported('EmptyStatement', ';;;;;;;'); 43 | notSupported('ExportAllDeclaration', 'export * from "mod";'); 44 | supported('ExportDefaultDeclaration', 'export default {};'); 45 | supported('ExportNamedDeclaration', 'const a = 1; export { a };'); 46 | supported('ExpressionStatement', '1 + 1;'); 47 | supported('ForInStatement', 'for (const i in [1,2,3]) { break; }'); 48 | supported('ForOfStatement', 'for (const i of [1,2,3]) { break; }'); 49 | supported('ForStatement', 'for (let i = 0; i < 3; i++) { break; }'); 50 | supported('FunctionDeclaration', 'function f() { }'); 51 | supported('IfStatement', 'if(true) { }; if(false) { } else { }'); 52 | notSupported('ImportDeclaration', 'import "mod";'); 53 | supported('LabeledStatement', 'label: { 1 }'); 54 | supported('ReturnStatement', '(() => { return 1; })()'); 55 | supported('SwitchStatement', 'switch(1) { case 2: break }'); 56 | supported('ThrowStatement', 'throw "test";', {}, (m) => m.toThrowError('test')); 57 | supported('TryStatement', 'try { foo(); } catch(e) { }'); 58 | supported('VariableDeclaration', 'const a = 1'); 59 | supported('WhileStatement', 'while(1) { break; }'); 60 | // notSupported('WithStatement', 'with(1) { foo() }'); 61 | 62 | it('should declare variable', () => { 63 | const check = (context: JSXContext, name: string, val: any) => { 64 | const attr = context.resolveIdentifier(name); 65 | expect(attr).toBeDefined(); 66 | expect(attr.value).toStrictEqual(val); 67 | }; 68 | 69 | const context = evaluate('const a = 1, { b, c: { d, ...e }, ...f } = { b: 2, c: { d: 3, e: 4 }, f: 5 }, [g, h, , i, ...j] = [6, 7, 8, 9, 10, 11]'); 70 | check(context, 'a', 1); 71 | check(context, 'b', 2); 72 | check(context, 'd', 3); 73 | check(context, 'e', { e: 4 }); 74 | check(context, 'f', { f: 5 }); 75 | check(context, 'g', 6); 76 | check(context, 'h', 7); 77 | check(context, 'i', 9); 78 | check(context, 'j', [10, 11]); 79 | 80 | const withDefault = evaluate('const { a = 1 } = { b: 2 }'); 81 | check(withDefault, 'a', 1); 82 | }); 83 | 84 | it('should run for loop', () => { 85 | let counter = 0; 86 | const binding = { 87 | call() { 88 | counter++; 89 | }, 90 | }; 91 | 92 | const context = evaluate('for (let i = 0; i < 10; i++) { call(); if(i >= 9) { continue }; call(); }', { binding }); 93 | expect(counter).toEqual(19); 94 | expect(context.stackSize).toStrictEqual(2); 95 | }); 96 | 97 | it('should run for of loop', () => { 98 | const called = []; 99 | const binding = { 100 | call(arg: number) { 101 | called.push(arg); 102 | }, 103 | }; 104 | 105 | const context = evaluate('for (const num of [1, 2, 3]) { call(num) }', { binding }); 106 | expect(called).toStrictEqual([1, 2, 3]); 107 | expect(context.stackSize).toStrictEqual(2); 108 | }); 109 | 110 | it('should run for in loop', () => { 111 | const called = []; 112 | const binding = { 113 | call(arg: string) { 114 | called.push(arg); 115 | }, 116 | }; 117 | 118 | const context = evaluate('for (const num in [1, 2, 3]) { call(num) }', { binding }); 119 | expect(called).toStrictEqual(['0', '1', '2']); 120 | expect(context.stackSize).toStrictEqual(2); 121 | }); 122 | 123 | it('should run switch', () => { 124 | let counter = 0; 125 | const binding = { 126 | incr() { 127 | counter++; 128 | }, 129 | decr() { 130 | counter--; 131 | }, 132 | }; 133 | const context = evaluate( 134 | ` 135 | for(let i = 0; i < 3; i++) { 136 | switch (i) { 137 | case 0: 138 | incr(); 139 | case 1: 140 | incr(); 141 | break; 142 | case 2: 143 | decr(); 144 | } 145 | } 146 | `, 147 | { binding }, 148 | ); 149 | expect(counter).toEqual(2); 150 | expect(context.stackSize).toStrictEqual(2); 151 | }); 152 | 153 | it('should support export', () => { 154 | const context = evaluate( 155 | ` 156 | const a = 1; 157 | export default 'def'; 158 | export { a }; 159 | export { a as b }; 160 | export const c = 3; 161 | `, 162 | {}, 163 | ); 164 | expect(context.exports.default).toEqual('def'); 165 | expect(context.exports.a).toEqual(1); 166 | expect(context.exports.b).toEqual(1); 167 | expect(context.exports.c).toEqual(3); 168 | expect(context.stackSize).toStrictEqual(2); 169 | }); 170 | 171 | it('should support while', () => { 172 | let coutner = 0; 173 | const binding = { 174 | call() { 175 | return coutner++; 176 | }, 177 | }; 178 | const context = evaluate('let i = 0; while(i < 3 && call() >= 0) { call(); i++ }', { binding }); 179 | 180 | expect(coutner).toEqual(6); 181 | expect(context.stackSize).toStrictEqual(2); 182 | }); 183 | 184 | it('should support do while', () => { 185 | let coutner = 0; 186 | const binding = { 187 | call() { 188 | return coutner++; 189 | }, 190 | }; 191 | const context = evaluate('let i = 0; do { call(); i++ } while(i < 3 && call() > 0)', { binding }); 192 | 193 | expect(coutner).toEqual(5); 194 | expect(context.stackSize).toStrictEqual(2); 195 | }); 196 | 197 | it('should support try catch finally', () => { 198 | let coutner = 0; 199 | let error: any = undefined; 200 | const binding = { 201 | call() { 202 | coutner++; 203 | }, 204 | collectError(err: any) { 205 | error = err; 206 | }, 207 | }; 208 | 209 | const context = evaluate( 210 | ` 211 | try { 212 | call(); 213 | throw "test"; 214 | call(); 215 | } catch (err) { 216 | collectError(err); 217 | } finally { 218 | call(); 219 | } 220 | `, 221 | { binding }, 222 | ); 223 | 224 | expect(coutner).toEqual(2); 225 | expect(error).toEqual('test'); 226 | expect(context.stackSize).toEqual(2); 227 | }); 228 | 229 | it('should support function', () => { 230 | const binding = { 231 | console, 232 | }; 233 | const context = evaluate( 234 | ` 235 | let i = 0; 236 | 237 | function incr() { 238 | i++; 239 | } 240 | 241 | const decr = function() { i-- }; 242 | 243 | incr(); 244 | incr(); 245 | decr(); 246 | export { i }; 247 | `, 248 | { binding }, 249 | ); 250 | 251 | expect(context.exports['i']).toEqual(1); 252 | expect(context.stackSize).toEqual(2); 253 | }); 254 | 255 | it('should support label with continue', () => { 256 | const logs = []; 257 | const binding = { 258 | log(line: string) { 259 | logs.push(line); 260 | }, 261 | }; 262 | evaluate( 263 | ` 264 | var i, j; 265 | 266 | loop1: 267 | for (i = 0; i < 3; i++) { 268 | loop2: 269 | for (j = 0; j < 3; j++) { 270 | if (i === 1 && j === 1) { 271 | continue loop1; 272 | } 273 | log('i = ' + i + ', j = ' + j); 274 | } 275 | } 276 | `, 277 | { binding }, 278 | ); 279 | 280 | expect(logs).toStrictEqual(['i = 0, j = 0', 'i = 0, j = 1', 'i = 0, j = 2', 'i = 1, j = 0', 'i = 2, j = 0', 'i = 2, j = 1', 'i = 2, j = 2']); 281 | }); 282 | 283 | it('should support label with break', () => { 284 | const logs = []; 285 | const binding = { 286 | log(line: string) { 287 | logs.push(line); 288 | }, 289 | }; 290 | evaluate( 291 | ` 292 | var i, j; 293 | 294 | loop1: 295 | for (i = 0; i < 3; i++) { 296 | loop2: 297 | for (j = 0; j < 3; j++) { 298 | if (i === 1 && j === 1) { 299 | break loop1; 300 | } 301 | log('i = ' + i + ', j = ' + j); 302 | } 303 | } 304 | `, 305 | { binding }, 306 | ); 307 | 308 | expect(logs).toStrictEqual(['i = 0, j = 0', 'i = 0, j = 1', 'i = 0, j = 2', 'i = 1, j = 0']); 309 | }); 310 | 311 | it('should support complex jump', () => { 312 | const logs = []; 313 | const binding = { 314 | log(message: string) { 315 | logs.push(message); 316 | }, 317 | }; 318 | 319 | evaluate( 320 | ` 321 | block: { 322 | let whileCount = 0; 323 | whileLabel: while (true) { 324 | whileCount++; 325 | log("while"); 326 | 327 | switch (whileCount) { 328 | case 1: 329 | continue whileLabel; 330 | case 2: 331 | continue; 332 | case 3: 333 | break whileLabel; 334 | } 335 | } 336 | 337 | let doWhileCount = 0; 338 | doWhileLabel: do { 339 | doWhileCount++; 340 | log("do while"); 341 | 342 | switch (doWhileCount) { 343 | case 1: 344 | continue doWhileLabel; 345 | case 2: 346 | continue; 347 | case 3: 348 | break doWhileLabel; 349 | } 350 | } while(true) 351 | 352 | forLabel: for (let forCount = 0; forCount < 4; forCount++) { 353 | log("for"); 354 | 355 | switch (forCount) { 356 | case 0: 357 | continue forLabel; 358 | case 1: 359 | continue; 360 | case 2: 361 | break forLabel; 362 | } 363 | } 364 | 365 | forOf: for (const a of [1, 2, 3, 4]) { 366 | log("for of " + a); 367 | 368 | switch (a) { 369 | case 1: 370 | continue forOf; 371 | case 2: 372 | continue; 373 | case 3: 374 | break forOf; 375 | } 376 | } 377 | 378 | forIn: for (const a in [1, 2, 3, 4]) { 379 | log("for in " + a); 380 | 381 | switch (a) { 382 | case '0': 383 | continue forIn; 384 | case '1': 385 | continue; 386 | case '2': 387 | break forIn; 388 | } 389 | } 390 | break block; 391 | 392 | log("not reached"); 393 | } 394 | `, 395 | { binding }, 396 | ); 397 | 398 | expect(logs).toStrictEqual([ 399 | 'while', 400 | 'while', 401 | 'while', 402 | 'do while', 403 | 'do while', 404 | 'do while', 405 | 'for', 406 | 'for', 407 | 'for', 408 | 'for of 1', 409 | 'for of 2', 410 | 'for of 3', 411 | 'for in 0', 412 | 'for in 1', 413 | 'for in 2', 414 | ]); 415 | }); 416 | 417 | it('should evaluate complex assignments', () => { 418 | const context = evaluate(` 419 | const a = 1; 420 | const { b } = { b: 2 }; 421 | const [c] = [3]; 422 | const { ['d' + 'd' ]: d } = { dd: 4 }; 423 | const e = ((a) => a)(5); 424 | const f = (({ a }) => a)({ a: 6 }); 425 | const g = (([a]) => a)([7]); 426 | const h = (({ ['d' + 'd']: a }) => a)({ dd: 8 }); 427 | const i = (({ a, ...b }) => b)({ a: 9, b: 9 }); 428 | const j = (([a, ...b]) => b)([10, 10, 10]); 429 | const k = ((a, ...b) => b)(11, 11, 11); 430 | const [, l] = [11, 12]; 431 | const m = ((a = 13) => a)(); 432 | const [n = 14] = []; 433 | const { o = 15 } = {}; 434 | `); 435 | 436 | const expects = { 437 | a: 1, 438 | b: 2, 439 | c: 3, 440 | d: 4, 441 | e: 5, 442 | f: 6, 443 | g: 7, 444 | h: 8, 445 | i: { b: 9 }, 446 | j: [10, 10], 447 | k: [11, 11], 448 | l: 12, 449 | m: 13, 450 | n: 14, 451 | o: 15, 452 | }; 453 | for (const [key, val] of Object.entries(expects)) { 454 | expect(context.resolveIdentifier(key).value).toStrictEqual(val); 455 | } 456 | }); 457 | 458 | it('should evaluate complex object', () => { 459 | evaluate( 460 | ` 461 | const object = { 462 | a: 1, 463 | get b() { return 2 }, 464 | set c(v) { 465 | this.d = v; 466 | }, 467 | e(v) { return v + this.b; } 468 | }; 469 | expect(object.a).toStrictEqual(1); 470 | expect(object.b).toStrictEqual(2); 471 | object.c = 3; 472 | expect(object.d).toStrictEqual(3); 473 | expect(object.e(2)).toStrictEqual(4); 474 | `, 475 | { binding: { expect } }, 476 | ); 477 | }); 478 | 479 | it('should evaluate class', () => { 480 | evaluate( 481 | ` 482 | class Animal { 483 | weight = 6; 484 | #privateProp = 8; 485 | ['a' + 2] = 10; 486 | constructor(name) { 487 | this.name = name; 488 | this.age = 3; 489 | } 490 | foo() { 491 | return 2; 492 | } 493 | get yearsOld() { 494 | return this.age 495 | } 496 | set yearsOld(a) { 497 | this.age = a; 498 | } 499 | static walk() { return 5 } 500 | publicMethod() { 501 | return this.#privateMethod(); 502 | } 503 | #privateMethod() { return 7 } 504 | get publicProp() { 505 | return this.#privateProp; 506 | } 507 | ['a' + 1]() { 508 | return 9; 509 | } 510 | } 511 | 512 | class Dog extends Animal { 513 | constructor() { 514 | super('dog'); 515 | } 516 | } 517 | 518 | const animal = new Animal('bird'); 519 | expect(animal.name).toStrictEqual('bird'); 520 | expect(animal.foo()).toStrictEqual(2); 521 | expect(animal.yearsOld).toStrictEqual(3); 522 | animal.yearsOld = 4; 523 | expect(animal.yearsOld).toStrictEqual(4); 524 | expect(Animal.walk()).toStrictEqual(5); 525 | expect(animal.weight).toStrictEqual(6); 526 | expect(animal.publicMethod()).toStrictEqual(7); 527 | expect(animal.publicProp).toStrictEqual(8); 528 | expect(animal.a1()).toStrictEqual(9); 529 | expect(animal.a2).toStrictEqual(10); 530 | 531 | const dog = new Dog(); 532 | expect(dog.name).toStrictEqual('dog'); 533 | `, 534 | { 535 | meriyah: { 536 | next: true, 537 | }, 538 | binding: { 539 | expect, 540 | console, 541 | }, 542 | }, 543 | ); 544 | }); 545 | }); 546 | -------------------------------------------------------------------------------- /src/evaluate/statement.ts: -------------------------------------------------------------------------------- 1 | import { ESTree } from 'meriyah'; 2 | import { Binding, evalBindingPattern, setBinding } from './bind'; 3 | import { evalClassDeclaration, evalClassExpression } from './class'; 4 | import { JSXContext } from './context'; 5 | import { JSXBreak, JSXContinue, JSXEvaluateError, JSXReturn } from './error'; 6 | import { evalExpression } from './expression'; 7 | import { evalFunction } from './function'; 8 | 9 | export const evalStatement = (stmt: ESTree.Statement, context: JSXContext) => { 10 | switch (stmt.type) { 11 | case 'BlockStatement': 12 | return evalBlockStatement(stmt, context); 13 | case 'BreakStatement': 14 | return evalBreakStatement(stmt, context); 15 | case 'ClassDeclaration': 16 | return evalClassDeclaration(stmt, context); 17 | case 'ClassExpression': 18 | return evalClassExpression(stmt, context); 19 | case 'ContinueStatement': 20 | return evalContinueStatement(stmt, context); 21 | case 'DebuggerStatement': 22 | return evalDebuggerStatement(stmt, context); 23 | case 'DoWhileStatement': 24 | return evalDoWhileStatement(stmt, context); 25 | case 'EmptyStatement': 26 | return evalEmptyStatement(stmt, context); 27 | case 'ExportAllDeclaration': 28 | return evalExportAllDeclaration(stmt, context); 29 | case 'ExportDefaultDeclaration': 30 | return evalExportDefaultDeclaration(stmt, context); 31 | case 'ExportNamedDeclaration': 32 | return evalExportNamedDeclaration(stmt, context); 33 | case 'ExpressionStatement': 34 | return evalExpressionStatement(stmt, context); 35 | case 'ForInStatement': 36 | return evalForInStatement(stmt, context); 37 | case 'ForOfStatement': 38 | return evalForOfStatement(stmt, context); 39 | case 'ForStatement': 40 | return evalForStatement(stmt, context); 41 | case 'FunctionDeclaration': 42 | return evalFunctionDeclaration(stmt, context); 43 | case 'IfStatement': 44 | return evalIfStatement(stmt, context); 45 | case 'ImportDeclaration': 46 | return evalImportDeclaration(stmt, context); 47 | case 'LabeledStatement': 48 | return evalLabeledStatement(stmt, context); 49 | case 'ReturnStatement': 50 | return evalReturnStatement(stmt, context); 51 | case 'SwitchStatement': 52 | return evalSwitchStatement(stmt, context); 53 | case 'ThrowStatement': 54 | return evalThrowStatement(stmt, context); 55 | case 'TryStatement': 56 | return evalTryStatement(stmt, context); 57 | case 'VariableDeclaration': 58 | return evalVariableDeclaration(stmt, context); 59 | case 'WhileStatement': 60 | return evalWhileStatement(stmt, context); 61 | case 'WithStatement': 62 | return evalWithStatement(stmt, context); 63 | default: 64 | throw new JSXEvaluateError('Not implemented statement', stmt, context); 65 | } 66 | }; 67 | 68 | export const evalBlockStatement = (stmt: ESTree.BlockStatement, context: JSXContext) => { 69 | const label = context.label; 70 | 71 | for (const child of stmt.body) { 72 | try { 73 | evalStatement(child, context); 74 | } catch (err) { 75 | if (label) { 76 | if (err instanceof JSXBreak) { 77 | if (err.isLabeled) { 78 | if (err.label === label) { 79 | break; 80 | } else { 81 | throw err; 82 | } 83 | } else { 84 | break; 85 | } 86 | } 87 | } 88 | throw err; 89 | } 90 | } 91 | }; 92 | 93 | export const evalBreakStatement = (stmt: ESTree.BreakStatement, __: JSXContext) => { 94 | throw new JSXBreak(stmt.label ? stmt.label.name : undefined); 95 | }; 96 | 97 | export const evalContinueStatement = (stmt: ESTree.ContinueStatement, __: JSXContext) => { 98 | throw new JSXContinue(stmt.label ? stmt.label.name : undefined); 99 | }; 100 | 101 | export const evalDebuggerStatement = (_: ESTree.DebuggerStatement, __: JSXContext) => { 102 | // eslint-disable-next-line no-debugger 103 | debugger; 104 | }; 105 | 106 | export const evalDoWhileStatement = (stmt: ESTree.DoWhileStatement, context: JSXContext) => { 107 | const label = context.label; 108 | 109 | do { 110 | try { 111 | evalStatement(stmt.body, context); 112 | } catch (err) { 113 | if (err instanceof JSXBreak) { 114 | if (err.isLabeled) { 115 | if (err.label === label) { 116 | break; 117 | } else { 118 | throw err; 119 | } 120 | } else { 121 | break; 122 | } 123 | } else if (err instanceof JSXContinue) { 124 | if (err.isLabeled) { 125 | if (err.label === label) { 126 | continue; 127 | } else { 128 | throw err; 129 | } 130 | } else { 131 | continue; 132 | } 133 | } 134 | throw err; 135 | } 136 | } while (evalExpression(stmt.test, context)); 137 | }; 138 | 139 | export const evalEmptyStatement = (_: ESTree.EmptyStatement, __: JSXContext) => {}; 140 | 141 | export const evalExportAllDeclaration = (stmt: ESTree.ExportAllDeclaration, context: JSXContext) => { 142 | throw new JSXEvaluateError('export all is not supported', stmt, context); 143 | }; 144 | 145 | export const evalExportDefaultDeclaration = (stmt: ESTree.ExportDefaultDeclaration, context: JSXContext) => { 146 | const value = (() => { 147 | switch (stmt.declaration.type) { 148 | case 'FunctionDeclaration': 149 | return evalFunctionDeclaration(stmt.declaration, context); 150 | case 'VariableDeclaration': 151 | return evalVariableDeclaration(stmt.declaration, context); 152 | default: 153 | return evalExpression(stmt.declaration, context); 154 | } 155 | })(); 156 | context.export('default', value); 157 | }; 158 | 159 | export const evalExportNamedDeclaration = (stmt: ESTree.ExportNamedDeclaration, context: JSXContext) => { 160 | stmt.specifiers.map((specifier) => { 161 | context.export(specifier.exported.name, evalExpression(specifier.local, context)); 162 | }); 163 | 164 | if (!stmt.declaration) return undefined; 165 | 166 | switch (stmt.declaration.type) { 167 | case 'FunctionDeclaration': { 168 | const [bind, func] = evalFunctionDeclaration(stmt.declaration, context); 169 | if (bind) { 170 | context.export(bind.name, func); 171 | } 172 | break; 173 | } 174 | case 'VariableDeclaration': { 175 | const binds = evalVariableDeclaration(stmt.declaration, context); 176 | const exportBind = (bind: Binding) => { 177 | switch (bind.type) { 178 | case 'Identifier': 179 | return context.export(bind.name, evalExpression(bind, context)); 180 | case 'Object': 181 | return Object.values(bind.binds).map((b) => exportBind(b)); 182 | case 'Array': 183 | return bind.binds.map((bind) => bind && exportBind(bind)); 184 | } 185 | }; 186 | return binds.forEach((bind) => exportBind(bind)); 187 | } 188 | default: 189 | return evalExpression(stmt.declaration, context); 190 | } 191 | }; 192 | 193 | export const evalExpressionStatement = (stmt: ESTree.ExpressionStatement, context: JSXContext) => { 194 | evalExpression(stmt.expression, context); 195 | }; 196 | 197 | export const evalForInStatement = (stmt: ESTree.ForInStatement, context: JSXContext) => { 198 | const label = context.label; 199 | const right = evalExpression(stmt.right, context); 200 | 201 | context.pushStack(context.resolveThis()); 202 | for (const iter in right) { 203 | context.popStack(); 204 | context.pushStack(context.resolveThis()); 205 | 206 | switch (stmt.left.type) { 207 | case 'VariableDeclaration': { 208 | const [bind] = stmt.left.declarations.map((dec) => evalBindingPattern(dec.id, context)); 209 | if (bind) { 210 | setBinding(bind, iter, context, stmt.left.kind); 211 | } 212 | break; 213 | } 214 | default: 215 | evalExpression(stmt.left, context); 216 | } 217 | 218 | try { 219 | evalStatement(stmt.body, context); 220 | } catch (err) { 221 | if (err instanceof JSXBreak) { 222 | if (err.isLabeled) { 223 | if (err.label === label) { 224 | break; 225 | } else { 226 | throw err; 227 | } 228 | } else { 229 | break; 230 | } 231 | } else if (err instanceof JSXContinue) { 232 | if (err.isLabeled) { 233 | if (err.label === label) { 234 | continue; 235 | } else { 236 | throw err; 237 | } 238 | } else { 239 | continue; 240 | } 241 | } 242 | throw err; 243 | } 244 | } 245 | context.popStack(); 246 | }; 247 | 248 | export const evalForOfStatement = (stmt: ESTree.ForOfStatement, context: JSXContext) => { 249 | const label = context.label; 250 | const right = evalExpression(stmt.right, context); 251 | 252 | context.pushStack(context.resolveThis()); 253 | for (const iter of right) { 254 | context.popStack(); 255 | context.pushStack(context.resolveThis()); 256 | 257 | switch (stmt.left.type) { 258 | case 'VariableDeclaration': { 259 | const [bind] = stmt.left.declarations.map((dec) => evalBindingPattern(dec.id, context)); 260 | if (bind) { 261 | setBinding(bind, iter, context, stmt.left.kind); 262 | } 263 | break; 264 | } 265 | default: 266 | evalExpression(stmt.left, context); 267 | } 268 | 269 | try { 270 | evalStatement(stmt.body, context); 271 | } catch (err) { 272 | if (err instanceof JSXBreak) { 273 | if (err.isLabeled) { 274 | if (err.label === label) { 275 | break; 276 | } else { 277 | throw err; 278 | } 279 | } else { 280 | break; 281 | } 282 | } else if (err instanceof JSXContinue) { 283 | if (err.isLabeled) { 284 | if (err.label === label) { 285 | continue; 286 | } else { 287 | throw err; 288 | } 289 | } else { 290 | continue; 291 | } 292 | } 293 | throw err; 294 | } 295 | } 296 | context.popStack(); 297 | }; 298 | 299 | export const evalForStatement = (stmt: ESTree.ForStatement, context: JSXContext) => { 300 | const label = context.label; 301 | context.pushStack(context.resolveThis()); 302 | const init = () => { 303 | if (stmt.init) { 304 | switch (stmt.init.type) { 305 | case 'VariableDeclaration': 306 | evalVariableDeclaration(stmt.init, context); 307 | break; 308 | default: 309 | evalExpression(stmt.init, context); 310 | } 311 | } 312 | }; 313 | const test = () => { 314 | return stmt.test ? evalExpression(stmt.test, context) : true; 315 | }; 316 | const update = () => { 317 | stmt.update && evalExpression(stmt.update, context); 318 | }; 319 | 320 | for (init(); test(); update()) { 321 | try { 322 | evalStatement(stmt.body, context); 323 | } catch (err) { 324 | if (err instanceof JSXBreak) { 325 | if (err.isLabeled) { 326 | if (err.label === label) { 327 | break; 328 | } else { 329 | throw err; 330 | } 331 | } else { 332 | break; 333 | } 334 | } else if (err instanceof JSXContinue) { 335 | if (err.isLabeled) { 336 | if (err.label === label) { 337 | continue; 338 | } else { 339 | throw err; 340 | } 341 | } else { 342 | continue; 343 | } 344 | } 345 | throw err; 346 | } 347 | } 348 | context.popStack(); 349 | }; 350 | 351 | export const evalFunctionDeclaration = (stmt: ESTree.FunctionDeclaration, context: JSXContext) => { 352 | return evalFunction(stmt, context); 353 | }; 354 | 355 | export const evalIfStatement = (stmt: ESTree.IfStatement, context: JSXContext) => { 356 | if (evalExpression(stmt.test, context)) { 357 | evalStatement(stmt.consequent, context); 358 | } else { 359 | stmt.alternate && evalStatement(stmt.alternate, context); 360 | } 361 | }; 362 | 363 | export const evalImportDeclaration = (stmt: ESTree.ImportDeclaration, context: JSXContext) => { 364 | throw new JSXEvaluateError('import is not supported', stmt, context); 365 | }; 366 | 367 | export const evalLabeledStatement = (stmt: ESTree.LabeledStatement, context: JSXContext) => { 368 | context.label = stmt.label.name; 369 | evalStatement(stmt.body, context); 370 | context.label = undefined; 371 | }; 372 | 373 | export const evalReturnStatement = (stmt: ESTree.ReturnStatement, context: JSXContext) => { 374 | const val = stmt.argument ? evalExpression(stmt.argument, context) : undefined; 375 | throw new JSXReturn(val); 376 | }; 377 | 378 | export const evalSwitchStatement = (stmt: ESTree.SwitchStatement, context: JSXContext) => { 379 | const label = context.label; 380 | const discriminant = evalExpression(stmt.discriminant, context); 381 | let match = false; 382 | for (const caseStmt of stmt.cases) { 383 | try { 384 | match = match || (caseStmt.test ? evalExpression(caseStmt.test, context) === discriminant : true); 385 | if (match) { 386 | caseStmt.consequent.forEach((stmt) => evalStatement(stmt, context)), context; 387 | } 388 | } catch (err) { 389 | if (err instanceof JSXBreak) { 390 | if (err.isLabeled) { 391 | if (err.label === label) { 392 | break; 393 | } else { 394 | throw err; 395 | } 396 | } else { 397 | break; 398 | } 399 | } 400 | throw err; 401 | } 402 | } 403 | }; 404 | 405 | export const evalThrowStatement = (stmt: ESTree.ThrowStatement, context: JSXContext) => { 406 | throw evalExpression(stmt.argument, context); 407 | }; 408 | 409 | export const evalTryStatement = (stmt: ESTree.TryStatement, context: JSXContext) => { 410 | try { 411 | evalStatement(stmt.block, context); 412 | } catch (error) { 413 | if (stmt.handler) { 414 | context.pushStack(context.resolveThis()); 415 | if (stmt.handler.param) { 416 | const binding = evalBindingPattern(stmt.handler.param, context); 417 | setBinding(binding, error, context, 'let'); 418 | } 419 | evalStatement(stmt.handler.body, context); 420 | context.popStack(); 421 | } else { 422 | throw error; 423 | } 424 | } finally { 425 | stmt.finalizer && evalStatement(stmt.finalizer, context); 426 | } 427 | }; 428 | 429 | export const evalVariableDeclaration = (stmt: ESTree.VariableDeclaration, context: JSXContext) => { 430 | const { kind } = stmt; 431 | 432 | return stmt.declarations.map((declaration) => { 433 | const binding = evalBindingPattern(declaration.id, context); 434 | setBinding(binding, declaration.init ? evalExpression(declaration.init, context) : undefined, context, kind); 435 | return binding; 436 | }); 437 | }; 438 | 439 | export const evalWhileStatement = (stmt: ESTree.WhileStatement, context: JSXContext) => { 440 | const label = context.label; 441 | 442 | while (evalExpression(stmt.test, context)) { 443 | try { 444 | evalStatement(stmt.body, context); 445 | } catch (err) { 446 | if (err instanceof JSXBreak) { 447 | if (err.isLabeled) { 448 | if (err.label === label) { 449 | break; 450 | } else { 451 | throw err; 452 | } 453 | } else { 454 | break; 455 | } 456 | } else if (err instanceof JSXContinue) { 457 | if (err.isLabeled) { 458 | if (err.label === label) { 459 | continue; 460 | } else { 461 | throw err; 462 | } 463 | } else { 464 | continue; 465 | } 466 | } 467 | throw err; 468 | } 469 | } 470 | }; 471 | 472 | export const evalWithStatement = (stmt: ESTree.WithStatement, context: JSXContext) => { 473 | throw new JSXEvaluateError('with is not supported', stmt, context); 474 | }; 475 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './types'; 2 | export * from './evaluate'; 3 | export * from './renderer'; 4 | -------------------------------------------------------------------------------- /src/renderer/__snapshots__/renderer.test.tsx.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`JSXRenderer should call Fallback Component on parse error 1`] = `"[1:3] notFound is not a function"`; 4 | 5 | exports[`JSXRenderer should call console.error on debug mode 1`] = `"[1:9] foo is not a function"`; 6 | 7 | exports[`JSXRenderer should call console.group on debug mode 1`] = ` 8 |

    9 | 1 10 |

    11 | `; 12 | 13 | exports[`JSXRenderer should catch error of syntax 1`] = `"[1:11]: Unexpected token"`; 14 | 15 | exports[`JSXRenderer should catch error with number object 1`] = `"[1:6] 1"`; 16 | 17 | exports[`JSXRenderer should catch error with object 1`] = `"[1:6] [object Object]"`; 18 | 19 | exports[`JSXRenderer should override with context 1`] = `"by context"`; 20 | 21 | exports[`JSXRenderer should render JSX 1`] = ` 22 | Array [ 23 | " 24 | ", 25 |

    26 | Hello, JSX World 27 |

    , 28 | " 29 | ", 30 |

    31 | This is paragraph. 32 |

    , 33 | " 34 | ", 35 |
      38 | 39 | 40 |
    • 41 | first 42 |
    • 43 | 44 | 45 |
    • 46 | second 47 |
    • 48 | 49 | 50 |
    , 51 | " 52 | ", 53 | "Fragment", 54 | ] 55 | `; 56 | 57 | exports[`JSXRenderer should render empty on non function component 1`] = `null`; 58 | 59 | exports[`JSXRenderer should render with component name 1`] = ` 60 | Array [ 61 | "Hello, World: ", 62 | "by context", 63 | " / ", 64 | "other function", 65 | ] 66 | `; 67 | 68 | exports[`JSXRenderer should render with custom component 1`] = ` 69 | Array [ 70 | " 71 | ", 72 | " 73 | ", 74 | ] 75 | `; 76 | -------------------------------------------------------------------------------- /src/renderer/filter.ts: -------------------------------------------------------------------------------- 1 | import { JSXElement, JSXFragment, JSXText } from '../types'; 2 | 3 | export type JSXElementFilter = (node: JSXElement) => JSXElement | undefined; 4 | export type JSXFragmentFilter = (node: JSXFragment) => JSXFragment | undefined; 5 | export type JSXTextFilter = (node: JSXText) => JSXText | undefined; 6 | -------------------------------------------------------------------------------- /src/renderer/index.ts: -------------------------------------------------------------------------------- 1 | export * from './options'; 2 | export * from './filter'; 3 | export * from './renderer'; 4 | -------------------------------------------------------------------------------- /src/renderer/isUnknownElementTagName.tsx: -------------------------------------------------------------------------------- 1 | export type UnknownHTMLElementTagNameFunction = (tagName: string) => boolean; 2 | 3 | const unknownHTMLElementCache: Record = {}; 4 | export const isUnknownHTMLElementTagName: UnknownHTMLElementTagNameFunction = (tagName) => { 5 | const cache = unknownHTMLElementCache[tagName]; 6 | if (cache !== undefined) return cache; 7 | 8 | unknownHTMLElementCache[tagName] = global.document.createElement(tagName) instanceof global.window.HTMLUnknownElement; 9 | return !!unknownHTMLElementCache[tagName]; 10 | }; 11 | -------------------------------------------------------------------------------- /src/renderer/options.ts: -------------------------------------------------------------------------------- 1 | import { Options } from '../types'; 2 | import { JSXElementFilter, JSXFragmentFilter, JSXTextFilter } from './filter'; 3 | import { UnknownHTMLElementTagNameFunction } from './isUnknownElementTagName'; 4 | 5 | export interface RenderingOptions extends Options { 6 | /** 7 | * List of filters to be applied to elements. 8 | */ 9 | elementFilters?: JSXElementFilter[]; 10 | 11 | /** 12 | * List of filters to be applied to fragments. 13 | */ 14 | fragmentFilters?: JSXFragmentFilter[]; 15 | 16 | /** 17 | * List of filters to be applied to text nodes. 18 | */ 19 | textFilters?: JSXTextFilter[]; 20 | 21 | /** 22 | * When this option is enabled, non-existent HTML elements will not be rendered. 23 | */ 24 | disableUnknownHTMLElement?: boolean; 25 | 26 | /** 27 | * Function to determine Unknown HTML Element 28 | */ 29 | isUnknownHTMLElementTagName?: UnknownHTMLElementTagNameFunction; 30 | } 31 | -------------------------------------------------------------------------------- /src/renderer/render.ts: -------------------------------------------------------------------------------- 1 | import { ESTree } from 'meriyah'; 2 | import { createElement, Fragment, ReactNode } from 'react'; 3 | import { AnyFunction } from '..'; 4 | import { JSXElement, JSXFragment, JSXNode, JSXText } from '../types'; 5 | import { isUnknownHTMLElementTagName } from './isUnknownElementTagName'; 6 | import { RenderingOptions } from './options'; 7 | 8 | const fileName = 'jsx'; 9 | 10 | export const renderJSX = (node: JSXNode | JSXNode[], options: RenderingOptions): ReactNode | ReactNode[] => { 11 | if (node === null) return node; 12 | if (node === undefined) return node; 13 | if (Array.isArray(node)) return node.map((n) => renderJSX(n, options)); 14 | 15 | switch (typeof node) { 16 | case 'boolean': 17 | return node; 18 | case 'string': 19 | case 'number': 20 | return renderJSXText(node, options); 21 | default: 22 | return renderJSXNode(node, options); 23 | } 24 | }; 25 | 26 | const renderJSXText = (text: JSXText, options: RenderingOptions): ReactNode => { 27 | return applyFilter(options.textFilters || [], text); 28 | }; 29 | 30 | const renderJSXNode = (node: JSXElement | JSXFragment, options: RenderingOptions): ReactNode => { 31 | switch (node.type) { 32 | case 'element': 33 | return renderJSXElement(node, options); 34 | case 'fragment': 35 | return renderJSXFragment(node, options); 36 | } 37 | }; 38 | 39 | const renderJSXElement = (element: JSXElement, options: RenderingOptions): ReactNode => { 40 | const filtered = applyFilter(options.elementFilters || [], element); 41 | if (!filtered) return undefined; 42 | 43 | if (options.disableUnknownHTMLElement && typeof filtered.component === 'string') { 44 | const { component } = filtered; 45 | const checker = options.isUnknownHTMLElementTagName || isUnknownHTMLElementTagName; 46 | if (checker(component)) return undefined; 47 | } 48 | 49 | const component = filtered.component as AnyFunction; 50 | if (typeof component === 'function') { 51 | const props = { ...filtered.props }; 52 | let children = component(props); 53 | if (!Array.isArray(children)) children = [children]; 54 | return createElement(Fragment, { ...renderSourcePosition(element.loc, options) }, ...children.map((child) => renderJSX(child, options))); 55 | } 56 | 57 | return createElement( 58 | filtered.component, 59 | { 60 | ...filtered.props, 61 | ...renderSourcePosition(element.loc, options), 62 | }, 63 | ...filtered.children.map((child) => renderJSX(child, options)), 64 | ); 65 | }; 66 | 67 | const renderJSXFragment = (fragment: JSXFragment, options: RenderingOptions): ReactNode => { 68 | const filtered = applyFilter(options.fragmentFilters || [], fragment); 69 | 70 | if (filtered) { 71 | return createElement( 72 | Fragment, 73 | { 74 | ...filtered.props, 75 | ...renderSourcePosition(fragment.loc, options), 76 | }, 77 | ...filtered.children.map((child) => renderJSX(child, options)), 78 | ); 79 | } else { 80 | return undefined; 81 | } 82 | }; 83 | 84 | const applyFilter = (filters: ((target: T) => T | undefined)[], node: T): T | undefined => { 85 | return filters.reduce((prev, filter) => (prev ? filter(prev) : undefined), node); 86 | }; 87 | 88 | type SourcePosition = { 89 | __source: { 90 | fileName: string; 91 | lineNumber?: number; 92 | columnNumber?: number; 93 | }; 94 | }; 95 | 96 | const renderSourcePosition = (loc: ESTree.Position | undefined, _options: RenderingOptions): SourcePosition | Record => { 97 | return loc ? { __source: { fileName, lineNumber: loc.line, columnNumber: loc.column } } : { __source: { fileName } }; 98 | }; 99 | -------------------------------------------------------------------------------- /src/renderer/renderer.test.tsx: -------------------------------------------------------------------------------- 1 | import React, { FC, ReactElement } from 'react'; 2 | import renderer from 'react-test-renderer'; 3 | import { JSXRenderer } from '.'; 4 | import { JSXElementFilter, JSXFragmentFilter, JSXTextFilter } from './filter'; 5 | import { JSXFallbackComponent, JSXRendererOptionsProvider } from './renderer'; 6 | import { JSDOM } from 'jsdom'; 7 | 8 | describe('JSXRenderer', () => { 9 | if (global.document === undefined) { 10 | // Node environment only 11 | const { window } = new JSDOM(); 12 | global.window = window as any; 13 | global.document = window.document; 14 | } 15 | 16 | const mockConsoleError = jest.spyOn(global.console, 'error').mockImplementation(); 17 | const mockConsoleGroup = jest.spyOn(global.console, 'group').mockImplementation(); 18 | const mockConsoleGroupEnd = jest.spyOn(global.console, 'groupEnd').mockImplementation(); 19 | const mockConsoleTime = jest.spyOn(global.console, 'time').mockImplementation(); 20 | const mockConsoleTimeEnd = jest.spyOn(global.console, 'timeEnd').mockImplementation(); 21 | 22 | afterEach(() => { 23 | mockConsoleError.mockClear(); 24 | mockConsoleGroup.mockClear(); 25 | mockConsoleGroupEnd.mockClear(); 26 | mockConsoleTime.mockClear(); 27 | mockConsoleTimeEnd.mockClear(); 28 | }); 29 | 30 | it('should render JSX', () => { 31 | const tree = renderer 32 | .create( 33 | Hello, JSX World 36 |

    This is paragraph.

    37 |
      38 |
    • first
    • 39 |
    • second
    • 40 |
    41 | <>Fragment 42 | `} 43 | keyPrefix="html" 44 | />, 45 | ) 46 | .toJSON(); 47 | expect(tree).toMatchSnapshot(); 48 | }); 49 | 50 | it('should catch error of syntax', () => { 51 | const tree = renderer.create().toJSON(); 52 | expect(tree).toMatchSnapshot(); 53 | }); 54 | 55 | it('should catch error with number object', () => { 56 | const tree = renderer 57 | .create( 58 | {raise()}

    `} 61 | binding={{ 62 | raise: () => { 63 | throw 1; 64 | }, 65 | }} 66 | />, 67 | ) 68 | .toJSON(); 69 | expect(tree).toMatchSnapshot(); 70 | }); 71 | 72 | it('should catch error with object', () => { 73 | const tree = renderer 74 | .create( 75 | {raise()}

    `} 78 | binding={{ 79 | raise: () => { 80 | throw { foo: 1 }; 81 | }, 82 | }} 83 | />, 84 | ) 85 | .toJSON(); 86 | expect(tree).toMatchSnapshot(); 87 | }); 88 | 89 | const elementFilters: JSXElementFilter[] = [(e) => (e.component === 'link' ? undefined : e), (e) => (e.component === 'script' ? undefined : e)]; 90 | const fragmentFilters: JSXFragmentFilter[] = [ 91 | (fragment) => { 92 | return fragment.children[0] === 'delete' ? undefined : fragment; 93 | }, 94 | ]; 95 | const textFilters: JSXTextFilter[] = [(text) => (text === 'ban' ? undefined : text)]; 96 | const fallback: JSXFallbackComponent = ({ error }) =>
    {error.message} is raised
    ; 97 | const test = (code: string, expected: ReactElement) => { 98 | it(`should render ${code}`, () => { 99 | const actual = renderer 100 | .create( 101 | , 115 | ) 116 | .toJSON(); 117 | expect(actual).toStrictEqual(renderer.create(expected).toJSON()); 118 | }); 119 | }; 120 | 121 | test('', <>); 122 | test('

    Hoge

    ',

    Hoge

    ); 123 | test('

    {true}

    ',

    {true}

    ); 124 | test('

    {null}

    ',

    {null}

    ); 125 | test('

    {undefined}

    ',

    {undefined}

    ); 126 | test( 127 | '

    <>frag

    ', 128 |

    129 | <>frag 130 |

    , 131 | ); 132 | test('

    <>delete

    ',

    ); 133 | test('