├── .prettierignore ├── .npmrc ├── lib ├── types.js ├── types.d.ts └── index.js ├── index.js ├── index.d.ts ├── .gitignore ├── .editorconfig ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── tsconfig.json ├── license ├── package.json ├── readme.md └── test.js /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.md 3 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /lib/types.js: -------------------------------------------------------------------------------- 1 | // Note: types only. 2 | export {} 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Note: types exposed from `index.d.ts`. 2 | export {buildJsx} from './lib/index.js' 3 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export type {Options, Runtime} from './lib/types.js' 2 | export {buildJsx} from './lib/index.js' 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.d.ts.map 3 | *.d.ts 4 | *.log 5 | coverage/ 6 | node_modules/ 7 | yarn.lock 8 | !/lib/types.d.ts 9 | !/index.d.ts 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | -------------------------------------------------------------------------------- /.github/workflows/bb.yml: -------------------------------------------------------------------------------- 1 | name: bb 2 | on: 3 | issues: 4 | types: [opened, reopened, edited, closed, labeled, unlabeled] 5 | pull_request_target: 6 | types: [opened, reopened, edited, closed, labeled, unlabeled] 7 | jobs: 8 | main: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: unifiedjs/beep-boop-beta@main 12 | with: 13 | repo-token: ${{secrets.GITHUB_TOKEN}} 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declaration": true, 6 | "declarationMap": true, 7 | "emitDeclarationOnly": true, 8 | "exactOptionalPropertyTypes": true, 9 | "lib": ["es2022"], 10 | "module": "node16", 11 | "strict": true, 12 | "target": "es2022" 13 | }, 14 | "exclude": ["coverage/", "node_modules/"], 15 | "include": ["**/*.js", "lib/types.d.ts", "index.d.ts"] 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: main 2 | on: 3 | - pull_request 4 | - push 5 | jobs: 6 | main: 7 | name: ${{matrix.node}} 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v4 11 | - uses: actions/setup-node@v4 12 | with: 13 | node-version: ${{matrix.node}} 14 | - run: npm install 15 | - run: npm test 16 | - uses: codecov/codecov-action@v4 17 | strategy: 18 | matrix: 19 | node: 20 | - lts/hydrogen 21 | - node 22 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2020 Titus Wormer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "estree-util-build-jsx", 3 | "version": "3.0.1", 4 | "description": "Transform JSX in estrees to function calls (for react, preact, and most hyperscript interfaces)", 5 | "license": "MIT", 6 | "keywords": [ 7 | "estree", 8 | "ast", 9 | "ecmascript", 10 | "javascript", 11 | "tree", 12 | "jsx", 13 | "xml", 14 | "build", 15 | "hyperscript", 16 | "compile", 17 | "call", 18 | "acorn", 19 | "espree", 20 | "react", 21 | "preact" 22 | ], 23 | "repository": "syntax-tree/estree-util-build-jsx", 24 | "bugs": "https://github.com/syntax-tree/estree-util-build-jsx/issues", 25 | "funding": { 26 | "type": "opencollective", 27 | "url": "https://opencollective.com/unified" 28 | }, 29 | "author": "Titus Wormer (https://wooorm.com)", 30 | "contributors": [ 31 | "Titus Wormer (https://wooorm.com)" 32 | ], 33 | "sideEffects": false, 34 | "type": "module", 35 | "exports": "./index.js", 36 | "files": [ 37 | "lib/", 38 | "index.d.ts", 39 | "index.js" 40 | ], 41 | "dependencies": { 42 | "@types/estree-jsx": "^1.0.0", 43 | "devlop": "^1.0.0", 44 | "estree-util-is-identifier-name": "^3.0.0", 45 | "estree-walker": "^3.0.0" 46 | }, 47 | "devDependencies": { 48 | "@types/node": "^22.0.0", 49 | "acorn": "^8.0.0", 50 | "acorn-jsx": "^5.0.0", 51 | "astring": "^1.0.0", 52 | "c8": "^10.0.0", 53 | "prettier": "^3.0.0", 54 | "remark-cli": "^12.0.0", 55 | "remark-preset-wooorm": "^10.0.0", 56 | "type-coverage": "^2.0.0", 57 | "typescript": "^5.0.0", 58 | "xo": "^0.59.0" 59 | }, 60 | "scripts": { 61 | "prepack": "npm run build && npm run format", 62 | "build": "tsc --build --clean && tsc --build && type-coverage", 63 | "format": "remark . -qfo && prettier . -w --log-level warn && xo --fix", 64 | "test-api": "node --conditions development test.js", 65 | "test-coverage": "c8 --100 --reporter lcov npm run test-api", 66 | "test": "npm run build && npm run format && npm run test-coverage" 67 | }, 68 | "prettier": { 69 | "bracketSpacing": false, 70 | "semi": false, 71 | "singleQuote": true, 72 | "tabWidth": 2, 73 | "trailingComma": "none", 74 | "useTabs": false 75 | }, 76 | "remarkConfig": { 77 | "plugins": [ 78 | "remark-preset-wooorm" 79 | ] 80 | }, 81 | "typeCoverage": { 82 | "atLeast": 100, 83 | "detail": true, 84 | "ignoreCatch": true, 85 | "strict": true 86 | }, 87 | "xo": { 88 | "overrides": [ 89 | { 90 | "files": [ 91 | "**/*.d.ts" 92 | ], 93 | "rules": { 94 | "@typescript-eslint/array-type": [ 95 | "error", 96 | { 97 | "default": "generic" 98 | } 99 | ], 100 | "@typescript-eslint/ban-types": [ 101 | "error", 102 | { 103 | "extendDefaults": true 104 | } 105 | ], 106 | "@typescript-eslint/consistent-type-definitions": [ 107 | "error", 108 | "interface" 109 | ] 110 | } 111 | } 112 | ], 113 | "prettier": true, 114 | "rules": { 115 | "unicorn/prefer-string-replace-all": "off" 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /lib/types.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * How to transform JSX. 3 | */ 4 | export type Runtime = 'automatic' | 'classic' 5 | 6 | /** 7 | * Configuration. 8 | * 9 | * > 👉 **Note**: you can also configure `runtime`, `importSource`, `pragma`, 10 | * > and `pragmaFrag` from within files through comments. 11 | */ 12 | export interface Options { 13 | /** 14 | * When in the automatic runtime, whether to import 15 | * `theSource/jsx-dev-runtime.js`, use `jsxDEV`, and pass location info when 16 | * available (default: `false`). 17 | * 18 | * This helps debugging but adds a lot of code that you don’t want in 19 | * production. 20 | */ 21 | development?: boolean | null | undefined 22 | /** 23 | * File path to the original source file (optional). 24 | * 25 | * Passed in location info to `jsxDEV` when using the automatic runtime with 26 | * `development: true`. 27 | */ 28 | filePath?: string | null | undefined 29 | /** 30 | * Place to import `jsx`, `jsxs`, `jsxDEV`, and `Fragment` from, when the 31 | * effective runtime is automatic (default: `'react'`). 32 | * 33 | * Comment form: `@jsxImportSource theSource`. 34 | * 35 | * > 👉 **Note**: `/jsx-runtime` or `/jsx-dev-runtime` is appended to this 36 | * > provided source. 37 | * > In CJS, that can resolve to a file (as in `theSource/jsx-runtime.js`), 38 | * > but for ESM an export map needs to be set up to point to files: 39 | * > 40 | * > ```js 41 | * > // … 42 | * > "exports": { 43 | * > // … 44 | * > "./jsx-runtime": "./path/to/jsx-runtime.js", 45 | * > "./jsx-dev-runtime": "./path/to/jsx-runtime.js" 46 | * > // … 47 | * > ``` 48 | */ 49 | importSource?: string | null | undefined 50 | /** 51 | * Identifier or member expression to use as a symbol for fragments when the 52 | * effective runtime is classic (default: `'React.Fragment'`). 53 | * 54 | * Comment form: `@jsxFrag identifier`. 55 | */ 56 | pragmaFrag?: string | null | undefined 57 | /** 58 | * Identifier or member expression to call when the effective runtime is 59 | * classic (default: `'React.createElement'`). 60 | * 61 | * Comment form: `@jsx identifier`. 62 | */ 63 | pragma?: string | null | undefined 64 | /** 65 | * Choose the runtime (default: `'classic'`). 66 | * 67 | * Comment form: `@jsxRuntime theRuntime`. 68 | */ 69 | runtime?: Runtime | null | undefined 70 | } 71 | 72 | /** 73 | * State where info from comments is gathered. 74 | */ 75 | export interface Annotations { 76 | /** 77 | * JSX identifier of fragment (`pragmaFrag`). 78 | */ 79 | jsxFrag?: string | undefined 80 | /** 81 | * Where to import an automatic JSX runtime from. 82 | */ 83 | jsxImportSource?: string | undefined 84 | /** 85 | * Runtime. 86 | */ 87 | jsxRuntime?: Runtime | undefined 88 | /** 89 | * JSX identifier (`pragma`). 90 | */ 91 | jsx?: string | undefined 92 | } 93 | 94 | /** 95 | * State of used identifiers from the automatic runtime. 96 | */ 97 | export interface Imports { 98 | /** 99 | * Symbol of `Fragment`. 100 | */ 101 | fragment?: boolean | undefined 102 | /** 103 | * Symbol of `jsxDEV`. 104 | */ 105 | jsxDEV?: boolean | undefined 106 | /** 107 | * Symbol of `jsxs`. 108 | */ 109 | jsxs?: boolean | undefined 110 | /** 111 | * Symbol of `jsx`. 112 | */ 113 | jsx?: boolean | undefined 114 | } 115 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # estree-util-build-jsx 2 | 3 | [![Build][build-badge]][build] 4 | [![Coverage][coverage-badge]][coverage] 5 | [![Downloads][downloads-badge]][downloads] 6 | [![Size][size-badge]][size] 7 | [![Sponsors][sponsors-badge]][collective] 8 | [![Backers][backers-badge]][collective] 9 | [![Chat][chat-badge]][chat] 10 | 11 | [estree][] utility to turn JSX into function calls: `` -> `h('x')`! 12 | 13 | ## Contents 14 | 15 | * [What is this?](#what-is-this) 16 | * [When should I use this?](#when-should-i-use-this) 17 | * [Install](#install) 18 | * [Use](#use) 19 | * [API](#api) 20 | * [`buildJsx(tree[, options])`](#buildjsxtree-options) 21 | * [`Options`](#options) 22 | * [`Runtime`](#runtime-1) 23 | * [Examples](#examples) 24 | * [Example: use with Acorn](#example-use-with-acorn) 25 | * [Types](#types) 26 | * [Compatibility](#compatibility) 27 | * [Related](#related) 28 | * [Security](#security) 29 | * [Contribute](#contribute) 30 | * [License](#license) 31 | 32 | ## What is this? 33 | 34 | This package is a utility that takes an [estree][] (JavaScript) syntax tree as 35 | input that contains embedded JSX nodes (elements, fragments) and turns them into 36 | function calls. 37 | 38 | ## When should I use this? 39 | 40 | If you already have a tree and only need to compile JSX away, use this. 41 | If you have code, use something like [SWC][] or [esbuild][] instead. 42 | 43 | ## Install 44 | 45 | This package is [ESM only][esm]. 46 | In Node.js (version 16+), install with [npm][]: 47 | 48 | ```sh 49 | npm install estree-util-build-jsx 50 | ``` 51 | 52 | In Deno with [`esm.sh`][esmsh]: 53 | 54 | ```js 55 | import {buildJsx} from 'https://esm.sh/estree-util-build-jsx@3' 56 | ``` 57 | 58 | In browsers with [`esm.sh`][esmsh]: 59 | 60 | ```html 61 | 64 | ``` 65 | 66 | ## Use 67 | 68 | Say we have the following `example.jsx`: 69 | 70 | ```js 71 | import x from 'xastscript' 72 | 73 | console.log( 74 | 75 | Born in the U.S.A. 76 | Bruce Springsteen 77 | April 6, 1984 78 | 79 | ) 80 | 81 | console.log( 82 | <> 83 | {1 + 1} 84 | 85 | 86 | 87 | ) 88 | ``` 89 | 90 | …and next to it a module `example.js`: 91 | 92 | ```js 93 | import fs from 'node:fs/promises' 94 | import jsx from 'acorn-jsx' 95 | import {fromJs} from 'esast-util-from-js' 96 | import {buildJsx} from 'estree-util-build-jsx' 97 | import {toJs} from 'estree-util-to-js' 98 | 99 | const doc = String(await fs.readFile('example.jsx')) 100 | 101 | const tree = fromJs(doc, {module: true, plugins: [jsx()]}) 102 | 103 | buildJsx(tree, {pragma: 'x', pragmaFrag: 'null'}) 104 | 105 | console.log(toJs(tree).value) 106 | ``` 107 | 108 | …now running `node example.js` yields: 109 | 110 | ```js 111 | import x from "xastscript"; 112 | console.log(x("album", { 113 | id: 123 114 | }, x("name", null, "Born in the U.S.A."), x("artist", null, "Bruce Springsteen"), x("releasedate", { 115 | date: "1984-04-06" 116 | }, "April 6, 1984"))); 117 | console.log(x(null, null, 1 + 1, x("self-closing"), x("x", Object.assign({ 118 | name: true, 119 | key: "value", 120 | key: expression 121 | }, spread)))); 122 | ``` 123 | 124 | ## API 125 | 126 | This package exports the identifier [`buildJsx`][api-build-jsx]. 127 | There is no default export. 128 | 129 | ### `buildJsx(tree[, options])` 130 | 131 | Turn JSX in `tree` into function calls: `` -> `h('x')`! 132 | 133 | ###### Algorithm 134 | 135 | In almost all cases, this utility is the same as the Babel plugin, except that 136 | they work on slightly different syntax trees. 137 | 138 | Some differences: 139 | 140 | * no pure annotations things 141 | * `this` is not a component: `` -> `h('this')`, not `h(this)` 142 | * namespaces are supported: `` -> `h('a:b', {'c:d': true})`, 143 | which throws by default in Babel or can be turned on with `throwIfNamespace` 144 | * no `useSpread`, `useBuiltIns`, or `filter` options 145 | 146 | ###### Parameters 147 | 148 | * `tree` ([`Node`][node]) 149 | — tree to transform (typically [`Program`][program]) 150 | * `options` ([`Options`][api-options], optional) 151 | — configuration 152 | 153 | ###### Returns 154 | 155 | Nothing (`undefined`). 156 | 157 | ### `Options` 158 | 159 | Configuration (TypeScript type). 160 | 161 | > 👉 **Note**: you can also configure `runtime`, `importSource`, `pragma`, and 162 | > `pragmaFrag` from within files through comments. 163 | 164 | ##### Fields 165 | 166 | ###### `runtime` 167 | 168 | Choose the [runtime][jsx-runtime] ([`Runtime`][api-runtime], default: `'classic'`). 169 | 170 | Comment form: `@jsxRuntime theRuntime`. 171 | 172 | ###### `importSource` 173 | 174 | Place to import `jsx`, `jsxs`, `jsxDEV`, and `Fragment` from, when the 175 | effective runtime is automatic (`string`, default: `'react'`). 176 | 177 | Comment form: `@jsxImportSource theSource`. 178 | 179 | > 👉 **Note**: `/jsx-runtime` or `/jsx-dev-runtime` is appended to this provided 180 | > source. 181 | > In CJS, that can resolve to a file (as in `theSource/jsx-runtime.js`), but for 182 | > ESM an export map needs to be set up to point to files: 183 | > 184 | > ```js 185 | > // … 186 | > "exports": { 187 | > // … 188 | > "./jsx-runtime": "./path/to/jsx-runtime.js", 189 | > "./jsx-dev-runtime": "./path/to/jsx-runtime.js" 190 | > // … 191 | > ``` 192 | 193 | ###### `pragma` 194 | 195 | Identifier or member expression to call when the effective runtime is classic 196 | (`string`, default: `'React.createElement'`). 197 | 198 | Comment form: `@jsx identifier`. 199 | 200 | ###### `pragmaFrag` 201 | 202 | Identifier or member expression to use as a symbol for fragments when the 203 | effective runtime is classic (`string`, default: `'React.Fragment'`). 204 | 205 | Comment form: `@jsxFrag identifier`. 206 | 207 | ###### `development` 208 | 209 | When in the automatic runtime, whether to import `theSource/jsx-dev-runtime.js`, 210 | use `jsxDEV`, and pass location info when available (`boolean`, default: `false`). 211 | 212 | This helps debugging but adds a lot of code that you don’t want in production. 213 | 214 | ###### `filePath` 215 | 216 | File path to the original source file (`string`, example: `'path/to/file.js'`). 217 | Passed in location info to `jsxDEV` when using the automatic runtime with 218 | `development: true`. 219 | 220 | ### `Runtime` 221 | 222 | How to transform JSX (TypeScript type). 223 | 224 | ###### Type 225 | 226 | ```ts 227 | type Runtime = 'automatic' | 'classic' 228 | ``` 229 | 230 | ## Examples 231 | 232 | ### Example: use with Acorn 233 | 234 | To support configuration from comments in Acorn, those comments have to be in 235 | the program. 236 | This is done by [`espree`][espree] but not automatically by [`acorn`][acorn]: 237 | 238 | ```js 239 | import {Parser} from 'acorn' 240 | import jsx from 'acorn-jsx' 241 | 242 | const doc = '' // To do: get `doc` somehow. 243 | 244 | const comments = [] 245 | const tree = Parser.extend(jsx()).parse(doc, {onComment: comments}) 246 | tree.comments = comments 247 | ``` 248 | 249 | ## Types 250 | 251 | This package is fully typed with [TypeScript][]. 252 | It exports the additional type [`Options`][api-options] and 253 | [`Runtime`][api-runtime]. 254 | 255 | ## Compatibility 256 | 257 | Projects maintained by the unified collective are compatible with maintained 258 | versions of Node.js. 259 | 260 | When we cut a new major release, we drop support for unmaintained versions of 261 | Node. 262 | This means we try to keep the current release line, `estree-util-build-jsx@^3`, 263 | compatible with Node.js 16. 264 | 265 | ## Related 266 | 267 | * [`syntax-tree/hast-util-to-estree`](https://github.com/syntax-tree/hast-util-to-estree) 268 | — turn [hast](https://github.com/syntax-tree/hast) (HTML) to [estree][] 269 | JSX 270 | * [`coderaiser/estree-to-babel`](https://github.com/coderaiser/estree-to-babel) 271 | — turn [estree][] to Babel trees 272 | 273 | ## Security 274 | 275 | This package is safe. 276 | 277 | ## Contribute 278 | 279 | See [`contributing.md` in `syntax-tree/.github`][contributing] for ways to get 280 | started. 281 | See [`support.md`][support] for ways to get help. 282 | 283 | This project has a [code of conduct][coc]. 284 | By interacting with this repository, organization, or community you agree to 285 | abide by its terms. 286 | 287 | ## License 288 | 289 | [MIT][license] © [Titus Wormer][author] 290 | 291 | 292 | 293 | [build-badge]: https://github.com/syntax-tree/estree-util-build-jsx/workflows/main/badge.svg 294 | 295 | [build]: https://github.com/syntax-tree/estree-util-build-jsx/actions 296 | 297 | [coverage-badge]: https://img.shields.io/codecov/c/github/syntax-tree/estree-util-build-jsx.svg 298 | 299 | [coverage]: https://codecov.io/github/syntax-tree/estree-util-build-jsx 300 | 301 | [downloads-badge]: https://img.shields.io/npm/dm/estree-util-build-jsx.svg 302 | 303 | [downloads]: https://www.npmjs.com/package/estree-util-build-jsx 304 | 305 | [size-badge]: https://img.shields.io/badge/dynamic/json?label=minzipped%20size&query=$.size.compressedSize&url=https://deno.bundlejs.com/?q=estree-util-build-jsx 306 | 307 | [size]: https://bundlejs.com/?q=estree-util-build-jsx 308 | 309 | [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg 310 | 311 | [backers-badge]: https://opencollective.com/unified/backers/badge.svg 312 | 313 | [collective]: https://opencollective.com/unified 314 | 315 | [chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg 316 | 317 | [chat]: https://github.com/syntax-tree/unist/discussions 318 | 319 | [esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 320 | 321 | [npm]: https://docs.npmjs.com/cli/install 322 | 323 | [esmsh]: https://esm.sh 324 | 325 | [license]: license 326 | 327 | [author]: https://wooorm.com 328 | 329 | [typescript]: https://www.typescriptlang.org 330 | 331 | [contributing]: https://github.com/syntax-tree/.github/blob/main/contributing.md 332 | 333 | [support]: https://github.com/syntax-tree/.github/blob/main/support.md 334 | 335 | [coc]: https://github.com/syntax-tree/.github/blob/main/code-of-conduct.md 336 | 337 | [acorn]: https://github.com/acornjs/acorn 338 | 339 | [estree]: https://github.com/estree/estree 340 | 341 | [espree]: https://github.com/eslint/espree 342 | 343 | [node]: https://github.com/estree/estree/blob/master/es5.md#node-objects 344 | 345 | [program]: https://github.com/estree/estree/blob/master/es5.md#programs 346 | 347 | [jsx-runtime]: https://reactjs.org/blog/2020/09/22/introducing-the-new-jsx-transform.html 348 | 349 | [swc]: https://swc.rs 350 | 351 | [esbuild]: https://esbuild.github.io 352 | 353 | [api-build-jsx]: #buildjsxtree-options 354 | 355 | [api-options]: #options 356 | 357 | [api-runtime]: #runtime-1 358 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import { 3 | * Expression, 4 | * Identifier, 5 | * ImportSpecifier, 6 | * JSXAttribute, 7 | * JSXIdentifier, 8 | * JSXMemberExpression, 9 | * JSXNamespacedName, 10 | * Literal, 11 | * MemberExpression, 12 | * Node, 13 | * ObjectExpression, 14 | * Property, 15 | * SpreadElement 16 | * } from 'estree-jsx' 17 | * @import {Annotations, Imports, Options} from './types.js' 18 | */ 19 | 20 | import {ok as assert} from 'devlop' 21 | import {name as isIdentifierName} from 'estree-util-is-identifier-name' 22 | import {walk} from 'estree-walker' 23 | 24 | const regex = /@(jsx|jsxFrag|jsxImportSource|jsxRuntime)\s+(\S+)/g 25 | 26 | /** 27 | * Turn JSX in `tree` into function calls: `` -> `h('x')`! 28 | * 29 | * ###### Algorithm 30 | * 31 | * In almost all cases, this utility is the same as the Babel plugin, except that 32 | * they work on slightly different syntax trees. 33 | * 34 | * Some differences: 35 | * 36 | * * no pure annotations things 37 | * * `this` is not a component: `` -> `h('this')`, not `h(this)` 38 | * * namespaces are supported: `` -> `h('a:b', {'c:d': true})`, 39 | * which throws by default in Babel or can be turned on with `throwIfNamespace` 40 | * * no `useSpread`, `useBuiltIns`, or `filter` options 41 | * 42 | * @param {Node} tree 43 | * Tree to transform (typically `Program`). 44 | * @param {Options | null | undefined} [options] 45 | * Configuration (optional). 46 | * @returns {undefined} 47 | * Nothing. 48 | */ 49 | export function buildJsx(tree, options) { 50 | const config = options || {} 51 | let automatic = config.runtime === 'automatic' 52 | /** @type {Annotations} */ 53 | const annotations = {} 54 | /** @type {Imports} */ 55 | const imports = {} 56 | 57 | walk(tree, { 58 | enter(node) { 59 | if (node.type === 'Program') { 60 | const comments = node.comments || [] 61 | let index = -1 62 | 63 | while (++index < comments.length) { 64 | regex.lastIndex = 0 65 | 66 | let match = regex.exec(comments[index].value) 67 | 68 | while (match) { 69 | // @ts-expect-error: `match[1]` is always a key, `match[2]` when 70 | // runtime is checked later. 71 | annotations[match[1]] = match[2] 72 | match = regex.exec(comments[index].value) 73 | } 74 | } 75 | 76 | if (annotations.jsxRuntime) { 77 | if (annotations.jsxRuntime === 'automatic') { 78 | automatic = true 79 | 80 | if (annotations.jsx) { 81 | throw new Error('Unexpected `@jsx` pragma w/ automatic runtime') 82 | } 83 | 84 | if (annotations.jsxFrag) { 85 | throw new Error( 86 | 'Unexpected `@jsxFrag` pragma w/ automatic runtime' 87 | ) 88 | } 89 | } else if (annotations.jsxRuntime === 'classic') { 90 | automatic = false 91 | 92 | if (annotations.jsxImportSource) { 93 | throw new Error( 94 | 'Unexpected `@jsxImportSource` w/ classic runtime' 95 | ) 96 | } 97 | } else { 98 | throw new Error( 99 | 'Unexpected `jsxRuntime` `' + 100 | annotations.jsxRuntime + 101 | '`, expected `automatic` or `classic`' 102 | ) 103 | } 104 | } 105 | } 106 | }, 107 | // eslint-disable-next-line complexity 108 | leave(node) { 109 | if (node.type === 'Program') { 110 | /** @type {Array} */ 111 | const specifiers = [] 112 | 113 | if (imports.fragment) { 114 | specifiers.push({ 115 | type: 'ImportSpecifier', 116 | imported: {type: 'Identifier', name: 'Fragment'}, 117 | local: {type: 'Identifier', name: '_Fragment'} 118 | }) 119 | } 120 | 121 | if (imports.jsx) { 122 | specifiers.push({ 123 | type: 'ImportSpecifier', 124 | imported: {type: 'Identifier', name: 'jsx'}, 125 | local: {type: 'Identifier', name: '_jsx'} 126 | }) 127 | } 128 | 129 | if (imports.jsxs) { 130 | specifiers.push({ 131 | type: 'ImportSpecifier', 132 | imported: {type: 'Identifier', name: 'jsxs'}, 133 | local: {type: 'Identifier', name: '_jsxs'} 134 | }) 135 | } 136 | 137 | if (imports.jsxDEV) { 138 | specifiers.push({ 139 | type: 'ImportSpecifier', 140 | imported: {type: 'Identifier', name: 'jsxDEV'}, 141 | local: {type: 'Identifier', name: '_jsxDEV'} 142 | }) 143 | } 144 | 145 | if (specifiers.length > 0) { 146 | let injectIndex = 0 147 | 148 | while (injectIndex < node.body.length) { 149 | const child = node.body[injectIndex] 150 | 151 | if ('directive' in child && child.directive) { 152 | injectIndex++ 153 | } else { 154 | break 155 | } 156 | } 157 | 158 | node.body.splice(injectIndex, 0, { 159 | type: 'ImportDeclaration', 160 | specifiers, 161 | source: { 162 | type: 'Literal', 163 | value: 164 | (annotations.jsxImportSource || 165 | config.importSource || 166 | 'react') + 167 | (config.development ? '/jsx-dev-runtime' : '/jsx-runtime') 168 | } 169 | }) 170 | } 171 | } 172 | 173 | if (node.type !== 'JSXElement' && node.type !== 'JSXFragment') { 174 | return 175 | } 176 | 177 | /** @type {Array} */ 178 | const children = [] 179 | let index = -1 180 | 181 | // Figure out `children`. 182 | while (++index < node.children.length) { 183 | const child = node.children[index] 184 | 185 | if (child.type === 'JSXExpressionContainer') { 186 | // Ignore empty expressions. 187 | if (child.expression.type !== 'JSXEmptyExpression') { 188 | children.push(child.expression) 189 | } 190 | } else if (child.type === 'JSXText') { 191 | const value = child.value 192 | // Replace tabs w/ spaces. 193 | .replace(/\t/g, ' ') 194 | // Use line feeds, drop spaces around them. 195 | .replace(/ *(\r?\n|\r) */g, '\n') 196 | // Collapse multiple line feeds. 197 | .replace(/\n+/g, '\n') 198 | // Drop final line feeds. 199 | .replace(/\n+$/, '') 200 | // Drop first line feeds. 201 | .replace(/^\n+/, '') 202 | // Replace line feeds with spaces. 203 | .replace(/\n/g, ' ') 204 | 205 | // Ignore collapsible text. 206 | if (value) { 207 | /** @type {Node} */ 208 | const text = {type: 'Literal', value} 209 | create(child, text) 210 | children.push(text) 211 | } 212 | } else { 213 | assert( 214 | child.type !== 'JSXElement' && 215 | child.type !== 'JSXFragment' && 216 | child.type !== 'JSXSpreadChild' 217 | ) 218 | children.push(child) 219 | } 220 | } 221 | 222 | /** @type {Identifier | Literal | MemberExpression} */ 223 | let name 224 | /** @type {Array} */ 225 | const fields = [] 226 | /** @type {Array} */ 227 | let parameters = [] 228 | /** @type {Expression | undefined} */ 229 | let key 230 | 231 | // Do the stuff needed for elements. 232 | if (node.type === 'JSXElement') { 233 | name = toIdentifier(node.openingElement.name) 234 | 235 | // If the name could be an identifier, but start with a lowercase letter, 236 | // it’s not a component. 237 | if (name.type === 'Identifier' && /^[a-z]/.test(name.name)) { 238 | /** @type {Node} */ 239 | const next = {type: 'Literal', value: name.name} 240 | create(name, next) 241 | name = next 242 | } 243 | 244 | /** @type {boolean | undefined} */ 245 | let spread 246 | const attributes = node.openingElement.attributes 247 | let index = -1 248 | 249 | // Place props in the right order, because we might have duplicates 250 | // in them and what’s spread in. 251 | while (++index < attributes.length) { 252 | const attribute = attributes[index] 253 | 254 | if (attribute.type === 'JSXSpreadAttribute') { 255 | if (attribute.argument.type === 'ObjectExpression') { 256 | fields.push(...attribute.argument.properties) 257 | } else { 258 | fields.push({type: 'SpreadElement', argument: attribute.argument}) 259 | } 260 | 261 | spread = true 262 | } else { 263 | const property = toProperty(attribute) 264 | 265 | if ( 266 | automatic && 267 | property.key.type === 'Identifier' && 268 | property.key.name === 'key' 269 | ) { 270 | if (spread) { 271 | throw new Error( 272 | 'Expected `key` to come before any spread expressions' 273 | ) 274 | } 275 | 276 | const value = property.value 277 | 278 | assert( 279 | value.type !== 'AssignmentPattern' && 280 | value.type !== 'ArrayPattern' && 281 | value.type !== 'ObjectPattern' && 282 | value.type !== 'RestElement' 283 | ) 284 | 285 | key = value 286 | } else { 287 | fields.push(property) 288 | } 289 | } 290 | } 291 | } 292 | // …and fragments. 293 | else if (automatic) { 294 | imports.fragment = true 295 | name = {type: 'Identifier', name: '_Fragment'} 296 | } else { 297 | name = toMemberExpression( 298 | annotations.jsxFrag || config.pragmaFrag || 'React.Fragment' 299 | ) 300 | } 301 | 302 | if (automatic) { 303 | if (children.length > 0) { 304 | fields.push({ 305 | type: 'Property', 306 | key: {type: 'Identifier', name: 'children'}, 307 | value: 308 | children.length > 1 309 | ? {type: 'ArrayExpression', elements: children} 310 | : children[0], 311 | kind: 'init', 312 | method: false, 313 | shorthand: false, 314 | computed: false 315 | }) 316 | } 317 | } else { 318 | parameters = children 319 | } 320 | 321 | /** @type {Identifier | Literal | MemberExpression} */ 322 | let callee 323 | 324 | if (automatic) { 325 | parameters.push({type: 'ObjectExpression', properties: fields}) 326 | 327 | if (key) { 328 | parameters.push(key) 329 | } else if (config.development) { 330 | parameters.push({type: 'Identifier', name: 'undefined'}) 331 | } 332 | 333 | const isStaticChildren = children.length > 1 334 | 335 | if (config.development) { 336 | imports.jsxDEV = true 337 | callee = { 338 | type: 'Identifier', 339 | name: '_jsxDEV' 340 | } 341 | parameters.push({type: 'Literal', value: isStaticChildren}) 342 | 343 | /** @type {ObjectExpression} */ 344 | const source = { 345 | type: 'ObjectExpression', 346 | properties: [ 347 | { 348 | type: 'Property', 349 | method: false, 350 | shorthand: false, 351 | computed: false, 352 | kind: 'init', 353 | key: {type: 'Identifier', name: 'fileName'}, 354 | value: { 355 | type: 'Literal', 356 | value: config.filePath || '' 357 | } 358 | } 359 | ] 360 | } 361 | 362 | if (node.loc) { 363 | source.properties.push( 364 | { 365 | type: 'Property', 366 | method: false, 367 | shorthand: false, 368 | computed: false, 369 | kind: 'init', 370 | key: {type: 'Identifier', name: 'lineNumber'}, 371 | value: {type: 'Literal', value: node.loc.start.line} 372 | }, 373 | { 374 | type: 'Property', 375 | method: false, 376 | shorthand: false, 377 | computed: false, 378 | kind: 'init', 379 | key: {type: 'Identifier', name: 'columnNumber'}, 380 | value: {type: 'Literal', value: node.loc.start.column + 1} 381 | } 382 | ) 383 | } 384 | 385 | parameters.push(source, {type: 'ThisExpression'}) 386 | } else if (isStaticChildren) { 387 | imports.jsxs = true 388 | callee = {type: 'Identifier', name: '_jsxs'} 389 | } else { 390 | imports.jsx = true 391 | callee = {type: 'Identifier', name: '_jsx'} 392 | } 393 | } 394 | // Classic. 395 | else { 396 | if (fields.length > 0) { 397 | parameters.unshift({type: 'ObjectExpression', properties: fields}) 398 | } else if (parameters.length > 0) { 399 | parameters.unshift({type: 'Literal', value: null}) 400 | } 401 | 402 | callee = toMemberExpression( 403 | annotations.jsx || config.pragma || 'React.createElement' 404 | ) 405 | } 406 | 407 | parameters.unshift(name) 408 | /** @type {Node} */ 409 | const call = { 410 | type: 'CallExpression', 411 | callee, 412 | arguments: parameters, 413 | optional: false 414 | } 415 | create(node, call) 416 | this.replace(call) 417 | } 418 | }) 419 | } 420 | 421 | /** 422 | * Turn a JSX attribute into a JavaScript property. 423 | * 424 | * @param {JSXAttribute} node 425 | * JSX attribute. 426 | * @returns {Property} 427 | * JS property. 428 | */ 429 | function toProperty(node) { 430 | /** @type {Expression} */ 431 | let value 432 | 433 | if (node.value) { 434 | if (node.value.type === 'JSXExpressionContainer') { 435 | const valueExpression = node.value.expression 436 | assert( 437 | valueExpression.type !== 'JSXEmptyExpression', 438 | '`JSXEmptyExpression` is not allowed in props.' 439 | ) 440 | value = valueExpression 441 | } 442 | // Literal or call expression. 443 | else { 444 | const nodeValue = node.value 445 | assert( 446 | nodeValue.type !== 'JSXElement' && nodeValue.type !== 'JSXFragment', 447 | 'JSX{Element,Fragment} are already compiled to `CallExpression`' 448 | ) 449 | value = nodeValue 450 | delete value.raw 451 | } 452 | } 453 | // Boolean property. 454 | else { 455 | value = {type: 'Literal', value: true} 456 | } 457 | 458 | /** @type {Property} */ 459 | const replacement = { 460 | type: 'Property', 461 | key: toIdentifier(node.name), 462 | value, 463 | kind: 'init', 464 | method: false, 465 | shorthand: false, 466 | computed: false 467 | } 468 | create(node, replacement) 469 | return replacement 470 | } 471 | 472 | /** 473 | * Turn a JSX identifier into a normal JS identifier. 474 | * 475 | * @param {JSXIdentifier | JSXMemberExpression | JSXNamespacedName} node 476 | * JSX identifier. 477 | * @returns {Identifier | Literal | MemberExpression} 478 | * JS identifier. 479 | */ 480 | function toIdentifier(node) { 481 | /** @type {Identifier | Literal | MemberExpression} */ 482 | let replace 483 | 484 | if (node.type === 'JSXMemberExpression') { 485 | // `property` is always a `JSXIdentifier`, but it could be something that 486 | // isn’t an ES identifier name. 487 | const id = toIdentifier(node.property) 488 | replace = { 489 | type: 'MemberExpression', 490 | object: toIdentifier(node.object), 491 | property: id, 492 | computed: id.type === 'Literal', 493 | optional: false 494 | } 495 | } else if (node.type === 'JSXNamespacedName') { 496 | replace = { 497 | type: 'Literal', 498 | value: node.namespace.name + ':' + node.name.name 499 | } 500 | } 501 | // Must be `JSXIdentifier`. 502 | else { 503 | replace = isIdentifierName(node.name) 504 | ? {type: 'Identifier', name: node.name} 505 | : {type: 'Literal', value: node.name} 506 | } 507 | 508 | create(node, replace) 509 | return replace 510 | } 511 | 512 | /** 513 | * Turn a dotted string into a member expression. 514 | * 515 | * @param {string} id 516 | * Identifiers. 517 | * @returns {Identifier | Literal | MemberExpression} 518 | * Expression. 519 | */ 520 | function toMemberExpression(id) { 521 | const identifiers = id.split('.') 522 | let index = -1 523 | /** @type {Identifier | Literal | MemberExpression | undefined} */ 524 | let result 525 | 526 | while (++index < identifiers.length) { 527 | /** @type {Identifier | Literal} */ 528 | const property = isIdentifierName(identifiers[index]) 529 | ? {type: 'Identifier', name: identifiers[index]} 530 | : {type: 'Literal', value: identifiers[index]} 531 | result = result 532 | ? { 533 | type: 'MemberExpression', 534 | object: result, 535 | property, 536 | computed: Boolean(index && property.type === 'Literal'), 537 | optional: false 538 | } 539 | : property 540 | } 541 | 542 | assert(result, 'always a result') 543 | return result 544 | } 545 | 546 | /** 547 | * Inherit some fields from `from` into `to`. 548 | * 549 | * @param {Node} from 550 | * Node to inherit from. 551 | * @param {Node} to 552 | * Node to add to. 553 | * @returns {undefined} 554 | * Nothing. 555 | */ 556 | function create(from, to) { 557 | const fields = ['start', 'end', 'loc', 'range', 'comments'] 558 | let index = -1 559 | 560 | while (++index < fields.length) { 561 | const field = fields[index] 562 | if (field in from) { 563 | // @ts-expect-error: indexable. 564 | to[field] = from[field] 565 | } 566 | } 567 | } 568 | -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @import {Comment, Expression, Program, Node} from 'estree-jsx' 3 | */ 4 | 5 | import assert from 'node:assert/strict' 6 | import test from 'node:test' 7 | import {Parser} from 'acorn' 8 | import jsx from 'acorn-jsx' 9 | import {generate} from 'astring' 10 | import {buildJsx} from 'estree-util-build-jsx' 11 | import {walk} from 'estree-walker' 12 | 13 | const parser = Parser.extend(jsx()) 14 | 15 | test('estree-util-build-jsx', async function (t) { 16 | await t.test('should expose the public api', async function () { 17 | assert.deepEqual( 18 | Object.keys(await import('estree-util-build-jsx')).sort(), 19 | ['buildJsx'] 20 | ) 21 | }) 22 | 23 | await t.test( 24 | 'should default to `React.createElement` / `React.Fragment`', 25 | function () { 26 | const tree = parse('<>') 27 | buildJsx(tree) 28 | 29 | assert.deepEqual(expression(tree), { 30 | type: 'CallExpression', 31 | callee: { 32 | type: 'MemberExpression', 33 | object: {type: 'Identifier', name: 'React'}, 34 | property: {type: 'Identifier', name: 'createElement'}, 35 | computed: false, 36 | optional: false 37 | }, 38 | arguments: [ 39 | { 40 | type: 'MemberExpression', 41 | object: {type: 'Identifier', name: 'React'}, 42 | property: {type: 'Identifier', name: 'Fragment'}, 43 | computed: false, 44 | optional: false 45 | }, 46 | {type: 'Literal', value: null}, 47 | { 48 | type: 'CallExpression', 49 | callee: { 50 | type: 'MemberExpression', 51 | object: {type: 'Identifier', name: 'React'}, 52 | property: {type: 'Identifier', name: 'createElement'}, 53 | computed: false, 54 | optional: false 55 | }, 56 | arguments: [{type: 'Literal', value: 'x'}], 57 | optional: false 58 | } 59 | ], 60 | optional: false 61 | }) 62 | } 63 | ) 64 | 65 | await t.test('should support `pragma`, `pragmaFrag`', function () { 66 | const tree = parse('<>') 67 | buildJsx(tree, {pragma: 'a', pragmaFrag: 'b'}) 68 | 69 | assert.deepEqual(expression(tree), { 70 | type: 'CallExpression', 71 | callee: {type: 'Identifier', name: 'a'}, 72 | arguments: [ 73 | {type: 'Identifier', name: 'b'}, 74 | {type: 'Literal', value: null}, 75 | { 76 | type: 'CallExpression', 77 | callee: {type: 'Identifier', name: 'a'}, 78 | arguments: [{type: 'Literal', value: 'x'}], 79 | optional: false 80 | } 81 | ], 82 | optional: false 83 | }) 84 | }) 85 | 86 | await t.test('should support `pragma` w/ non-identifiers (1)', function () { 87 | const tree = parse('') 88 | buildJsx(tree, {pragma: 'a.b-c'}) 89 | 90 | assert.deepEqual(expression(tree), { 91 | type: 'CallExpression', 92 | callee: { 93 | type: 'MemberExpression', 94 | object: {type: 'Identifier', name: 'a'}, 95 | property: {type: 'Literal', value: 'b-c'}, 96 | computed: true, 97 | optional: false 98 | }, 99 | arguments: [{type: 'Literal', value: 'x'}], 100 | optional: false 101 | }) 102 | 103 | assert.equal(generate(tree), 'a["b-c"]("x");\n') 104 | }) 105 | 106 | await t.test('should support `@jsx`, `@jsxFrag` comments', function () { 107 | const tree = parse('/* @jsx a @jsxFrag b */\n<>') 108 | buildJsx(tree) 109 | 110 | assert.deepEqual(expression(tree), { 111 | type: 'CallExpression', 112 | callee: {type: 'Identifier', name: 'a'}, 113 | arguments: [ 114 | {type: 'Identifier', name: 'b'}, 115 | {type: 'Literal', value: null}, 116 | { 117 | type: 'CallExpression', 118 | callee: {type: 'Identifier', name: 'a'}, 119 | arguments: [{type: 'Literal', value: 'x'}], 120 | optional: false 121 | } 122 | ], 123 | optional: false 124 | }) 125 | }) 126 | 127 | await t.test( 128 | 'should throw when `@jsx` is set in the automatic runtime', 129 | function () { 130 | assert.throws(function () { 131 | buildJsx(parse('/* @jsx a @jsxRuntime automatic */')) 132 | }, /Unexpected `@jsx` pragma w\/ automatic runtime/) 133 | } 134 | ) 135 | 136 | await t.test( 137 | 'should throw when `@jsxFrag` is set in the automatic runtime', 138 | function () { 139 | assert.throws(function () { 140 | buildJsx(parse('/* @jsxFrag a @jsxRuntime automatic */')) 141 | }, /Unexpected `@jsxFrag` pragma w\/ automatic runtime/) 142 | } 143 | ) 144 | 145 | await t.test( 146 | 'should throw when `@jsxImportSource` is set in the classic runtime', 147 | function () { 148 | assert.throws(function () { 149 | buildJsx(parse('/* @jsxImportSource a @jsxRuntime classic */')) 150 | }, /Unexpected `@jsxImportSource` w\/ classic runtime/) 151 | } 152 | ) 153 | 154 | await t.test( 155 | 'should throw on a non-automatic nor classic `@jsxRuntime`', 156 | function () { 157 | assert.throws(function () { 158 | buildJsx(parse('/* @jsxRuntime a */')) 159 | }, /Unexpected `jsxRuntime` `a`, expected `automatic` or `classic`/) 160 | } 161 | ) 162 | 163 | await t.test('should ignore other comments', function () { 164 | const tree = parse('// a\n<>') 165 | buildJsx(tree) 166 | 167 | assert.deepEqual(expression(tree), { 168 | type: 'CallExpression', 169 | callee: { 170 | type: 'MemberExpression', 171 | object: {type: 'Identifier', name: 'React'}, 172 | property: {type: 'Identifier', name: 'createElement'}, 173 | computed: false, 174 | optional: false 175 | }, 176 | arguments: [ 177 | { 178 | type: 'MemberExpression', 179 | object: {type: 'Identifier', name: 'React'}, 180 | property: {type: 'Identifier', name: 'Fragment'}, 181 | computed: false, 182 | optional: false 183 | }, 184 | {type: 'Literal', value: null}, 185 | { 186 | type: 'CallExpression', 187 | callee: { 188 | type: 'MemberExpression', 189 | object: {type: 'Identifier', name: 'React'}, 190 | property: {type: 'Identifier', name: 'createElement'}, 191 | computed: false, 192 | optional: false 193 | }, 194 | arguments: [{type: 'Literal', value: 'x'}], 195 | optional: false 196 | } 197 | ], 198 | optional: false 199 | }) 200 | }) 201 | 202 | await t.test('should support a self-closing element', function () { 203 | const tree = parse('') 204 | buildJsx(tree, {pragma: 'h'}) 205 | 206 | assert.deepEqual(expression(tree), { 207 | type: 'CallExpression', 208 | callee: {type: 'Identifier', name: 'h'}, 209 | arguments: [{type: 'Literal', value: 'a'}], 210 | optional: false 211 | }) 212 | }) 213 | 214 | await t.test('should support a closed element', function () { 215 | const tree = parse('b') 216 | buildJsx(tree, {pragma: 'h'}) 217 | 218 | assert.deepEqual(expression(tree), { 219 | type: 'CallExpression', 220 | callee: {type: 'Identifier', name: 'h'}, 221 | arguments: [ 222 | {type: 'Literal', value: 'a'}, 223 | {type: 'Literal', value: null}, 224 | {type: 'Literal', value: 'b'} 225 | ], 226 | optional: false 227 | }) 228 | }) 229 | 230 | await t.test( 231 | 'should support dots in a tag name for member expressions', 232 | function () { 233 | const tree = parse('') 234 | buildJsx(tree, {pragma: 'h'}) 235 | 236 | assert.deepEqual(expression(tree), { 237 | type: 'CallExpression', 238 | callee: {type: 'Identifier', name: 'h'}, 239 | arguments: [ 240 | { 241 | type: 'MemberExpression', 242 | object: {type: 'Identifier', name: 'a'}, 243 | property: {type: 'Identifier', name: 'b'}, 244 | computed: false, 245 | optional: false 246 | } 247 | ], 248 | optional: false 249 | }) 250 | } 251 | ) 252 | 253 | await t.test( 254 | 'should support dots *and* dashes in tag names (1)', 255 | function () { 256 | const tree = parse('') 257 | buildJsx(tree, {pragma: 'h'}) 258 | 259 | assert.deepEqual(expression(tree), { 260 | type: 'CallExpression', 261 | callee: {type: 'Identifier', name: 'h'}, 262 | arguments: [ 263 | { 264 | type: 'MemberExpression', 265 | object: {type: 'Identifier', name: 'a'}, 266 | property: {type: 'Literal', value: 'b-c'}, 267 | computed: true, 268 | optional: false 269 | } 270 | ], 271 | optional: false 272 | }) 273 | 274 | assert.equal(generate(tree), 'h(a["b-c"]);\n') 275 | } 276 | ) 277 | 278 | await t.test( 279 | 'should support dots *and* dashes in tag names (2)', 280 | function () { 281 | const tree = parse('') 282 | buildJsx(tree, {pragma: 'h'}) 283 | 284 | assert.deepEqual(expression(tree), { 285 | type: 'CallExpression', 286 | callee: {type: 'Identifier', name: 'h'}, 287 | arguments: [ 288 | { 289 | type: 'MemberExpression', 290 | object: {type: 'Literal', value: 'a-b'}, 291 | property: {type: 'Identifier', name: 'c'}, 292 | computed: false, 293 | optional: false 294 | } 295 | ], 296 | optional: false 297 | }) 298 | 299 | assert.equal(generate(tree), 'h(("a-b").c);\n') 300 | } 301 | ) 302 | await t.test( 303 | 'should support dots in a tag name for member expressions (2)', 304 | function () { 305 | const tree = parse('') 306 | buildJsx(tree, {pragma: 'h'}) 307 | 308 | assert.deepEqual(expression(tree), { 309 | type: 'CallExpression', 310 | callee: {type: 'Identifier', name: 'h'}, 311 | arguments: [ 312 | { 313 | type: 'MemberExpression', 314 | object: { 315 | type: 'MemberExpression', 316 | object: { 317 | type: 'MemberExpression', 318 | object: {type: 'Identifier', name: 'a'}, 319 | property: {type: 'Identifier', name: 'b'}, 320 | computed: false, 321 | optional: false 322 | }, 323 | property: {type: 'Identifier', name: 'c'}, 324 | computed: false, 325 | optional: false 326 | }, 327 | property: {type: 'Identifier', name: 'd'}, 328 | computed: false, 329 | optional: false 330 | } 331 | ], 332 | optional: false 333 | }) 334 | } 335 | ) 336 | 337 | await t.test( 338 | 'should support colons in a tag name for namespaces', 339 | function () { 340 | const tree = parse('') 341 | buildJsx(tree, {pragma: 'h'}) 342 | 343 | assert.deepEqual(expression(tree), { 344 | type: 'CallExpression', 345 | callee: {type: 'Identifier', name: 'h'}, 346 | arguments: [{type: 'Literal', value: 'a:b'}], 347 | optional: false 348 | }) 349 | } 350 | ) 351 | 352 | await t.test('should support dashes in tag names', function () { 353 | const tree = parse('') 354 | buildJsx(tree, {pragma: 'h'}) 355 | 356 | assert.deepEqual(expression(tree), { 357 | type: 'CallExpression', 358 | callee: {type: 'Identifier', name: 'h'}, 359 | arguments: [{type: 'Literal', value: 'a-b'}], 360 | optional: false 361 | }) 362 | }) 363 | 364 | await t.test('should non-lowercase for components in tag names', function () { 365 | const tree = parse('') 366 | buildJsx(tree, {pragma: 'h'}) 367 | 368 | assert.deepEqual(expression(tree), { 369 | type: 'CallExpression', 370 | callee: {type: 'Identifier', name: 'h'}, 371 | arguments: [{type: 'Identifier', name: 'A'}], 372 | optional: false 373 | }) 374 | }) 375 | 376 | await t.test('should support a boolean prop', function () { 377 | const tree = parse('') 378 | buildJsx(tree, {pragma: 'h'}) 379 | 380 | assert.deepEqual(expression(tree), { 381 | type: 'CallExpression', 382 | callee: {type: 'Identifier', name: 'h'}, 383 | arguments: [ 384 | {type: 'Literal', value: 'a'}, 385 | { 386 | type: 'ObjectExpression', 387 | properties: [ 388 | { 389 | type: 'Property', 390 | key: {type: 'Identifier', name: 'b'}, 391 | value: {type: 'Literal', value: true}, 392 | kind: 'init', 393 | method: false, 394 | shorthand: false, 395 | computed: false 396 | } 397 | ] 398 | } 399 | ], 400 | optional: false 401 | }) 402 | }) 403 | 404 | await t.test('should support colons in prop names', function () { 405 | const tree = parse('') 406 | buildJsx(tree, {pragma: 'h'}) 407 | 408 | assert.deepEqual(expression(tree), { 409 | type: 'CallExpression', 410 | callee: {type: 'Identifier', name: 'h'}, 411 | arguments: [ 412 | {type: 'Literal', value: 'a'}, 413 | { 414 | type: 'ObjectExpression', 415 | properties: [ 416 | { 417 | type: 'Property', 418 | key: {type: 'Literal', value: 'b:c'}, 419 | value: {type: 'Literal', value: true}, 420 | kind: 'init', 421 | method: false, 422 | shorthand: false, 423 | computed: false 424 | } 425 | ] 426 | } 427 | ], 428 | optional: false 429 | }) 430 | }) 431 | 432 | await t.test( 433 | 'should support a prop name that can’t be an identifier', 434 | function () { 435 | const tree = parse('') 436 | buildJsx(tree, {pragma: 'h'}) 437 | 438 | assert.deepEqual(expression(tree), { 439 | type: 'CallExpression', 440 | callee: {type: 'Identifier', name: 'h'}, 441 | arguments: [ 442 | {type: 'Literal', value: 'a'}, 443 | { 444 | type: 'ObjectExpression', 445 | properties: [ 446 | { 447 | type: 'Property', 448 | key: {type: 'Literal', value: 'b-c'}, 449 | value: {type: 'Literal', value: true}, 450 | kind: 'init', 451 | method: false, 452 | shorthand: false, 453 | computed: false 454 | } 455 | ] 456 | } 457 | ], 458 | optional: false 459 | }) 460 | } 461 | ) 462 | 463 | await t.test('should support a prop value', function () { 464 | const tree = parse('') 465 | buildJsx(tree, {pragma: 'h'}) 466 | 467 | assert.deepEqual(expression(tree), { 468 | type: 'CallExpression', 469 | callee: {type: 'Identifier', name: 'h'}, 470 | arguments: [ 471 | {type: 'Literal', value: 'a'}, 472 | { 473 | type: 'ObjectExpression', 474 | properties: [ 475 | { 476 | type: 'Property', 477 | key: {type: 'Identifier', name: 'b'}, 478 | value: {type: 'Literal', value: 'c'}, 479 | kind: 'init', 480 | method: false, 481 | shorthand: false, 482 | computed: false 483 | } 484 | ] 485 | } 486 | ], 487 | optional: false 488 | }) 489 | }) 490 | 491 | await t.test('should support an expression as a prop value', function () { 492 | const tree = parse('') 493 | buildJsx(tree, {pragma: 'h'}) 494 | 495 | assert.deepEqual(expression(tree), { 496 | type: 'CallExpression', 497 | callee: {type: 'Identifier', name: 'h'}, 498 | arguments: [ 499 | {type: 'Literal', value: 'a'}, 500 | { 501 | type: 'ObjectExpression', 502 | properties: [ 503 | { 504 | type: 'Property', 505 | key: {type: 'Identifier', name: 'b'}, 506 | value: {type: 'Identifier', name: 'c'}, 507 | kind: 'init', 508 | method: false, 509 | shorthand: false, 510 | computed: false 511 | } 512 | ] 513 | } 514 | ], 515 | optional: false 516 | }) 517 | }) 518 | 519 | await t.test('should support an expression as a prop value (2)', function () { 520 | const tree = parse('') 521 | buildJsx(tree, {pragma: 'h'}) 522 | 523 | assert.deepEqual(expression(tree), { 524 | type: 'CallExpression', 525 | callee: {type: 'Identifier', name: 'h'}, 526 | arguments: [ 527 | {type: 'Literal', value: 'a'}, 528 | { 529 | type: 'ObjectExpression', 530 | properties: [ 531 | { 532 | type: 'Property', 533 | key: {type: 'Identifier', name: 'b'}, 534 | value: {type: 'Literal', value: 1}, 535 | kind: 'init', 536 | method: false, 537 | shorthand: false, 538 | computed: false 539 | } 540 | ] 541 | } 542 | ], 543 | optional: false 544 | }) 545 | }) 546 | 547 | await t.test('should support a fragment as a prop value', function () { 548 | const tree = parse('c />') 549 | buildJsx(tree, {pragma: 'h', pragmaFrag: 'f'}) 550 | 551 | assert.deepEqual(expression(tree), { 552 | type: 'CallExpression', 553 | callee: {type: 'Identifier', name: 'h'}, 554 | arguments: [ 555 | {type: 'Literal', value: 'a'}, 556 | { 557 | type: 'ObjectExpression', 558 | properties: [ 559 | { 560 | type: 'Property', 561 | key: {type: 'Identifier', name: 'b'}, 562 | value: { 563 | type: 'CallExpression', 564 | callee: {type: 'Identifier', name: 'h'}, 565 | arguments: [ 566 | {type: 'Identifier', name: 'f'}, 567 | {type: 'Literal', value: null}, 568 | {type: 'Literal', value: 'c'} 569 | ], 570 | optional: false 571 | }, 572 | kind: 'init', 573 | method: false, 574 | shorthand: false, 575 | computed: false 576 | } 577 | ] 578 | } 579 | ], 580 | optional: false 581 | }) 582 | }) 583 | 584 | await t.test('should support an element as a prop value', function () { 585 | const tree = parse(' />') 586 | buildJsx(tree, {pragma: 'h'}) 587 | 588 | assert.deepEqual(expression(tree), { 589 | type: 'CallExpression', 590 | callee: {type: 'Identifier', name: 'h'}, 591 | arguments: [ 592 | {type: 'Literal', value: 'a'}, 593 | { 594 | type: 'ObjectExpression', 595 | properties: [ 596 | { 597 | type: 'Property', 598 | key: {type: 'Identifier', name: 'b'}, 599 | value: { 600 | type: 'CallExpression', 601 | callee: {type: 'Identifier', name: 'h'}, 602 | arguments: [{type: 'Literal', value: 'c'}], 603 | optional: false 604 | }, 605 | kind: 'init', 606 | method: false, 607 | shorthand: false, 608 | computed: false 609 | } 610 | ] 611 | } 612 | ], 613 | optional: false 614 | }) 615 | }) 616 | 617 | await t.test('should support a single spread prop', function () { 618 | const tree = parse('') 619 | buildJsx(tree, {pragma: 'h'}) 620 | 621 | assert.deepEqual(expression(tree), { 622 | type: 'CallExpression', 623 | callee: {type: 'Identifier', name: 'h'}, 624 | arguments: [ 625 | {type: 'Literal', value: 'a'}, 626 | { 627 | type: 'ObjectExpression', 628 | properties: [ 629 | {type: 'SpreadElement', argument: {type: 'Identifier', name: 'b'}} 630 | ] 631 | } 632 | ], 633 | optional: false 634 | }) 635 | }) 636 | 637 | await t.test('should support a spread prop and another prop', function () { 638 | const tree = parse('') 639 | buildJsx(tree, {pragma: 'h'}) 640 | 641 | assert.deepEqual(expression(tree), { 642 | type: 'CallExpression', 643 | callee: {type: 'Identifier', name: 'h'}, 644 | arguments: [ 645 | {type: 'Literal', value: 'a'}, 646 | { 647 | type: 'ObjectExpression', 648 | properties: [ 649 | { 650 | type: 'SpreadElement', 651 | argument: {type: 'Identifier', name: 'b'} 652 | }, 653 | { 654 | type: 'Property', 655 | key: {type: 'Identifier', name: 'c'}, 656 | value: {type: 'Literal', value: true}, 657 | kind: 'init', 658 | method: false, 659 | shorthand: false, 660 | computed: false 661 | } 662 | ] 663 | } 664 | ], 665 | optional: false 666 | }) 667 | }) 668 | 669 | await t.test('should support a prop and a spread prop', function () { 670 | const tree = parse('') 671 | buildJsx(tree, {pragma: 'h'}) 672 | 673 | assert.deepEqual(expression(tree), { 674 | type: 'CallExpression', 675 | callee: {type: 'Identifier', name: 'h'}, 676 | arguments: [ 677 | {type: 'Literal', value: 'a'}, 678 | { 679 | type: 'ObjectExpression', 680 | properties: [ 681 | { 682 | type: 'Property', 683 | key: {type: 'Identifier', name: 'b'}, 684 | value: {type: 'Literal', value: true}, 685 | kind: 'init', 686 | method: false, 687 | shorthand: false, 688 | computed: false 689 | }, 690 | {type: 'SpreadElement', argument: {type: 'Identifier', name: 'c'}} 691 | ] 692 | } 693 | ], 694 | optional: false 695 | }) 696 | }) 697 | 698 | await t.test('should support two spread props', function () { 699 | const tree = parse('') 700 | buildJsx(tree, {pragma: 'h'}) 701 | 702 | assert.deepEqual(expression(tree), { 703 | type: 'CallExpression', 704 | callee: {type: 'Identifier', name: 'h'}, 705 | arguments: [ 706 | {type: 'Literal', value: 'a'}, 707 | { 708 | type: 'ObjectExpression', 709 | properties: [ 710 | { 711 | type: 'SpreadElement', 712 | argument: {type: 'Identifier', name: 'b'} 713 | }, 714 | { 715 | type: 'SpreadElement', 716 | argument: {type: 'Identifier', name: 'c'} 717 | } 718 | ] 719 | } 720 | ], 721 | optional: false 722 | }) 723 | }) 724 | 725 | await t.test('should support more complex spreads', function () { 726 | const tree = parse('') 727 | buildJsx(tree, {pragma: 'h'}) 728 | 729 | assert.deepEqual(expression(tree), { 730 | type: 'CallExpression', 731 | callee: {type: 'Identifier', name: 'h'}, 732 | arguments: [ 733 | {type: 'Literal', value: 'a'}, 734 | { 735 | type: 'ObjectExpression', 736 | properties: [ 737 | { 738 | type: 'Property', 739 | method: false, 740 | shorthand: false, 741 | computed: false, 742 | key: {type: 'Identifier', name: 'b'}, 743 | value: {type: 'Literal', value: 1}, 744 | kind: 'init' 745 | }, 746 | { 747 | type: 'SpreadElement', 748 | argument: {type: 'Identifier', name: 'c'} 749 | }, 750 | { 751 | type: 'Property', 752 | method: false, 753 | shorthand: false, 754 | computed: false, 755 | key: {type: 'Identifier', name: 'd'}, 756 | value: {type: 'Literal', value: 2}, 757 | kind: 'init' 758 | } 759 | ] 760 | } 761 | ], 762 | optional: false 763 | }) 764 | }) 765 | 766 | await t.test('should support expressions content', function () { 767 | const tree = parse('{1}') 768 | buildJsx(tree, {pragma: 'h'}) 769 | 770 | assert.deepEqual(expression(tree), { 771 | type: 'CallExpression', 772 | callee: {type: 'Identifier', name: 'h'}, 773 | arguments: [ 774 | {type: 'Literal', value: 'a'}, 775 | {type: 'Literal', value: null}, 776 | {type: 'Literal', value: 1} 777 | ], 778 | optional: false 779 | }) 780 | }) 781 | 782 | await t.test('should support empty expressions content', function () { 783 | const tree = parse('{}') 784 | buildJsx(tree, {pragma: 'h'}) 785 | 786 | assert.deepEqual(expression(tree), { 787 | type: 'CallExpression', 788 | callee: {type: 'Identifier', name: 'h'}, 789 | arguments: [{type: 'Literal', value: 'a'}], 790 | optional: false 791 | }) 792 | }) 793 | 794 | await t.test('should support initial spaces in content', function () { 795 | const tree = parse(' b') 796 | buildJsx(tree, {pragma: 'h'}) 797 | 798 | assert.deepEqual(expression(tree), { 799 | type: 'CallExpression', 800 | callee: {type: 'Identifier', name: 'h'}, 801 | arguments: [ 802 | {type: 'Literal', value: 'a'}, 803 | {type: 'Literal', value: null}, 804 | {type: 'Literal', value: ' b'} 805 | ], 806 | optional: false 807 | }) 808 | }) 809 | 810 | await t.test('should support final spaces in content', function () { 811 | const tree = parse('b ') 812 | buildJsx(tree, {pragma: 'h'}) 813 | 814 | assert.deepEqual(expression(tree), { 815 | type: 'CallExpression', 816 | callee: {type: 'Identifier', name: 'h'}, 817 | arguments: [ 818 | {type: 'Literal', value: 'a'}, 819 | {type: 'Literal', value: null}, 820 | {type: 'Literal', value: 'b '} 821 | ], 822 | optional: false 823 | }) 824 | }) 825 | 826 | await t.test( 827 | 'should support initial and final spaces in content', 828 | function () { 829 | const tree = parse(' b ') 830 | buildJsx(tree, {pragma: 'h'}) 831 | 832 | assert.deepEqual(expression(tree), { 833 | type: 'CallExpression', 834 | callee: {type: 'Identifier', name: 'h'}, 835 | arguments: [ 836 | {type: 'Literal', value: 'a'}, 837 | {type: 'Literal', value: null}, 838 | {type: 'Literal', value: ' b '} 839 | ], 840 | optional: false 841 | }) 842 | } 843 | ) 844 | 845 | await t.test('should support spaces around line endings', function () { 846 | const tree = parse(' b \r c \n d \n ') 847 | buildJsx(tree, {pragma: 'h'}) 848 | 849 | assert.deepEqual(expression(tree), { 850 | type: 'CallExpression', 851 | callee: {type: 'Identifier', name: 'h'}, 852 | arguments: [ 853 | {type: 'Literal', value: 'a'}, 854 | {type: 'Literal', value: null}, 855 | {type: 'Literal', value: ' b c d'} 856 | ], 857 | optional: false 858 | }) 859 | }) 860 | 861 | await t.test( 862 | 'should support skip empty or whitespace only line endings', 863 | function () { 864 | const tree = parse(' b \r \n c \n\n d \n ') 865 | buildJsx(tree, {pragma: 'h'}) 866 | 867 | assert.deepEqual(expression(tree), { 868 | type: 'CallExpression', 869 | callee: {type: 'Identifier', name: 'h'}, 870 | arguments: [ 871 | {type: 'Literal', value: 'a'}, 872 | {type: 'Literal', value: null}, 873 | {type: 'Literal', value: ' b c d'} 874 | ], 875 | optional: false 876 | }) 877 | } 878 | ) 879 | 880 | await t.test('should support skip whitespace only content', function () { 881 | const tree = parse(' \t\n ') 882 | buildJsx(tree, {pragma: 'h'}) 883 | 884 | assert.deepEqual(expression(tree), { 885 | type: 'CallExpression', 886 | callee: {type: 'Identifier', name: 'h'}, 887 | arguments: [{type: 'Literal', value: 'a'}], 888 | optional: false 889 | }) 890 | }) 891 | 892 | await t.test('should trim strings with leading line feed', function () { 893 | const tree = parse('\n line1\n') 894 | buildJsx(tree, {pragma: 'h'}) 895 | 896 | assert.deepEqual(expression(tree), { 897 | type: 'CallExpression', 898 | callee: {type: 'Identifier', name: 'h'}, 899 | arguments: [ 900 | {type: 'Literal', value: 'a'}, 901 | {type: 'Literal', value: null}, 902 | {type: 'Literal', value: 'line1'} 903 | ], 904 | optional: false 905 | }) 906 | }) 907 | 908 | await t.test( 909 | 'should trim strings with leading line feed (multiline test)', 910 | function () { 911 | const tree = parse('\n line1{" "}\n line2\n') 912 | buildJsx(tree, {pragma: 'h'}) 913 | 914 | assert.deepEqual(expression(tree), { 915 | type: 'CallExpression', 916 | callee: {type: 'Identifier', name: 'h'}, 917 | arguments: [ 918 | {type: 'Literal', value: 'a'}, 919 | {type: 'Literal', value: null}, 920 | {type: 'Literal', value: 'line1'}, 921 | {type: 'Literal', value: ' '}, 922 | {type: 'Literal', value: 'line2'} 923 | ], 924 | optional: false 925 | }) 926 | } 927 | ) 928 | 929 | await t.test('should integrate w/ generators (`astring`)', function () { 930 | const tree = parse('<>\n h\n') 931 | buildJsx(tree, {pragma: 'h', pragmaFrag: 'f'}) 932 | 933 | assert.deepEqual( 934 | generate(tree), 935 | 'h(f, null, h("a", {\n b: true,\n c: "d",\n e: f,\n ...g\n}, "h"));\n' 936 | ) 937 | }) 938 | 939 | await t.test('should support positional info', function () { 940 | const tree = parse('<>\n h\n', false) 941 | buildJsx(tree) 942 | 943 | assert.deepEqual(tree, { 944 | type: 'Program', 945 | start: 0, 946 | end: 38, 947 | loc: {start: {line: 1, column: 0}, end: {line: 3, column: 3}}, 948 | range: [0, 38], 949 | body: [ 950 | { 951 | type: 'ExpressionStatement', 952 | start: 0, 953 | end: 38, 954 | loc: {start: {line: 1, column: 0}, end: {line: 3, column: 3}}, 955 | range: [0, 38], 956 | expression: { 957 | type: 'CallExpression', 958 | callee: { 959 | type: 'MemberExpression', 960 | object: {type: 'Identifier', name: 'React'}, 961 | property: {type: 'Identifier', name: 'createElement'}, 962 | computed: false, 963 | optional: false 964 | }, 965 | arguments: [ 966 | { 967 | type: 'MemberExpression', 968 | object: {type: 'Identifier', name: 'React'}, 969 | property: {type: 'Identifier', name: 'Fragment'}, 970 | computed: false, 971 | optional: false 972 | }, 973 | {type: 'Literal', value: null}, 974 | { 975 | type: 'CallExpression', 976 | callee: { 977 | type: 'MemberExpression', 978 | object: {type: 'Identifier', name: 'React'}, 979 | property: {type: 'Identifier', name: 'createElement'}, 980 | computed: false, 981 | optional: false 982 | }, 983 | arguments: [ 984 | { 985 | type: 'Literal', 986 | value: 'a', 987 | start: 6, 988 | end: 7, 989 | loc: { 990 | start: {line: 2, column: 3}, 991 | end: {line: 2, column: 4} 992 | }, 993 | range: [6, 7] 994 | }, 995 | { 996 | type: 'ObjectExpression', 997 | properties: [ 998 | { 999 | type: 'Property', 1000 | key: { 1001 | type: 'Identifier', 1002 | name: 'b', 1003 | start: 8, 1004 | end: 9, 1005 | loc: { 1006 | start: {line: 2, column: 5}, 1007 | end: {line: 2, column: 6} 1008 | }, 1009 | range: [8, 9] 1010 | }, 1011 | value: {type: 'Literal', value: true}, 1012 | kind: 'init', 1013 | method: false, 1014 | shorthand: false, 1015 | computed: false, 1016 | start: 8, 1017 | end: 9, 1018 | loc: { 1019 | start: {line: 2, column: 5}, 1020 | end: {line: 2, column: 6} 1021 | }, 1022 | range: [8, 9] 1023 | }, 1024 | { 1025 | type: 'Property', 1026 | key: { 1027 | type: 'Identifier', 1028 | name: 'c', 1029 | start: 10, 1030 | end: 11, 1031 | loc: { 1032 | start: {line: 2, column: 7}, 1033 | end: {line: 2, column: 8} 1034 | }, 1035 | range: [10, 11] 1036 | }, 1037 | value: { 1038 | type: 'Literal', 1039 | start: 12, 1040 | end: 15, 1041 | loc: { 1042 | start: {line: 2, column: 9}, 1043 | end: {line: 2, column: 12} 1044 | }, 1045 | range: [12, 15], 1046 | value: 'd' 1047 | }, 1048 | kind: 'init', 1049 | method: false, 1050 | shorthand: false, 1051 | computed: false, 1052 | start: 10, 1053 | end: 15, 1054 | loc: { 1055 | start: {line: 2, column: 7}, 1056 | end: {line: 2, column: 12} 1057 | }, 1058 | range: [10, 15] 1059 | }, 1060 | { 1061 | type: 'Property', 1062 | key: { 1063 | type: 'Identifier', 1064 | name: 'e', 1065 | start: 16, 1066 | end: 17, 1067 | loc: { 1068 | start: {line: 2, column: 13}, 1069 | end: {line: 2, column: 14} 1070 | }, 1071 | range: [16, 17] 1072 | }, 1073 | value: { 1074 | type: 'Identifier', 1075 | start: 19, 1076 | end: 20, 1077 | loc: { 1078 | start: {line: 2, column: 16}, 1079 | end: {line: 2, column: 17} 1080 | }, 1081 | range: [19, 20], 1082 | name: 'f' 1083 | }, 1084 | kind: 'init', 1085 | method: false, 1086 | shorthand: false, 1087 | computed: false, 1088 | start: 16, 1089 | end: 21, 1090 | loc: { 1091 | start: {line: 2, column: 13}, 1092 | end: {line: 2, column: 18} 1093 | }, 1094 | range: [16, 21] 1095 | }, 1096 | { 1097 | type: 'SpreadElement', 1098 | argument: { 1099 | type: 'Identifier', 1100 | start: 26, 1101 | end: 27, 1102 | loc: { 1103 | start: {line: 2, column: 23}, 1104 | end: {line: 2, column: 24} 1105 | }, 1106 | range: [26, 27], 1107 | name: 'g' 1108 | } 1109 | } 1110 | ] 1111 | }, 1112 | { 1113 | type: 'Literal', 1114 | value: 'h', 1115 | start: 29, 1116 | end: 30, 1117 | loc: { 1118 | start: {line: 2, column: 26}, 1119 | end: {line: 2, column: 27} 1120 | }, 1121 | range: [29, 30] 1122 | } 1123 | ], 1124 | optional: false, 1125 | start: 5, 1126 | end: 34, 1127 | loc: { 1128 | start: {line: 2, column: 2}, 1129 | end: {line: 2, column: 31} 1130 | }, 1131 | range: [5, 34] 1132 | } 1133 | ], 1134 | optional: false, 1135 | start: 0, 1136 | end: 38, 1137 | loc: {start: {line: 1, column: 0}, end: {line: 3, column: 3}}, 1138 | range: [0, 38] 1139 | } 1140 | } 1141 | ], 1142 | sourceType: 'script', 1143 | comments: [] 1144 | }) 1145 | }) 1146 | 1147 | await t.test('should support no comments on `program`', function () { 1148 | const tree = parse('<>', true, false) 1149 | buildJsx(tree) 1150 | 1151 | assert.deepEqual(tree, { 1152 | type: 'Program', 1153 | body: [ 1154 | { 1155 | type: 'ExpressionStatement', 1156 | expression: { 1157 | type: 'CallExpression', 1158 | callee: { 1159 | type: 'MemberExpression', 1160 | object: {type: 'Identifier', name: 'React'}, 1161 | property: {type: 'Identifier', name: 'createElement'}, 1162 | computed: false, 1163 | optional: false 1164 | }, 1165 | arguments: [ 1166 | { 1167 | type: 'MemberExpression', 1168 | object: {type: 'Identifier', name: 'React'}, 1169 | property: {type: 'Identifier', name: 'Fragment'}, 1170 | computed: false, 1171 | optional: false 1172 | }, 1173 | {type: 'Literal', value: null}, 1174 | { 1175 | type: 'CallExpression', 1176 | callee: { 1177 | type: 'MemberExpression', 1178 | object: {type: 'Identifier', name: 'React'}, 1179 | property: {type: 'Identifier', name: 'createElement'}, 1180 | computed: false, 1181 | optional: false 1182 | }, 1183 | arguments: [{type: 'Literal', value: 'x'}], 1184 | optional: false 1185 | } 1186 | ], 1187 | optional: false 1188 | } 1189 | } 1190 | ], 1191 | sourceType: 'script' 1192 | }) 1193 | }) 1194 | 1195 | await t.test( 1196 | 'should support the automatic runtime (fragment, jsx, settings)', 1197 | function () { 1198 | const tree = parse('<>a') 1199 | buildJsx(tree, {runtime: 'automatic'}) 1200 | 1201 | assert.equal( 1202 | generate(tree), 1203 | [ 1204 | 'import {Fragment as _Fragment, jsx as _jsx} from "react/jsx-runtime";', 1205 | '_jsx(_Fragment, {', 1206 | ' children: "a"', 1207 | '});', 1208 | '' 1209 | ].join('\n') 1210 | ) 1211 | } 1212 | ) 1213 | 1214 | await t.test( 1215 | 'should support the automatic runtime (jsxs, key, comment)', 1216 | function () { 1217 | const tree = parse('/*@jsxRuntime automatic*/\nb{1}') 1218 | buildJsx(tree) 1219 | 1220 | assert.equal( 1221 | generate(tree), 1222 | [ 1223 | 'import {jsxs as _jsxs} from "react/jsx-runtime";', 1224 | '_jsxs("a", {', 1225 | ' children: ["b", 1]', 1226 | '}, "a");', 1227 | '' 1228 | ].join('\n') 1229 | ) 1230 | } 1231 | ) 1232 | 1233 | await t.test( 1234 | 'should support the automatic runtime (props, spread, children)', 1235 | function () { 1236 | const tree = parse('d') 1237 | buildJsx(tree, {runtime: 'automatic'}) 1238 | 1239 | assert.equal( 1240 | generate(tree), 1241 | [ 1242 | 'import {jsx as _jsx} from "react/jsx-runtime";', 1243 | '_jsx("a", {', 1244 | ' b: "1",', 1245 | ' ...c,', 1246 | ' children: "d"', 1247 | '});', 1248 | '' 1249 | ].join('\n') 1250 | ) 1251 | } 1252 | ) 1253 | 1254 | await t.test( 1255 | 'should support the automatic runtime (spread, props, children)', 1256 | function () { 1257 | const tree = parse('f') 1258 | buildJsx(tree, {runtime: 'automatic'}) 1259 | 1260 | assert.equal( 1261 | generate(tree), 1262 | [ 1263 | 'import {jsx as _jsx} from "react/jsx-runtime";', 1264 | '_jsx("a", {', 1265 | ' b: 1,', 1266 | ' c: 2,', 1267 | ' d: "e",', 1268 | ' children: "f"', 1269 | '});', 1270 | '' 1271 | ].join('\n') 1272 | ) 1273 | } 1274 | ) 1275 | 1276 | await t.test( 1277 | 'should support the automatic runtime (no props, children)', 1278 | function () { 1279 | const tree = parse('b') 1280 | buildJsx(tree, {runtime: 'automatic'}) 1281 | 1282 | assert.equal( 1283 | generate(tree), 1284 | [ 1285 | 'import {jsx as _jsx} from "react/jsx-runtime";', 1286 | '_jsx("a", {', 1287 | ' children: "b"', 1288 | '});', 1289 | '' 1290 | ].join('\n') 1291 | ) 1292 | } 1293 | ) 1294 | 1295 | await t.test( 1296 | 'should support the automatic runtime (no props, no children)', 1297 | function () { 1298 | const tree = parse('') 1299 | buildJsx(tree, {runtime: 'automatic'}) 1300 | 1301 | assert.equal( 1302 | generate(tree), 1303 | [ 1304 | 'import {jsx as _jsx} from "react/jsx-runtime";', 1305 | '_jsx("a", {});', 1306 | '' 1307 | ].join('\n') 1308 | ) 1309 | } 1310 | ) 1311 | 1312 | await t.test( 1313 | 'should support the automatic runtime (key, no props, no children)', 1314 | function () { 1315 | const tree = parse('') 1316 | buildJsx(tree, {runtime: 'automatic'}) 1317 | 1318 | assert.equal( 1319 | generate(tree), 1320 | [ 1321 | 'import {jsx as _jsx} from "react/jsx-runtime";', 1322 | '_jsx("a", {}, true);', 1323 | '' 1324 | ].join('\n') 1325 | ) 1326 | } 1327 | ) 1328 | 1329 | await t.test( 1330 | 'should support the automatic runtime (fragment, jsx, settings, development)', 1331 | function () { 1332 | const tree = parse('<>a', false) 1333 | buildJsx(tree, { 1334 | runtime: 'automatic', 1335 | development: true, 1336 | filePath: 'index.js' 1337 | }) 1338 | 1339 | assert.equal( 1340 | generate(tree), 1341 | [ 1342 | 'import {Fragment as _Fragment, jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', 1343 | '_jsxDEV(_Fragment, {', 1344 | ' children: "a"', 1345 | '}, undefined, false, {', 1346 | ' fileName: "index.js",', 1347 | ' lineNumber: 1,', 1348 | ' columnNumber: 1', 1349 | '}, this);', 1350 | '' 1351 | ].join('\n') 1352 | ) 1353 | } 1354 | ) 1355 | 1356 | await t.test( 1357 | 'should support the automatic runtime (jsxs, key, comment, development)', 1358 | function () { 1359 | const tree = parse('b{1}', false) 1360 | buildJsx(tree, { 1361 | runtime: 'automatic', 1362 | development: true, 1363 | filePath: 'index.js' 1364 | }) 1365 | 1366 | assert.equal( 1367 | generate(tree), 1368 | [ 1369 | 'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', 1370 | '_jsxDEV("a", {', 1371 | ' children: ["b", 1]', 1372 | '}, "a", true, {', 1373 | ' fileName: "index.js",', 1374 | ' lineNumber: 1,', 1375 | ' columnNumber: 1', 1376 | '}, this);', 1377 | '' 1378 | ].join('\n') 1379 | ) 1380 | } 1381 | ) 1382 | 1383 | await t.test( 1384 | 'should support the automatic runtime (props, spread, children, development)', 1385 | function () { 1386 | const tree = parse('d', false) 1387 | 1388 | buildJsx(tree, { 1389 | runtime: 'automatic', 1390 | development: true, 1391 | filePath: 'index.js' 1392 | }) 1393 | 1394 | assert.equal( 1395 | generate(tree), 1396 | [ 1397 | 'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', 1398 | '_jsxDEV("a", {', 1399 | ' b: "1",', 1400 | ' ...c,', 1401 | ' children: "d"', 1402 | '}, undefined, false, {', 1403 | ' fileName: "index.js",', 1404 | ' lineNumber: 1,', 1405 | ' columnNumber: 1', 1406 | '}, this);', 1407 | '' 1408 | ].join('\n') 1409 | ) 1410 | } 1411 | ) 1412 | 1413 | await t.test( 1414 | 'should support the automatic runtime (spread, props, children, development)', 1415 | function () { 1416 | const tree = parse('f', false) 1417 | 1418 | buildJsx(tree, { 1419 | runtime: 'automatic', 1420 | development: true, 1421 | filePath: 'index.js' 1422 | }) 1423 | 1424 | assert.equal( 1425 | generate(tree), 1426 | [ 1427 | 'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', 1428 | '_jsxDEV("a", {', 1429 | ' b: 1,', 1430 | ' c: 2,', 1431 | ' d: "e",', 1432 | ' children: "f"', 1433 | '}, undefined, false, {', 1434 | ' fileName: "index.js",', 1435 | ' lineNumber: 1,', 1436 | ' columnNumber: 1', 1437 | '}, this);', 1438 | '' 1439 | ].join('\n') 1440 | ) 1441 | } 1442 | ) 1443 | 1444 | await t.test( 1445 | 'should support the automatic runtime (no props, children, development)', 1446 | function () { 1447 | const tree = parse('b', false) 1448 | 1449 | buildJsx(tree, { 1450 | runtime: 'automatic', 1451 | development: true, 1452 | filePath: 'index.js' 1453 | }) 1454 | 1455 | assert.equal( 1456 | generate(tree), 1457 | [ 1458 | 'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', 1459 | '_jsxDEV("a", {', 1460 | ' children: "b"', 1461 | '}, undefined, false, {', 1462 | ' fileName: "index.js",', 1463 | ' lineNumber: 1,', 1464 | ' columnNumber: 1', 1465 | '}, this);', 1466 | '' 1467 | ].join('\n') 1468 | ) 1469 | } 1470 | ) 1471 | 1472 | await t.test( 1473 | 'should support the automatic runtime (no props, no children, development)', 1474 | function () { 1475 | const tree = parse('', false) 1476 | 1477 | buildJsx(tree, { 1478 | runtime: 'automatic', 1479 | development: true, 1480 | filePath: 'index.js' 1481 | }) 1482 | 1483 | assert.equal( 1484 | generate(tree), 1485 | [ 1486 | 'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', 1487 | '_jsxDEV("a", {}, undefined, false, {', 1488 | ' fileName: "index.js",', 1489 | ' lineNumber: 1,', 1490 | ' columnNumber: 1', 1491 | '}, this);', 1492 | '' 1493 | ].join('\n') 1494 | ) 1495 | } 1496 | ) 1497 | 1498 | await t.test( 1499 | 'should support the automatic runtime (key, no props, no children, development)', 1500 | function () { 1501 | const tree = parse('', false) 1502 | 1503 | buildJsx(tree, { 1504 | runtime: 'automatic', 1505 | development: true, 1506 | filePath: 'index.js' 1507 | }) 1508 | 1509 | assert.equal( 1510 | generate(tree), 1511 | [ 1512 | 'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', 1513 | '_jsxDEV("a", {}, true, false, {', 1514 | ' fileName: "index.js",', 1515 | ' lineNumber: 1,', 1516 | ' columnNumber: 1', 1517 | '}, this);', 1518 | '' 1519 | ].join('\n') 1520 | ) 1521 | } 1522 | ) 1523 | 1524 | await t.test( 1525 | 'should support the automatic runtime (no props, no children, development, no filePath)', 1526 | function () { 1527 | const tree = parse('', false) 1528 | 1529 | buildJsx(tree, { 1530 | runtime: 'automatic', 1531 | development: true 1532 | }) 1533 | 1534 | assert.equal( 1535 | generate(tree), 1536 | [ 1537 | 'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', 1538 | '_jsxDEV("a", {}, undefined, false, {', 1539 | ' fileName: "",', 1540 | ' lineNumber: 1,', 1541 | ' columnNumber: 1', 1542 | '}, this);', 1543 | '' 1544 | ].join('\n') 1545 | ) 1546 | } 1547 | ) 1548 | 1549 | await t.test( 1550 | 'should support the automatic runtime (no props, no children, development, empty filePath)', 1551 | function () { 1552 | const tree = parse('', false) 1553 | 1554 | buildJsx(tree, { 1555 | runtime: 'automatic', 1556 | development: true, 1557 | filePath: '' 1558 | }) 1559 | 1560 | assert.equal( 1561 | generate(tree), 1562 | [ 1563 | 'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', 1564 | '_jsxDEV("a", {}, undefined, false, {', 1565 | ' fileName: "",', 1566 | ' lineNumber: 1,', 1567 | ' columnNumber: 1', 1568 | '}, this);', 1569 | '' 1570 | ].join('\n') 1571 | ) 1572 | } 1573 | ) 1574 | 1575 | await t.test( 1576 | 'should support the automatic runtime (no props, no children, development, no locations)', 1577 | function () { 1578 | const tree = parse('') 1579 | 1580 | buildJsx(tree, { 1581 | runtime: 'automatic', 1582 | development: true, 1583 | filePath: 'index.js' 1584 | }) 1585 | 1586 | assert.equal( 1587 | generate(tree), 1588 | [ 1589 | 'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', 1590 | '_jsxDEV("a", {}, undefined, false, {', 1591 | ' fileName: "index.js"', 1592 | '}, this);', 1593 | '' 1594 | ].join('\n') 1595 | ) 1596 | } 1597 | ) 1598 | 1599 | await t.test( 1600 | 'should support the automatic runtime (no props, nested children, development, positional info)', 1601 | function () { 1602 | const tree = parse('\n \n', false) 1603 | 1604 | buildJsx(tree, { 1605 | runtime: 'automatic', 1606 | development: true, 1607 | filePath: 'index.js' 1608 | }) 1609 | 1610 | assert.equal( 1611 | generate(tree), 1612 | [ 1613 | 'import {jsxDEV as _jsxDEV} from "react/jsx-dev-runtime";', 1614 | '_jsxDEV("a", {', 1615 | ' children: _jsxDEV("b", {}, undefined, false, {', 1616 | ' fileName: "index.js",', 1617 | ' lineNumber: 2,', 1618 | ' columnNumber: 3', 1619 | ' }, this)', 1620 | '}, undefined, false, {', 1621 | ' fileName: "index.js",', 1622 | ' lineNumber: 1,', 1623 | ' columnNumber: 1', 1624 | '}, this);', 1625 | '' 1626 | ].join('\n') 1627 | ) 1628 | } 1629 | ) 1630 | 1631 | await t.test('should throw on spread after `key`', function () { 1632 | assert.throws(function () { 1633 | buildJsx(parse(''), {runtime: 'automatic'}) 1634 | }, /Expected `key` to come before any spread expressions/) 1635 | }) 1636 | 1637 | await t.test( 1638 | 'should prefer a `jsxRuntime` comment over a `runtime` option', 1639 | function () { 1640 | const tree = parse('/*@jsxRuntime classic*/ ') 1641 | 1642 | buildJsx(tree, {runtime: 'automatic'}) 1643 | 1644 | assert.equal(generate(tree), 'React.createElement("a");\n') 1645 | } 1646 | ) 1647 | 1648 | await t.test('should keep directives first', function () { 1649 | const tree = parse('"use client"\nconst x = ') 1650 | 1651 | buildJsx(tree, {runtime: 'automatic'}) 1652 | 1653 | assert.equal( 1654 | generate(tree), 1655 | '"use client";\nimport {jsx as _jsx} from "react/jsx-runtime";\nconst x = _jsx("a", {});\n' 1656 | ) 1657 | }) 1658 | }) 1659 | 1660 | /** 1661 | * @param {Program} program 1662 | * @returns {Expression} 1663 | */ 1664 | function expression(program) { 1665 | const head = program.body[0] 1666 | 1667 | if (!head || head.type !== 'ExpressionStatement') { 1668 | throw new Error('Expected single expression') 1669 | } 1670 | 1671 | return head.expression 1672 | } 1673 | 1674 | /** 1675 | * Parse a string of JS. 1676 | * 1677 | * @param {string} document 1678 | * Value. 1679 | * @param {boolean} [clean=true] 1680 | * Clean positional info (default: `true`). 1681 | * @param {boolean} [addComments=true] 1682 | * Add comments (default: `true`). 1683 | * @returns {Program} 1684 | * ESTree program. 1685 | */ 1686 | function parse(document, clean, addComments) { 1687 | /** @type {Array} */ 1688 | const comments = [] 1689 | const tree = /** @type {Program} */ ( 1690 | parser.parse(document, { 1691 | ecmaVersion: 'latest', 1692 | ranges: true, 1693 | locations: true, 1694 | // @ts-expect-error: acorn is similar enough to estree. 1695 | onComment: comments 1696 | }) 1697 | ) 1698 | 1699 | if (addComments !== false) tree.comments = comments 1700 | 1701 | if (clean !== false) walk(tree, {leave}) 1702 | 1703 | // eslint-disable-next-line unicorn/prefer-structured-clone -- JSON casting needed to remove class stuff. 1704 | return JSON.parse(JSON.stringify(tree)) 1705 | } 1706 | 1707 | /** 1708 | * Clean a node. 1709 | * 1710 | * @param {Node} n 1711 | * ESTree node. 1712 | */ 1713 | function leave(n) { 1714 | delete n.loc 1715 | delete n.range 1716 | // @ts-expect-error: exists on acorn nodes. 1717 | delete n.start 1718 | // @ts-expect-error: exists on acorn nodes. 1719 | delete n.end 1720 | // @ts-expect-error: exists on acorn nodes. 1721 | delete n.raw 1722 | } 1723 | --------------------------------------------------------------------------------