├── .editorconfig ├── .github └── workflows │ ├── bb.yml │ └── main.yml ├── .gitignore ├── .npmrc ├── .prettierignore ├── index.js ├── lib ├── index.js └── jsx.js ├── license ├── package.json ├── readme.md ├── test └── index.js └── tsconfig.json /.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 | -------------------------------------------------------------------------------- /.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@v3 11 | - uses: actions/setup-node@v3 12 | with: 13 | node-version: ${{matrix.node}} 14 | - run: npm install 15 | - run: npm test 16 | - uses: codecov/codecov-action@v3 17 | strategy: 18 | matrix: 19 | node: 20 | - lts/gallium 21 | - node 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.d.ts 3 | *.log 4 | coverage/ 5 | node_modules/ 6 | yarn.lock 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | ignore-scripts=true 2 | package-lock=false 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | coverage/ 2 | *.md 3 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('./lib/index.js').Handler} Handler 3 | * @typedef {import('./lib/index.js').Handlers} Handlers 4 | * @typedef {import('./lib/index.js').Options} Options 5 | * @typedef {import('./lib/index.js').Result} Result 6 | * @typedef {import('./lib/index.js').State} State 7 | */ 8 | 9 | export {toJs} from './lib/index.js' 10 | export {jsx} from './lib/jsx.js' 11 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('astring').State} State 3 | * @typedef {import('estree-jsx').Node} Nodes 4 | * @typedef {import('estree-jsx').Program} Program 5 | * @typedef {typeof import('source-map').SourceMapGenerator} SourceMapGenerator 6 | * @typedef {import('source-map').RawSourceMap} Map 7 | */ 8 | 9 | /** 10 | * @typedef {Record} Generator 11 | * 12 | * @callback Handler 13 | * Handle a particular node. 14 | * @param {Generator} this 15 | * `astring` generator. 16 | * @param {any} node 17 | * Node to serialize. 18 | * @param {State} state 19 | * Info passed around. 20 | * @returns {undefined} 21 | * Nothing. 22 | * 23 | * @typedef {Partial} Handlers 24 | */ 25 | 26 | /** 27 | * @typedef {OptionsWithMaybeMapGenerator} Options 28 | * Configuration. 29 | * 30 | * @typedef OptionsFieldsBase 31 | * Base shared option fields. 32 | * @property {Handlers | null | undefined} [handlers] 33 | * Object mapping node types to functions handling the corresponding nodes. 34 | * 35 | * @typedef OptionsFieldsWithoutSourceMapGenerator 36 | * Extra option fields where there’s definitely no source map generator. 37 | * @property {null | undefined} [SourceMapGenerator] 38 | * Generate a source map by passing a `SourceMapGenerator` from `source-map` 39 | * in; this works if there is positional info on nodes. 40 | * @property {null | undefined} [filePath] 41 | * Path to input file; only used in source map. 42 | * 43 | * @typedef OptionsFieldsWithSourceMapGenerator 44 | * Extra option fields where there’s definitely a source map generator. 45 | * @property {SourceMapGenerator} SourceMapGenerator 46 | * Generate a source map by passing a `SourceMapGenerator` from `source-map` 47 | * in; this works if there is positional info on nodes. 48 | * @property {string | null | undefined} [filePath] 49 | * Path to input file; only used in source map. 50 | * 51 | * @typedef OptionsFieldsMaybeSourceMapGenerator 52 | * Extra option fields where there may or may not be a source map generator. 53 | * @property {SourceMapGenerator | null | undefined} [SourceMapGenerator] 54 | * Generate a source map by passing a `SourceMapGenerator` from `source-map` 55 | * in; this works if there is positional info on nodes. 56 | * @property {string | null | undefined} [filePath] 57 | * Path to input file; only used in source map. 58 | * 59 | * @typedef {OptionsFieldsBase & OptionsFieldsWithoutSourceMapGenerator} OptionsWithoutSourceMapGenerator 60 | * Options where there’s definitely no source map generator. 61 | * @typedef {OptionsFieldsBase & OptionsFieldsWithSourceMapGenerator} OptionsWithSourceMapGenerator 62 | * Options where there’s definitely a source map generator. 63 | * @typedef {OptionsFieldsBase & OptionsFieldsMaybeSourceMapGenerator} OptionsWithMaybeMapGenerator 64 | * Options where there may or may not be a source map generator. 65 | * 66 | * @typedef {ResultWithMaybeSourceMapGenerator} Result 67 | * Result. 68 | * 69 | * @typedef ResultFieldsBase 70 | * Base shared result fields. 71 | * @property {string} value 72 | * Serialized JavaScript. 73 | * 74 | * @typedef ResultFieldsWithoutSourceMapGenerator 75 | * Extra result fields where there’s definitely no source map generator. 76 | * @property {undefined} map 77 | * Source map as (parsed) JSON, if `SourceMapGenerator` is passed. 78 | * 79 | * @typedef ResultFieldsWithSourceMapGenerator 80 | * Extra result fields where there’s definitely a source map generator. 81 | * @property {Map} map 82 | * Source map as (parsed) JSON, if `SourceMapGenerator` is not passed. 83 | * 84 | * @typedef ResultFieldsMaybeSourceMapGenerator 85 | * Extra result fields where there may or may not be a source map generator. 86 | * @property {Map | undefined} map 87 | * Source map as (parsed) JSON, if `SourceMapGenerator` might be passed. 88 | * 89 | * @typedef {ResultFieldsBase & ResultFieldsWithoutSourceMapGenerator} ResultWithoutSourceMapGenerator 90 | * Result where there’s definitely no source map generator. 91 | * @typedef {ResultFieldsBase & ResultFieldsWithSourceMapGenerator} ResultWithSourceMapGenerator 92 | * Result where there’s definitely a source map generator. 93 | * @typedef {ResultFieldsBase & ResultFieldsMaybeSourceMapGenerator} ResultWithMaybeSourceMapGenerator 94 | * Result where there may or may not be a source map generator. 95 | */ 96 | 97 | import {GENERATOR, generate} from 'astring' 98 | 99 | /** @type {Options} */ 100 | const emptyOptions = {} 101 | 102 | /** 103 | * Serialize an estree as JavaScript. 104 | * 105 | * @overload 106 | * @param {Program} tree 107 | * @param {OptionsWithSourceMapGenerator} options 108 | * @returns {ResultWithSourceMapGenerator} 109 | * 110 | * @overload 111 | * @param {Program} tree 112 | * @param {OptionsWithMaybeMapGenerator} options 113 | * @returns {ResultWithMaybeSourceMapGenerator} 114 | * 115 | * @overload 116 | * @param {Program} tree 117 | * @param {OptionsWithoutSourceMapGenerator | null | undefined} [options] 118 | * @returns {ResultWithoutSourceMapGenerator} 119 | * 120 | * @param {Program} tree 121 | * Estree (esast). 122 | * @param {Options | null | undefined} [options] 123 | * Configuration (optional). 124 | * @returns {Result} 125 | * Result, optionally with source map. 126 | */ 127 | export function toJs(tree, options) { 128 | const {SourceMapGenerator, filePath, handlers} = options || emptyOptions 129 | const sourceMap = SourceMapGenerator 130 | ? new SourceMapGenerator({file: filePath || '.js'}) 131 | : undefined 132 | 133 | const value = generate( 134 | tree, 135 | // @ts-expect-error: `sourceMap` can be undefined, `astring` types are buggy. 136 | { 137 | comments: true, 138 | generator: {...GENERATOR, ...handlers}, 139 | sourceMap: sourceMap || undefined 140 | } 141 | ) 142 | const map = sourceMap ? sourceMap.toJSON() : undefined 143 | 144 | return {value, map} 145 | } 146 | -------------------------------------------------------------------------------- /lib/jsx.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('estree-jsx').JSXAttribute} JsxAttribute 3 | * @typedef {import('estree-jsx').JSXClosingElement} JsxClosingElement 4 | * @typedef {import('estree-jsx').JSXClosingFragment} JsxClosingFragment 5 | * @typedef {import('estree-jsx').JSXElement} JsxElement 6 | * @typedef {import('estree-jsx').JSXExpressionContainer} JsxExpressionContainer 7 | * @typedef {import('estree-jsx').JSXFragment} JsxFragment 8 | * @typedef {import('estree-jsx').JSXIdentifier} JsxIdentifier 9 | * @typedef {import('estree-jsx').JSXMemberExpression} JsxMemberExpression 10 | * @typedef {import('estree-jsx').JSXNamespacedName} JsxNamespacedName 11 | * @typedef {import('estree-jsx').JSXOpeningElement} JsxOpeningElement 12 | * @typedef {import('estree-jsx').JSXOpeningFragment} JsxOpeningFragment 13 | * @typedef {import('estree-jsx').JSXSpreadAttribute} JsxSpreadAttribute 14 | * @typedef {import('estree-jsx').JSXText} JsxText 15 | * 16 | * @typedef {import('./index.js').Generator} Generator 17 | * @typedef {import('./index.js').State} State 18 | */ 19 | 20 | export const jsx = { 21 | JSXAttribute: jsxAttribute, 22 | JSXClosingElement: jsxClosingElement, 23 | JSXClosingFragment: jsxClosingFragment, 24 | JSXElement: jsxElement, 25 | JSXEmptyExpression: jsxEmptyExpression, 26 | JSXExpressionContainer: jsxExpressionContainer, 27 | JSXFragment: jsxFragment, 28 | JSXIdentifier: jsxIdentifier, 29 | JSXMemberExpression: jsxMemberExpression, 30 | JSXNamespacedName: jsxNamespacedName, 31 | JSXOpeningElement: jsxOpeningElement, 32 | JSXOpeningFragment: jsxOpeningFragment, 33 | JSXSpreadAttribute: jsxSpreadAttribute, 34 | JSXText: jsxText 35 | } 36 | 37 | /** 38 | * `attr` 39 | * `attr="something"` 40 | * `attr={1}` 41 | * 42 | * @this {Generator} 43 | * `astring` generator. 44 | * @param {JsxAttribute} node 45 | * Node to serialize. 46 | * @param {State} state 47 | * Info passed around. 48 | * @returns {undefined} 49 | * Nothing. 50 | */ 51 | function jsxAttribute(node, state) { 52 | this[node.name.type](node.name, state) 53 | 54 | if (node.value !== null && node.value !== undefined) { 55 | state.write('=') 56 | 57 | // Encode double quotes in attribute values. 58 | if (node.value.type === 'Literal') { 59 | state.write( 60 | '"' + encodeJsx(String(node.value.value)).replace(/"/g, '"') + '"', 61 | node 62 | ) 63 | } else { 64 | this[node.value.type](node.value, state) 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * `` 71 | * 72 | * @this {Generator} 73 | * `astring` generator. 74 | * @param {JsxClosingElement} node 75 | * Node to serialize. 76 | * @param {State} state 77 | * Info passed around. 78 | * @returns {undefined} 79 | * Nothing. 80 | */ 81 | function jsxClosingElement(node, state) { 82 | state.write('') 85 | } 86 | 87 | /** 88 | * `` 89 | * 90 | * @this {Generator} 91 | * `astring` generator. 92 | * @param {JsxClosingFragment} node 93 | * Node to serialize. 94 | * @param {State} state 95 | * Info passed around. 96 | * @returns {undefined} 97 | * Nothing. 98 | */ 99 | function jsxClosingFragment(node, state) { 100 | state.write('', node) 101 | } 102 | 103 | /** 104 | * `
` 105 | * `
` 106 | * 107 | * @this {Generator} 108 | * `astring` generator. 109 | * @param {JsxElement} node 110 | * Node to serialize. 111 | * @param {State} state 112 | * Info passed around. 113 | * @returns {undefined} 114 | * Nothing. 115 | */ 116 | function jsxElement(node, state) { 117 | let index = -1 118 | 119 | this[node.openingElement.type](node.openingElement, state) 120 | 121 | if (node.children) { 122 | while (++index < node.children.length) { 123 | const child = node.children[index] 124 | 125 | // Supported in types but not by Acorn. 126 | /* c8 ignore next 3 */ 127 | if (child.type === 'JSXSpreadChild') { 128 | throw new Error('JSX spread children are not supported') 129 | } 130 | 131 | this[child.type](child, state) 132 | } 133 | } 134 | 135 | if (node.closingElement) { 136 | this[node.closingElement.type](node.closingElement, state) 137 | } 138 | } 139 | 140 | /** 141 | * `{}` (always in a `JSXExpressionContainer`, which does the curlies) 142 | * 143 | * @this {Generator} 144 | * `astring` generator. 145 | * @returns {undefined} 146 | * Nothing. 147 | */ 148 | function jsxEmptyExpression() {} 149 | 150 | /** 151 | * `{expression}` 152 | * 153 | * @this {Generator} 154 | * `astring` generator. 155 | * @param {JsxExpressionContainer} node 156 | * Node to serialize. 157 | * @param {State} state 158 | * Info passed around. 159 | * @returns {undefined} 160 | * Nothing. 161 | */ 162 | function jsxExpressionContainer(node, state) { 163 | state.write('{') 164 | this[node.expression.type](node.expression, state) 165 | state.write('}') 166 | } 167 | 168 | /** 169 | * `<>` 170 | * 171 | * @this {Generator} 172 | * `astring` generator. 173 | * @param {JsxFragment} node 174 | * Node to serialize. 175 | * @param {State} state 176 | * Info passed around. 177 | * @returns {undefined} 178 | * Nothing. 179 | */ 180 | function jsxFragment(node, state) { 181 | let index = -1 182 | 183 | this[node.openingFragment.type](node.openingFragment, state) 184 | 185 | if (node.children) { 186 | while (++index < node.children.length) { 187 | const child = node.children[index] 188 | 189 | // Supported in types but not by Acorn. 190 | /* c8 ignore next 3 */ 191 | if (child.type === 'JSXSpreadChild') { 192 | throw new Error('JSX spread children are not supported') 193 | } 194 | 195 | this[child.type](child, state) 196 | } 197 | } 198 | 199 | this[node.closingFragment.type](node.closingFragment, state) 200 | } 201 | 202 | /** 203 | * `div` 204 | * 205 | * @this {Generator} 206 | * `astring` generator. 207 | * @param {JsxIdentifier} node 208 | * Node to serialize. 209 | * @param {State} state 210 | * Info passed around. 211 | * @returns {undefined} 212 | * Nothing. 213 | */ 214 | function jsxIdentifier(node, state) { 215 | state.write(node.name, node) 216 | } 217 | 218 | /** 219 | * `member.expression` 220 | * 221 | * @this {Generator} 222 | * `astring` generator. 223 | * @param {JsxMemberExpression} node 224 | * Node to serialize. 225 | * @param {State} state 226 | * Info passed around. 227 | * @returns {undefined} 228 | * Nothing. 229 | */ 230 | function jsxMemberExpression(node, state) { 231 | this[node.object.type](node.object, state) 232 | state.write('.') 233 | this[node.property.type](node.property, state) 234 | } 235 | 236 | /** 237 | * `ns:name` 238 | * 239 | * @this {Generator} 240 | * `astring` generator. 241 | * @param {JsxNamespacedName} node 242 | * Node to serialize. 243 | * @param {State} state 244 | * Info passed around. 245 | * @returns {undefined} 246 | * Nothing. 247 | */ 248 | function jsxNamespacedName(node, state) { 249 | this[node.namespace.type](node.namespace, state) 250 | state.write(':') 251 | this[node.name.type](node.name, state) 252 | } 253 | 254 | /** 255 | * `
` 256 | * 257 | * @this {Generator} 258 | * `astring` generator. 259 | * @param {JsxOpeningElement} node 260 | * Node to serialize. 261 | * @param {State} state 262 | * Info passed around. 263 | * @returns {undefined} 264 | * Nothing. 265 | */ 266 | function jsxOpeningElement(node, state) { 267 | let index = -1 268 | 269 | state.write('<') 270 | this[node.name.type](node.name, state) 271 | 272 | if (node.attributes) { 273 | while (++index < node.attributes.length) { 274 | state.write(' ') 275 | this[node.attributes[index].type](node.attributes[index], state) 276 | } 277 | } 278 | 279 | state.write(node.selfClosing ? ' />' : '>') 280 | } 281 | 282 | /** 283 | * `<>` 284 | * 285 | * @this {Generator} 286 | * `astring` generator. 287 | * @param {JsxOpeningFragment} node 288 | * Node to serialize. 289 | * @param {State} state 290 | * Info passed around. 291 | * @returns {undefined} 292 | * Nothing. 293 | */ 294 | function jsxOpeningFragment(node, state) { 295 | state.write('<>', node) 296 | } 297 | 298 | /** 299 | * `{...argument}` 300 | * 301 | * @this {Generator} 302 | * `astring` generator. 303 | * @param {JsxSpreadAttribute} node 304 | * Node to serialize. 305 | * @param {State} state 306 | * Info passed around. 307 | * @returns {undefined} 308 | * Nothing. 309 | */ 310 | function jsxSpreadAttribute(node, state) { 311 | state.write('{') 312 | // eslint-disable-next-line new-cap 313 | this.SpreadElement(node, state) 314 | state.write('}') 315 | } 316 | 317 | /** 318 | * `!` 319 | * 320 | * @this {Generator} 321 | * `astring` generator. 322 | * @param {JsxText} node 323 | * Node to serialize. 324 | * @param {State} state 325 | * Info passed around. 326 | * @returns {undefined} 327 | * Nothing. 328 | */ 329 | function jsxText(node, state) { 330 | state.write(encodeJsx(node.value).replace(/[<>{}]/g, replaceJsxChar), node) 331 | } 332 | 333 | /** 334 | * Make sure that character references don’t pop up. 335 | * 336 | * For example, the text `©` should stay that way, and not turn into `©`. 337 | * We could encode all `&` (easy but verbose) or look for actual valid 338 | * references (complex but cleanest output). 339 | * Looking for the 2nd character gives us a middle ground. 340 | * The `#` is for (decimal and hexadecimal) numeric references, the letters 341 | * are for the named references. 342 | * 343 | * @param {string} value 344 | * Value to encode. 345 | * @returns {string} 346 | * Encoded value. 347 | */ 348 | function encodeJsx(value) { 349 | return value.replace(/&(?=[#a-z])/gi, '&') 350 | } 351 | 352 | /** 353 | * @param {string} $0 354 | * @returns {string} 355 | */ 356 | function replaceJsxChar($0) { 357 | return $0 === '<' 358 | ? '<' 359 | : $0 === '>' 360 | ? '>' 361 | : $0 === '{' 362 | ? '{' 363 | : '}' 364 | } 365 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2022 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-to-js", 3 | "version": "2.0.0", 4 | "description": "estree (and esast) utility to serialize to JavaScript", 5 | "license": "MIT", 6 | "keywords": [ 7 | "unist", 8 | "estree", 9 | "estree-util", 10 | "esast", 11 | "esast-util", 12 | "util", 13 | "utility", 14 | "js", 15 | "serialize", 16 | "stringify", 17 | "tostring", 18 | "astring" 19 | ], 20 | "repository": "syntax-tree/estree-util-to-js", 21 | "bugs": "https://github.com/syntax-tree/estree-util-to-js/issues", 22 | "funding": { 23 | "type": "opencollective", 24 | "url": "https://opencollective.com/unified" 25 | }, 26 | "author": "Titus Wormer (https://wooorm.com)", 27 | "contributors": [ 28 | "Titus Wormer (https://wooorm.com)" 29 | ], 30 | "sideEffects": false, 31 | "type": "module", 32 | "exports": "./index.js", 33 | "files": [ 34 | "lib/", 35 | "index.d.ts", 36 | "index.js" 37 | ], 38 | "dependencies": { 39 | "@types/estree-jsx": "^1.0.0", 40 | "astring": "^1.8.0", 41 | "source-map": "^0.7.0" 42 | }, 43 | "devDependencies": { 44 | "@types/node": "^20.0.0", 45 | "c8": "^8.0.0", 46 | "prettier": "^3.0.0", 47 | "remark-cli": "^11.0.0", 48 | "remark-preset-wooorm": "^9.0.0", 49 | "type-coverage": "^2.0.0", 50 | "typescript": "^5.0.0", 51 | "xo": "^0.55.0" 52 | }, 53 | "scripts": { 54 | "prepack": "npm run build && npm run format", 55 | "build": "tsc --build --clean && tsc --build && type-coverage", 56 | "format": "remark . -qfo && prettier . -w --log-level warn && xo --fix", 57 | "test-api": "node --conditions development test/index.js", 58 | "test-coverage": "c8 --100 --reporter lcov npm run test-api", 59 | "test": "npm run build && npm run format && npm run test-coverage" 60 | }, 61 | "prettier": { 62 | "bracketSpacing": false, 63 | "semi": false, 64 | "singleQuote": true, 65 | "tabWidth": 2, 66 | "trailingComma": "none", 67 | "useTabs": false 68 | }, 69 | "remarkConfig": { 70 | "plugins": [ 71 | "remark-preset-wooorm" 72 | ] 73 | }, 74 | "typeCoverage": { 75 | "atLeast": 100, 76 | "detail": true, 77 | "ignoreCatch": true, 78 | "#": "needed `any`s", 79 | "ignoreFiles": [ 80 | "lib/index.d.ts" 81 | ], 82 | "strict": true 83 | }, 84 | "xo": { 85 | "prettier": true, 86 | "rules": { 87 | "unicorn/prefer-string-replace-all": "off" 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # estree-util-to-js 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][] (and [esast][]) utility to serialize estrees as JavaScript. 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 | * [`toJs(tree[, options])`](#tojstree-options) 21 | * [`jsx`](#jsx) 22 | * [`Handler`](#handler) 23 | * [`Handlers`](#handlers) 24 | * [`Map`](#map) 25 | * [`Options`](#options) 26 | * [`Result`](#result) 27 | * [`State`](#state) 28 | * [Examples](#examples) 29 | * [Example: source maps](#example-source-maps) 30 | * [Example: comments](#example-comments) 31 | * [Example: JSX](#example-jsx) 32 | * [Types](#types) 33 | * [Compatibility](#compatibility) 34 | * [Contribute](#contribute) 35 | * [License](#license) 36 | 37 | ## What is this? 38 | 39 | This package is a utility that turns an estree syntax tree into a string of 40 | JavaScript. 41 | 42 | ## When should I use this? 43 | 44 | You can use this utility when you want to get the serialized JavaScript that is 45 | represented by the syntax tree, either because you’re done with the syntax tree, 46 | or because you’re integrating with another tool that does not support syntax 47 | trees. 48 | 49 | This utility is particularly useful when integrating with other unified tools, 50 | such as unist and vfile. 51 | 52 | The utility [`esast-util-from-js`][esast-util-from-js] does the inverse of this 53 | utility. 54 | It turns JS into esast. 55 | 56 | ## Install 57 | 58 | This package is [ESM only][esm]. 59 | In Node.js (version 16+), install with [npm][]: 60 | 61 | ```sh 62 | npm install estree-util-to-js 63 | ``` 64 | 65 | In Deno with [`esm.sh`][esmsh]: 66 | 67 | ```js 68 | import {toJs} from 'https://esm.sh/estree-util-to-js@2' 69 | ``` 70 | 71 | In browsers with [`esm.sh`][esmsh]: 72 | 73 | ```html 74 | 77 | ``` 78 | 79 | ## Use 80 | 81 | ```js 82 | import fs from 'node:fs/promises' 83 | import {parse} from 'acorn' 84 | import {toJs} from 'estree-util-to-js' 85 | 86 | const file = String(await fs.readFile('index.js')) 87 | 88 | const tree = parse(file, {ecmaVersion: 2022, sourceType: 'module', locations: true}) 89 | 90 | // @ts-expect-error: acorn is funky but it works fine. 91 | console.log(toJs(tree)) 92 | ``` 93 | 94 | Yields: 95 | 96 | ```js 97 | { 98 | value: "export {toJs} from './lib/index.js';\nexport {jsx} from './lib/jsx.js';\n", 99 | map: undefined 100 | } 101 | ``` 102 | 103 | ## API 104 | 105 | This package exports the identifiers [`jsx`][api-jsx] and [`toJs`][api-to-js]. 106 | There is no default export. 107 | 108 | ### `toJs(tree[, options])` 109 | 110 | Serialize an estree as JavaScript. 111 | 112 | ###### Parameters 113 | 114 | * `tree` ([`Program`][program]) 115 | — estree 116 | * `options` ([`Options`][api-options]) 117 | — configuration 118 | 119 | ###### Returns 120 | 121 | Result, optionally with source map ([`Result`][api-result]). 122 | 123 | ### `jsx` 124 | 125 | Map of handlers to handle the nodes of JSX extensions in JavaScript 126 | ([`Handlers`][api-handlers]). 127 | 128 | ### `Handler` 129 | 130 | Handle a particular node (TypeScript type). 131 | 132 | ###### Parameters 133 | 134 | * `this` (`Generator`) 135 | — `astring` generator 136 | * `node` ([`Node`][node]) 137 | — node to serialize 138 | * `state` ([`State`][api-state]) 139 | — info passed around 140 | 141 | ###### Returns 142 | 143 | Nothing (`undefined`). 144 | 145 | ### `Handlers` 146 | 147 | Handlers of nodes (TypeScript type). 148 | 149 | ###### Type 150 | 151 | ```ts 152 | type Handlers = Partial> 153 | ``` 154 | 155 | ### `Map` 156 | 157 | Raw source map from `source-map` (TypeScript type). 158 | 159 | ### `Options` 160 | 161 | Configuration (TypeScript type). 162 | 163 | ###### Fields 164 | 165 | * `SourceMapGenerator` ([`SourceMapGenerator`][source-map]) 166 | — generate a source map with this class 167 | * `filePath` (`string`) 168 | — path to original input file 169 | * `handlers` ([`Handlers`][api-handlers]) 170 | — extra handlers 171 | 172 | ### `Result` 173 | 174 | Result (TypeScript type). 175 | 176 | ###### Fields 177 | 178 | * `value` (`string`) 179 | — serialized JavaScript 180 | * `map` ([`Map`][api-map] or `undefined`) 181 | — source map as (parsed) JSON 182 | 183 | ### `State` 184 | 185 | State from `astring` (TypeScript type). 186 | 187 | ## Examples 188 | 189 | ### Example: source maps 190 | 191 | Source maps are supported when passing the `SourceMapGenerator` class from 192 | [`source-map`][source-map]. 193 | You should also pass `filePath`. 194 | Modified example from § Use above: 195 | 196 | ```diff 197 | import fs from 'node:fs/promises' 198 | import {parse} from 'acorn' 199 | +import {SourceMapGenerator} from 'source-map' 200 | import {toJs} from 'estree-util-to-js' 201 | 202 | -const file = String(await fs.readFile('index.js')) 203 | +const filePath = 'index.js' 204 | +const file = String(await fs.readFile(filePath)) 205 | 206 | const tree = parse(file, { 207 | ecmaVersion: 2022, 208 | @@ -11,4 +13,4 @@ const tree = parse(file, { 209 | }) 210 | 211 | // @ts-expect-error: acorn is funky but it works fine. 212 | -console.log(toJs(tree)) 213 | +console.log(toJs(tree, {filePath, SourceMapGenerator})) 214 | ``` 215 | 216 | Yields: 217 | 218 | ```js 219 | { 220 | value: "export {toJs} from './lib/index.js';\nexport {jsx} from './lib/jsx.js';\n", 221 | map: { 222 | version: 3, 223 | sources: [ 'index.js' ], 224 | names: [], 225 | mappings: 'QAOQ,WAAW;QACX,UAAU', 226 | file: 'index.js' 227 | } 228 | } 229 | ``` 230 | 231 | ### Example: comments 232 | 233 | To get comments to work, they have to be inside the tree. 234 | This is not done by Acorn. 235 | [`estree-util-attach-comments`][estree-util-attach-comments] can do that. 236 | Modified example from § Use above: 237 | 238 | ```diff 239 | import fs from 'node:fs/promises' 240 | import {parse} from 'acorn' 241 | +import {attachComments} from 'estree-util-attach-comments' 242 | import {toJs} from 'estree-util-to-js' 243 | 244 | const file = String(await fs.readFile('index.js')) 245 | 246 | +/** @type {Array} */ 247 | +const comments = [] 248 | const tree = parse(file, { 249 | ecmaVersion: 2022, 250 | sourceType: 'module', 251 | - locations: true 252 | + locations: true, 253 | + // @ts-expect-error: acorn is funky these comments are fine. 254 | + onComment: comments 255 | }) 256 | +attachComments(tree, comments) 257 | 258 | // @ts-expect-error: acorn is funky but it works fine. 259 | console.log(toJs(tree)) 260 | ``` 261 | 262 | Yields: 263 | 264 | ```js 265 | { 266 | value: '/**\n' + 267 | "* @typedef {import('./lib/index.js').Options} Options\n" + 268 | "* @typedef {import('./lib/types.js').Handler} Handler\n" + 269 | "* @typedef {import('./lib/types.js').Handlers} Handlers\n" + 270 | "* @typedef {import('./lib/types.js').State} State\n" + 271 | '*/\n' + 272 | "export {toJs} from './lib/index.js';\n" + 273 | "export {jsx} from './lib/jsx.js';\n", 274 | map: undefined 275 | } 276 | ``` 277 | 278 | ### Example: JSX 279 | 280 | To get JSX to work, handlers need to be registered. 281 | This is not done by default, but they are exported as `jsx` and can be passed. 282 | Modified example from § Use above: 283 | 284 | ```diff 285 | import fs from 'node:fs/promises' 286 | -import {parse} from 'acorn' 287 | -import {toJs} from 'estree-util-to-js' 288 | +import {Parser} from 'acorn' 289 | +import acornJsx from 'acorn-jsx' 290 | +import {jsx, toJs} from 'estree-util-to-js' 291 | 292 | -const file = String(await fs.readFile('index.js')) 293 | +const file = '<>{1 + 1}' 294 | 295 | -const tree = parse(file, { 296 | +const tree = Parser.extend(acornJsx()).parse(file, { 297 | ecmaVersion: 2022, 298 | sourceType: 'module', 299 | locations: true 300 | }) 301 | 302 | // @ts-expect-error: acorn is funky but it works fine. 303 | -console.log(toJs(tree)) 304 | +console.log(toJs(tree, {handlers: jsx})) 305 | ``` 306 | 307 | Yields: 308 | 309 | ```js 310 | { value: '<>{1 + 1};\n', map: undefined } 311 | ``` 312 | 313 | ## Types 314 | 315 | This package is fully typed with [TypeScript][]. 316 | It exports the additional types [`Handler`][api-handler], 317 | [`Handlers`][api-handlers], 318 | [`Map`][api-map], 319 | [`Options`][api-options], 320 | [`Result`][api-result], and 321 | [`State`][api-state]. 322 | 323 | ## Compatibility 324 | 325 | Projects maintained by the unified collective are compatible with maintained 326 | versions of Node.js. 327 | 328 | When we cut a new major release, we drop support for unmaintained versions of 329 | Node. 330 | This means we try to keep the current release line, `estree-util-to-js@^2`, 331 | compatible with Node.js 16. 332 | 333 | ## Contribute 334 | 335 | See [`contributing.md`][contributing] in [`syntax-tree/.github`][health] for 336 | ways to get started. 337 | See [`support.md`][support] for ways to get help. 338 | 339 | This project has a [code of conduct][coc]. 340 | By interacting with this repository, organization, or community you agree to 341 | abide by its terms. 342 | 343 | ## License 344 | 345 | [MIT][license] © [Titus Wormer][author] 346 | 347 | 348 | 349 | [build-badge]: https://github.com/syntax-tree/estree-util-to-js/workflows/main/badge.svg 350 | 351 | [build]: https://github.com/syntax-tree/estree-util-to-js/actions 352 | 353 | [coverage-badge]: https://img.shields.io/codecov/c/github/syntax-tree/estree-util-to-js.svg 354 | 355 | [coverage]: https://codecov.io/github/syntax-tree/estree-util-to-js 356 | 357 | [downloads-badge]: https://img.shields.io/npm/dm/estree-util-to-js.svg 358 | 359 | [downloads]: https://www.npmjs.com/package/estree-util-to-js 360 | 361 | [size-badge]: https://img.shields.io/badge/dynamic/json?label=minzipped%20size&query=$.size.compressedSize&url=https://deno.bundlejs.com/?q=estree-util-to-js 362 | 363 | [size]: https://bundlejs.com/?q=estree-util-to-js 364 | 365 | [sponsors-badge]: https://opencollective.com/unified/sponsors/badge.svg 366 | 367 | [backers-badge]: https://opencollective.com/unified/backers/badge.svg 368 | 369 | [collective]: https://opencollective.com/unified 370 | 371 | [chat-badge]: https://img.shields.io/badge/chat-discussions-success.svg 372 | 373 | [chat]: https://github.com/syntax-tree/unist/discussions 374 | 375 | [npm]: https://docs.npmjs.com/cli/install 376 | 377 | [esm]: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c 378 | 379 | [esmsh]: https://esm.sh 380 | 381 | [typescript]: https://www.typescriptlang.org 382 | 383 | [license]: license 384 | 385 | [author]: https://wooorm.com 386 | 387 | [health]: https://github.com/syntax-tree/.github 388 | 389 | [contributing]: https://github.com/syntax-tree/.github/blob/main/contributing.md 390 | 391 | [support]: https://github.com/syntax-tree/.github/blob/main/support.md 392 | 393 | [coc]: https://github.com/syntax-tree/.github/blob/main/code-of-conduct.md 394 | 395 | [esast]: https://github.com/syntax-tree/esast 396 | 397 | [esast-util-from-js]: https://github.com/syntax-tree/esast-util-from-js 398 | 399 | [estree]: https://github.com/estree/estree 400 | 401 | [estree-util-attach-comments]: https://github.com/syntax-tree/estree-util-attach-comments 402 | 403 | [program]: https://github.com/estree/estree/blob/master/es2015.md#programs 404 | 405 | [node]: https://github.com/estree/estree/blob/master/es5.md#node-objects 406 | 407 | [source-map]: https://github.com/mozilla/source-map 408 | 409 | [api-jsx]: #jsx 410 | 411 | [api-to-js]: #tojstree-options 412 | 413 | [api-handler]: #handler 414 | 415 | [api-handlers]: #handlers 416 | 417 | [api-map]: #map 418 | 419 | [api-options]: #options 420 | 421 | [api-state]: #state 422 | 423 | [api-result]: #result 424 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @typedef {import('estree-jsx').Program} Program 3 | */ 4 | 5 | import assert from 'node:assert/strict' 6 | import test from 'node:test' 7 | import {Parser} from 'acorn' 8 | import acornJsx from 'acorn-jsx' 9 | import {jsx, toJs} from 'estree-util-to-js' 10 | import {SourceMapGenerator} from 'source-map' 11 | 12 | test('toJs', async function (t) { 13 | await t.test('should expose the public api', async function () { 14 | assert.deepEqual(Object.keys(await import('estree-util-to-js')).sort(), [ 15 | 'jsx', 16 | 'toJs' 17 | ]) 18 | }) 19 | 20 | await t.test('should serialize js', async function () { 21 | assert.deepEqual(toJs(fromJs('const a = 1')), { 22 | value: 'const a = 1;\n', 23 | map: undefined 24 | }) 25 | }) 26 | 27 | await t.test('should serialize js w/ a source map', async function () { 28 | assert.deepEqual(toJs(fromJs('const a = 1'), {SourceMapGenerator}), { 29 | value: 'const a = 1;\n', 30 | map: { 31 | version: 3, 32 | sources: ['.js'], 33 | names: ['a'], 34 | mappings: 'MAAMA,IAAI', 35 | file: '.js' 36 | } 37 | }) 38 | }) 39 | 40 | await t.test( 41 | 'should serialize js w/ a source map and a file path', 42 | async function () { 43 | assert.deepEqual( 44 | toJs(fromJs('const a = 1'), { 45 | SourceMapGenerator, 46 | filePath: 'example.js' 47 | }), 48 | { 49 | value: 'const a = 1;\n', 50 | map: { 51 | version: 3, 52 | sources: ['example.js'], 53 | names: ['a'], 54 | mappings: 'MAAMA,IAAI', 55 | file: 'example.js' 56 | } 57 | } 58 | ) 59 | } 60 | ) 61 | 62 | await t.test( 63 | 'should supports jsx (opening and closing tag)', 64 | async function () { 65 | assert.equal( 66 | toJs(fromJs('1', true), {handlers: jsx}).value, 67 | '1;\n' 68 | ) 69 | } 70 | ) 71 | 72 | await t.test( 73 | 'should supports jsx (opening and closing fragment)', 74 | async function () { 75 | assert.equal( 76 | toJs(fromJs('<>1', true), {handlers: jsx}).value, 77 | '<>1;\n' 78 | ) 79 | } 80 | ) 81 | 82 | await t.test('should supports jsx (member name)', async function () { 83 | assert.equal( 84 | toJs(fromJs('', true), {handlers: jsx}).value, 85 | ';\n' 86 | ) 87 | }) 88 | 89 | await t.test('should supports jsx (namespaced name)', async function () { 90 | assert.equal( 91 | toJs(fromJs('', true), {handlers: jsx}).value, 92 | ';\n' 93 | ) 94 | }) 95 | 96 | await t.test('should supports jsx (attributes)', async function () { 97 | assert.equal( 98 | toJs(fromJs('', true), {handlers: jsx}).value, 99 | ';\n' 100 | ) 101 | }) 102 | 103 | await t.test('should supports jsx (namespaced attribute)', async function () { 104 | assert.equal( 105 | toJs(fromJs('', true), {handlers: jsx}).value, 106 | ';\n' 107 | ) 108 | }) 109 | 110 | await t.test('should supports jsx (expressions)', async function () { 111 | assert.equal( 112 | toJs(fromJs('empty: {}, comment: {/*b*/}, value: {1}', true), { 113 | handlers: jsx 114 | }).value, 115 | 'empty: {}, comment: {}, value: {1};\n' 116 | ) 117 | }) 118 | 119 | await t.test('should supports jsx (text)', async function () { 120 | assert.equal( 121 | toJs(fromJs('1 < 2 > 3 { 4 } 5', true), { 122 | handlers: jsx 123 | }).value, 124 | '1 < 2 > 3 { 4 } 5;\n' 125 | ) 126 | }) 127 | }) 128 | 129 | /** 130 | * @param {string} value 131 | * JavaScript. 132 | * @param {boolean | null | undefined} [jsx=false] 133 | * Whether to parse as JSX (default: `false`). 134 | * @returns {Program} 135 | * ESTree program. 136 | */ 137 | function fromJs(value, jsx) { 138 | const parser = jsx ? Parser.extend(acornJsx()) : Parser 139 | // @ts-expect-error: fine. 140 | return parser.parse(value, { 141 | ecmaVersion: 2022, 142 | sourceType: 'module', 143 | locations: true 144 | }) 145 | } 146 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "checkJs": true, 4 | "customConditions": ["development"], 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "exactOptionalPropertyTypes": true, 8 | "lib": ["es2022"], 9 | "module": "node16", 10 | "strict": true, 11 | "target": "es2022" 12 | }, 13 | "exclude": ["coverage/", "node_modules/"], 14 | "include": ["**/*.js"] 15 | } 16 | --------------------------------------------------------------------------------