├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── pnpm-lock.yaml ├── src ├── index.js ├── print │ ├── handlers.js │ └── index.js └── utils │ ├── comments.js │ ├── id.js │ └── push_array.js ├── test ├── samples │ ├── array-expressions │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── arrow-function-as-statement │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── arrow-function-assignment-object-pattern │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── arrow-function-parenthesized │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── at-prefix │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── await-precedence │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── basic │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── bigint │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── break-continue │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── call-expressions │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── chain-expressions │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── class-private │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── class-property │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── class-static-block │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── comment-block-with-sigil │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── comment-block │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── comment-inline-inserted │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── comment-inline │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── comment-interpolated │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── comment-mixed-trailing │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── comment-within-call-expression │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── comment-within-parentheses │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── deconflict-let │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── deconflict-method │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── destructured-declaration │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── empty-body │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── export │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── function-declaration │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── hash-prefix-arrow │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── hash-prefix-for-loop-head │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── hash-prefix-reused │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── hash-prefix │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── import-as │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── import-default-and-named │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── import-many │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── import │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── inserted-parameter │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── inserted-parameters │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── logical-expression │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── meta-property │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── method │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── nested-blocks-b │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── nested-blocks │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── object-expressions │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── parenthesized-expression │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── regex │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── removes-parens │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── sourcemap │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── string-literal │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── switch │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── tagged-template │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── try-catch │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── var-declaration │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ ├── with │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js │ └── yield │ │ ├── expected.js │ │ ├── expected.js.map │ │ └── input.js └── test.js └── tsconfig.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | 9 | # cancel in-progress runs on new commits to same PR (github.event.number) 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.event.number || github.sha }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | Tests: 16 | runs-on: ${{ matrix.os }} 17 | timeout-minutes: 30 18 | strategy: 19 | fail-fast: false 20 | matrix: 21 | node-version: [16] 22 | os: [ubuntu-latest] 23 | steps: 24 | - run: git config --global core.autocrlf false 25 | - uses: actions/checkout@v3 26 | - uses: pnpm/action-setup@v2.2.2 27 | - uses: actions/setup-node@v3 28 | with: 29 | node-version: ${{ matrix.node-version }} 30 | cache: pnpm 31 | - run: pnpm install --frozen-lockfile 32 | - run: pnpm test 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /types 4 | dist 5 | test/**/_actual.* 6 | test/fuzz -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none" 5 | } 6 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # code-red changelog 2 | 3 | ## 1.0.4 4 | 5 | - Add `types` to `pkg.exports` ([#83](https://github.com/Rich-Harris/code-red/pull/83)) 6 | 7 | ## 1.0.3 8 | 9 | - Fix inline comments in function arguments ([#79](https://github.com/Rich-Harris/code-red/pull/79)) 10 | 11 | ## 1.0.2 12 | 13 | - Use `dts-buddy` to generate type declarations ([#78](https://github.com/Rich-Harris/code-red/pull/78)) 14 | 15 | ## 1.0.1 16 | 17 | - Remove `dist` directory from package ([#77](https://github.com/Rich-Harris/code-red/pull/77)) 18 | - Fix position of inline comments ([#76](https://github.com/Rich-Harris/code-red/pull/76)) 19 | 20 | ## 1.0.0 21 | 22 | - Breaking: remove CJS build, remove `main` and `module` ([#75](https://github.com/Rich-Harris/code-red/pull/75)) 23 | 24 | ## 0.2.7 25 | 26 | - Update to `es2022` ([#73](https://github.com/Rich-Harris/code-red/pull/73)) 27 | 28 | ## 0.2.6 29 | 30 | - Replace `sourcemap-codec` with `@jridgewell/sourcemap-codec` ([#74](https://github.com/Rich-Harris/code-red/pull/74)) 31 | 32 | ## 0.2.5 33 | 34 | - Prevent stack overflow with very large ASTs ([#71](https://github.com/Rich-Harris/code-red/pull/71)) 35 | 36 | ## 0.2.4 37 | 38 | - Fix output for arrow functions where body is an object destructuring assignment ([#70](https://github.com/Rich-Harris/code-red/pull/70)) 39 | 40 | ## 0.2.3 41 | 42 | - Fix output with comments within parenthesized `return` statement ([#36](https://github.com/Rich-Harris/code-red/issues/36)) 43 | - Fix output for identifier at root of AST ([#37](https://github.com/Rich-Harris/code-red/issues/37)) 44 | - Fix output for statements with empty bodies ([#65](https://github.com/Rich-Harris/code-red/issues/65)) 45 | 46 | ## 0.2.2 47 | 48 | - Update dependencies ([#63](https://github.com/Rich-Harris/code-red/pull/63)) 49 | 50 | ## 0.2.1 51 | 52 | - Fix handling of string literal raw values ([#61](https://github.com/Rich-Harris/code-red/pull/61)) 53 | 54 | ## 0.2.0 55 | 56 | - Rewrite in JavaScript 57 | 58 | ## 0.1.7 59 | 60 | - Include dependencies ([#56](https://github.com/Rich-Harris/code-red/pull/56)) 61 | - Add `sourceMapEncodeMappings` option ([#51](https://github.com/Rich-Harris/code-red/pull/51)) 62 | 63 | ## 0.1.6 64 | 65 | - Only use shorthand for non-computed properties ([#58](https://github.com/Rich-Harris/code-red/pull/58)) 66 | 67 | ## 0.1.5 68 | 69 | - Use `node.raw` where possible ([#55](https://github.com/Rich-Harris/code-red/pull/55)) 70 | - Support BigInt (([#54](https://github.com/Rich-Harris/code-red/issues/54))) 71 | 72 | ## 0.1.4 73 | 74 | - Fix rendering of nullish coalescing operator alongside other logical operators ([#52](https://github.com/Rich-Harris/code-red/issues/52)) 75 | 76 | ## 0.1.3 77 | 78 | - Support nullish coalescing operator ([#42](https://github.com/Rich-Harris/code-red/issues/42)) 79 | - Support optional chaining ([#43](https://github.com/Rich-Harris/code-red/issues/43)) 80 | 81 | ## 0.1.2 82 | 83 | - Don't crash when using an arrow function as a statement ([#38](https://github.com/Rich-Harris/code-red/issues/38)) 84 | 85 | ## 0.1.1 86 | 87 | - Wrap arrow functions in parens as appropriate ([#31](https://github.com/Rich-Harris/code-red/issues/31)) 88 | - Throw on invalid expressions ([#31](https://github.com/Rich-Harris/code-red/issues/31)) 89 | 90 | ## 0.1.0 91 | 92 | - Throw on unhandled sigils ([#30](https://github.com/Rich-Harris/code-red/pull/30)) 93 | 94 | ## 0.0.32 95 | 96 | - Prevent syntax errors when combining comments ([#28](https://github.com/Rich-Harris/code-red/issues/28)) 97 | 98 | ## 0.0.31 99 | 100 | - Expose wrapped versions of Acorn methods to facilitate comment preservation ([#26](https://github.com/Rich-Harris/code-red/issues/26)) 101 | 102 | ## 0.0.30 103 | 104 | - Wrap `await` argument in parens if necessary ([#24](https://github.com/Rich-Harris/code-red/issues/24)) 105 | 106 | ## 0.0.29 107 | 108 | - Handle sigils in comments ([#21](https://github.com/Rich-Harris/code-red/issues/21)) 109 | 110 | ## 0.0.28 111 | 112 | - Add `toString` and `toUrl` methods on sourcemap objects ([#22](https://github.com/Rich-Harris/code-red/pull/22)) 113 | 114 | ## 0.0.27 115 | 116 | - Handle parenthesized expressions 117 | 118 | ## 0.0.26 119 | 120 | - Always replace comment values ([#20](https://github.com/Rich-Harris/code-red/pull/20)) 121 | 122 | ## 0.0.25 123 | 124 | - Fix async/generator functions in object methods ([#18](https://github.com/Rich-Harris/code-red/issues/18)) 125 | 126 | ## 0.0.24 127 | 128 | - Determine shorthand eligibility after stringification ([#17](https://github.com/Rich-Harris/code-red/pull/17)) 129 | 130 | ## 0.0.23 131 | 132 | - Unescape sigils in literals ([#16](https://github.com/Rich-Harris/code-red/pull/16)) 133 | 134 | ## 0.0.22 135 | 136 | - Prevent erroneous object shorthand when key is an identifier ([#14](https://github.com/Rich-Harris/code-red/issues/14)) 137 | 138 | ## 0.0.21 139 | 140 | - Deconflict #-identifiers in function names ([#10](https://github.com/Rich-Harris/code-red/issues/10)) 141 | - Fix object expression with string literal key matching value ([#9](https://github.com/Rich-Harris/code-red/pull/9)) 142 | 143 | ## 0.0.20 144 | 145 | - Update deps 146 | 147 | ## 0.0.19 148 | 149 | - Attach comments 150 | 151 | ## 0.0.18 152 | 153 | - Handle mixed named/default imports ([#3](https://github.com/Rich-Harris/code-red/issues/3)) 154 | - Update dependencies ([#4](https://github.com/Rich-Harris/code-red/issues/4)) 155 | 156 | ## 0.0.17 157 | 158 | - Fixes and additions 159 | 160 | ## 0.0.16 161 | 162 | - Improve some aspects of generated code 163 | 164 | ## 0.0.15 165 | 166 | - Flatten patterns 167 | 168 | ## 0.0.13-14 169 | 170 | - Sourcemaps 171 | 172 | ## 0.0.12 173 | 174 | - Flatten object properties 175 | 176 | ## 0.0.11 177 | 178 | - Handle deconfliction edge case 179 | 180 | ## 0.0.10 181 | 182 | - Tweak some TypeScript stuff 183 | 184 | ## 0.0.9 185 | 186 | - Adopt estree types 187 | - Add a `p` function for creating properties 188 | 189 | ## 0.0.8 190 | 191 | - Allow strings to be treated as identifiers 192 | 193 | ## 0.0.7 194 | 195 | - Various 196 | 197 | ## 0.0.6 198 | 199 | - Flatten arguments and parameters 200 | 201 | ## 0.0.5 202 | 203 | - Omit missing statements 204 | - Flatten arrays of statements 205 | 206 | ## 0.0.4 207 | 208 | - Use fork of astring 209 | 210 | ## 0.0.3 211 | 212 | - Allow return outside function 213 | - Print code on syntax error 214 | 215 | ## 0.0.2 216 | 217 | - Support `@`-prefixed names (replaceable globals) 218 | - Support `#`-prefixed names (automatically deconflicted) 219 | 220 | ## 0.0.1 221 | 222 | - First experimental release 223 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Rich Harris 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # code-red 2 | 3 | Experimental toolkit for writing x-to-JavaScript compilers. It is used in [Svelte](https://svelte.dev). 4 | 5 | 6 | ## API 7 | 8 | The `code-red` package exposes three core functions — `b`, `x` and `print`. 9 | 10 | `b` and `x` take a template literal and return an [ESTree](https://github.com/estree/estree) program body, or a single node: 11 | 12 | ```js 13 | import { b, x } from 'code-red'; 14 | 15 | const expression = x`i + j`; 16 | 17 | assert.equal(expression.type, 'AssignmentExpression'); 18 | assert.equal(expression.operator, '+'); 19 | assert.equal(expression.left.name, 'i'); 20 | assert.equal(expression.right.name, 'j'); 21 | 22 | const body = b` 23 | const i = 1; 24 | const j = 2; 25 | const k = i + j; 26 | `; 27 | 28 | assert.equal(body.length, 3); 29 | assert.equal(body[0].type, 'VariableDeclaration'); 30 | ``` 31 | 32 | Expressions in template literals correspond to replacement nodes — so you could express the above like so: 33 | 34 | ```js 35 | const i = x`i`; 36 | const j = x`j`; 37 | const expression = x`${i} + ${j}`; 38 | 39 | const body = b` 40 | const ${i} = 1; 41 | const ${j} = 2; 42 | const k = ${expression}; 43 | `; 44 | ``` 45 | 46 | The `print` function takes a node and turns it into a `{code, map}` object: 47 | 48 | ```js 49 | const add = x` 50 | function add(${i}, ${j}) { 51 | return ${expression}; 52 | } 53 | `; 54 | 55 | print(add).code; 56 | /* 57 | function add(i, j) { 58 | return i + j; 59 | } 60 | */ 61 | 62 | i.name = 'foo'; 63 | j.name = 'bar'; 64 | 65 | print(add).code; 66 | /* 67 | function add(foo, bar) { 68 | return foo + bar; 69 | } 70 | */ 71 | ``` 72 | 73 | ## Prefixes 74 | 75 | ### `@`-prefixed names (replaceable globals) 76 | 77 | So that you can use globals in your code. In Svelte, we use this to insert utility functions. 78 | 79 | ```js 80 | // input 81 | import { x } from 'code-red'; 82 | x`@foo(bar)` 83 | 84 | // output 85 | FOO(bar) 86 | ``` 87 | 88 | ### `#`-prefixed names (automatically deconflicted names) 89 | 90 | So that you can insert variables in your code without worrying if they clash with existing variable names. 91 | 92 | 93 | `bar` used in user code and in inserted code gets a `$1` suffix: 94 | 95 | ```js 96 | // input 97 | import { x } from 'code-red'; 98 | x` 99 | function foo(#bar) { 100 | return #bar * bar; 101 | }`; 102 | 103 | // output 104 | function foo(bar$1) { 105 | return bar$1 * bar; 106 | } 107 | ``` 108 | 109 | Without conflicts, no `$1` suffix: 110 | 111 | ```js 112 | // input 113 | import { b } from 'code-red'; 114 | b`const foo = #bar => #bar * 2`; 115 | 116 | // output 117 | const foo = bar => bar * 2; 118 | ``` 119 | 120 | ## Optimiser 121 | 122 | TODO add an optimiser that e.g. collapses consecutive identical if blocks 123 | 124 | 125 | ## Compiler 126 | 127 | TODO add a `code-red/compiler` module that replaces template literals with the nodes they evaluate to, so that there's nothing to parse at runtime. 128 | 129 | 130 | ## Sourcemaps 131 | 132 | TODO support source mappings for inserted nodes with location information. 133 | 134 | 135 | ## License 136 | 137 | [MIT](LICENSE) 138 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "code-red", 3 | "description": "code-red", 4 | "version": "1.0.4", 5 | "repository": "Rich-Harris/code-red", 6 | "exports": { 7 | ".": { 8 | "types": "./types/index.d.ts", 9 | "import": "./src/index.js" 10 | } 11 | }, 12 | "type": "module", 13 | "types": "types/index.d.ts", 14 | "files": [ 15 | "src", 16 | "types" 17 | ], 18 | "scripts": { 19 | "build": "dts-buddy", 20 | "test": "uvu test test.js", 21 | "prepublishOnly": "npm test && npm run build", 22 | "repl": "node -e \"import('./src/index.js').then(mod => { x = mod.x; b = mod.b; print = mod.print });\" -i" 23 | }, 24 | "license": "MIT", 25 | "dependencies": { 26 | "@jridgewell/sourcemap-codec": "^1.4.15", 27 | "@types/estree": "^1.0.1", 28 | "acorn": "^8.10.0", 29 | "estree-walker": "^3.0.3", 30 | "periscopic": "^3.1.0" 31 | }, 32 | "devDependencies": { 33 | "@types/node": "^20.4.10", 34 | "dts-buddy": "^0.1.9", 35 | "eslump": "^3.0.0", 36 | "uvu": "^0.5.6" 37 | }, 38 | "packageManager": "pnpm@8.6.0" 39 | } -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.1' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | dependencies: 8 | '@jridgewell/sourcemap-codec': 9 | specifier: ^1.4.15 10 | version: 1.4.15 11 | '@types/estree': 12 | specifier: ^1.0.1 13 | version: 1.0.1 14 | acorn: 15 | specifier: ^8.10.0 16 | version: 8.10.0 17 | estree-walker: 18 | specifier: ^3.0.3 19 | version: 3.0.3 20 | periscopic: 21 | specifier: ^3.1.0 22 | version: 3.1.0 23 | 24 | devDependencies: 25 | '@types/node': 26 | specifier: ^20.4.10 27 | version: 20.4.10 28 | dts-buddy: 29 | specifier: ^0.1.9 30 | version: 0.1.9 31 | eslump: 32 | specifier: ^3.0.0 33 | version: 3.0.0 34 | uvu: 35 | specifier: ^0.5.6 36 | version: 0.5.6 37 | 38 | packages: 39 | 40 | /@aashutoshrathi/word-wrap@1.2.6: 41 | resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} 42 | engines: {node: '>=0.10.0'} 43 | dev: true 44 | 45 | /@babel/code-frame@7.22.10: 46 | resolution: {integrity: sha512-/KKIMG4UEL35WmI9OlvMhurwtytjvXoFcGNrOvyG9zIzA8YmPjVtIZUf7b05+TPO7G7/GEmLHDaoCgACHl9hhA==} 47 | engines: {node: '>=6.9.0'} 48 | dependencies: 49 | '@babel/highlight': 7.22.10 50 | chalk: 2.4.2 51 | dev: true 52 | 53 | /@babel/helper-validator-identifier@7.22.5: 54 | resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==} 55 | engines: {node: '>=6.9.0'} 56 | dev: true 57 | 58 | /@babel/highlight@7.22.10: 59 | resolution: {integrity: sha512-78aUtVcT7MUscr0K5mIEnkwxPE0MaxkR5RxRwuHaQ+JuU5AmTPhY+do2mdzVTnIJJpyBglql2pehuBIWHug+WQ==} 60 | engines: {node: '>=6.9.0'} 61 | dependencies: 62 | '@babel/helper-validator-identifier': 7.22.5 63 | chalk: 2.4.2 64 | js-tokens: 4.0.0 65 | dev: true 66 | 67 | /@jridgewell/gen-mapping@0.3.3: 68 | resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} 69 | engines: {node: '>=6.0.0'} 70 | dependencies: 71 | '@jridgewell/set-array': 1.1.2 72 | '@jridgewell/sourcemap-codec': 1.4.15 73 | '@jridgewell/trace-mapping': 0.3.19 74 | dev: true 75 | 76 | /@jridgewell/resolve-uri@3.1.1: 77 | resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} 78 | engines: {node: '>=6.0.0'} 79 | dev: true 80 | 81 | /@jridgewell/set-array@1.1.2: 82 | resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} 83 | engines: {node: '>=6.0.0'} 84 | dev: true 85 | 86 | /@jridgewell/source-map@0.3.5: 87 | resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} 88 | dependencies: 89 | '@jridgewell/gen-mapping': 0.3.3 90 | '@jridgewell/trace-mapping': 0.3.19 91 | dev: true 92 | 93 | /@jridgewell/sourcemap-codec@1.4.15: 94 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 95 | 96 | /@jridgewell/trace-mapping@0.3.19: 97 | resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} 98 | dependencies: 99 | '@jridgewell/resolve-uri': 3.1.1 100 | '@jridgewell/sourcemap-codec': 1.4.15 101 | dev: true 102 | 103 | /@types/estree@1.0.1: 104 | resolution: {integrity: sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==} 105 | dev: false 106 | 107 | /@types/node@20.4.10: 108 | resolution: {integrity: sha512-vwzFiiy8Rn6E0MtA13/Cxxgpan/N6UeNYR9oUu6kuJWxu6zCk98trcDp8CBhbtaeuq9SykCmXkFr2lWLoPcvLg==} 109 | dev: true 110 | 111 | /acorn@8.10.0: 112 | resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} 113 | engines: {node: '>=0.4.0'} 114 | hasBin: true 115 | dev: false 116 | 117 | /ansi-styles@3.2.1: 118 | resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} 119 | engines: {node: '>=4'} 120 | dependencies: 121 | color-convert: 1.9.3 122 | dev: true 123 | 124 | /chalk@2.4.2: 125 | resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} 126 | engines: {node: '>=4'} 127 | dependencies: 128 | ansi-styles: 3.2.1 129 | escape-string-regexp: 1.0.5 130 | supports-color: 5.5.0 131 | dev: true 132 | 133 | /color-convert@1.9.3: 134 | resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} 135 | dependencies: 136 | color-name: 1.1.3 137 | dev: true 138 | 139 | /color-name@1.1.3: 140 | resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} 141 | dev: true 142 | 143 | /deep-is@0.1.4: 144 | resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} 145 | dev: true 146 | 147 | /dequal@2.0.3: 148 | resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} 149 | engines: {node: '>=6'} 150 | dev: true 151 | 152 | /diff@5.1.0: 153 | resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==} 154 | engines: {node: '>=0.3.1'} 155 | dev: true 156 | 157 | /dts-buddy@0.1.9: 158 | resolution: {integrity: sha512-I12BngU+NFY2njbTFCpiDqyvEFSYoMxqMpwBt1pykBEbWY6DbyDd9m0GOzd1hhpSDHDulgmQUntw5EZPNaptNA==} 159 | hasBin: true 160 | dependencies: 161 | '@jridgewell/source-map': 0.3.5 162 | '@jridgewell/sourcemap-codec': 1.4.15 163 | globrex: 0.1.2 164 | kleur: 4.1.5 165 | locate-character: 3.0.0 166 | magic-string: 0.30.2 167 | sade: 1.8.1 168 | tiny-glob: 0.2.9 169 | ts-api-utils: 0.0.46(typescript@5.1.6) 170 | typescript: 5.1.6 171 | dev: true 172 | 173 | /escape-string-regexp@1.0.5: 174 | resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} 175 | engines: {node: '>=0.8.0'} 176 | dev: true 177 | 178 | /eslump@3.0.0: 179 | resolution: {integrity: sha512-TAhAytVHCHMk0yDxCbS881uuAEPfoqdnmp2mkwDgBoM5lVBfoE9nYxq0LCdgJS8NASV8V8NIUF3N4YjhiuuTBg==} 180 | hasBin: true 181 | dependencies: 182 | '@babel/code-frame': 7.22.10 183 | mkdirp: 1.0.4 184 | optionator: 0.9.3 185 | random-int: 2.0.1 186 | random-item: 3.1.0 187 | shift-codegen: 7.0.3 188 | shift-fuzzer: 2.0.0 189 | shift-parser: 7.0.3 190 | shift-reducer: 6.0.0 191 | dev: true 192 | 193 | /estree-walker@3.0.3: 194 | resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} 195 | dependencies: 196 | '@types/estree': 1.0.1 197 | dev: false 198 | 199 | /esutils@2.0.3: 200 | resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} 201 | engines: {node: '>=0.10.0'} 202 | dev: true 203 | 204 | /fast-levenshtein@2.0.6: 205 | resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} 206 | dev: true 207 | 208 | /globalyzer@0.1.0: 209 | resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} 210 | dev: true 211 | 212 | /globrex@0.1.2: 213 | resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} 214 | dev: true 215 | 216 | /has-flag@3.0.0: 217 | resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} 218 | engines: {node: '>=4'} 219 | dev: true 220 | 221 | /is-reference@3.0.1: 222 | resolution: {integrity: sha512-baJJdQLiYaJdvFbJqXrcGv3WU3QCzBlUcI5QhbesIm6/xPsvmO+2CDoi/GMOFBQEQm+PXkwOPrp9KK5ozZsp2w==} 223 | dependencies: 224 | '@types/estree': 1.0.1 225 | dev: false 226 | 227 | /js-tokens@4.0.0: 228 | resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} 229 | dev: true 230 | 231 | /kleur@4.1.5: 232 | resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} 233 | engines: {node: '>=6'} 234 | dev: true 235 | 236 | /levn@0.4.1: 237 | resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} 238 | engines: {node: '>= 0.8.0'} 239 | dependencies: 240 | prelude-ls: 1.2.1 241 | type-check: 0.4.0 242 | dev: true 243 | 244 | /locate-character@3.0.0: 245 | resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} 246 | dev: true 247 | 248 | /magic-string@0.30.2: 249 | resolution: {integrity: sha512-lNZdu7pewtq/ZvWUp9Wpf/x7WzMTsR26TWV03BRZrXFsv+BI6dy8RAiKgm1uM/kyR0rCfUcqvOlXKG66KhIGug==} 250 | engines: {node: '>=12'} 251 | dependencies: 252 | '@jridgewell/sourcemap-codec': 1.4.15 253 | dev: true 254 | 255 | /mkdirp@1.0.4: 256 | resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} 257 | engines: {node: '>=10'} 258 | hasBin: true 259 | dev: true 260 | 261 | /mri@1.2.0: 262 | resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} 263 | engines: {node: '>=4'} 264 | dev: true 265 | 266 | /multimap@1.1.0: 267 | resolution: {integrity: sha512-0ZIR9PasPxGXmRsEF8jsDzndzHDj7tIav+JUmvIFB/WHswliFnquxECT/De7GR4yg99ky/NlRKJT82G1y271bw==} 268 | dev: true 269 | 270 | /object-assign@4.1.1: 271 | resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} 272 | engines: {node: '>=0.10.0'} 273 | dev: true 274 | 275 | /optionator@0.9.3: 276 | resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} 277 | engines: {node: '>= 0.8.0'} 278 | dependencies: 279 | '@aashutoshrathi/word-wrap': 1.2.6 280 | deep-is: 0.1.4 281 | fast-levenshtein: 2.0.6 282 | levn: 0.4.1 283 | prelude-ls: 1.2.1 284 | type-check: 0.4.0 285 | dev: true 286 | 287 | /periscopic@3.1.0: 288 | resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} 289 | dependencies: 290 | '@types/estree': 1.0.1 291 | estree-walker: 3.0.3 292 | is-reference: 3.0.1 293 | dev: false 294 | 295 | /prelude-ls@1.2.1: 296 | resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} 297 | engines: {node: '>= 0.8.0'} 298 | dev: true 299 | 300 | /random-int@2.0.1: 301 | resolution: {integrity: sha512-YALjWK2Rt9EMIv9BF/3mvlzFWQathsvb5UZmN1QmhfIOfcQYXc/UcLzg0ablqesSBpBVLt2Tlwv/eTuBh4LXUQ==} 302 | engines: {node: '>=8'} 303 | dev: true 304 | 305 | /random-item@3.1.0: 306 | resolution: {integrity: sha512-0DyAT8LYBNQKSkqcPjia/HNoWCZ5JWBdAQWjBQVh5DMVv3Fv7V90I8/AuUf8NW4zdFn27i9qj8Kp6wI5JsiiOA==} 307 | engines: {node: '>=8'} 308 | dev: true 309 | 310 | /sade@1.8.1: 311 | resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} 312 | engines: {node: '>=6'} 313 | dependencies: 314 | mri: 1.2.0 315 | dev: true 316 | 317 | /shift-ast@6.0.0: 318 | resolution: {integrity: sha512-XXxDcEBWVBzqWXfNYJlLyJ1/9kMvOXVRXiqPjkOrTCC5qRsBvEMJMRLLFhU3tn8ue56Y7IZyBE6bexFum5QLUw==} 319 | dev: true 320 | 321 | /shift-ast@6.1.0: 322 | resolution: {integrity: sha512-Vj4XUIJIFPIh6VcBGJ1hjH/kM88XGer94Pr7Rvxa+idEylDsrwtLw268HoxGo5xReL6T3DdRl/9/Pr1XihZ/8Q==} 323 | dev: true 324 | 325 | /shift-codegen@7.0.3: 326 | resolution: {integrity: sha512-dfCVVdBF0qZ6pkajQ3bjxRdNEltyxEITVe7tBJkQt2eCI3znUkSxq0VSe/tTWq1LKHeAS4HuOiqYEuHMFkSq9w==} 327 | dependencies: 328 | esutils: 2.0.3 329 | object-assign: 4.1.1 330 | shift-reducer: 6.0.0 331 | dev: true 332 | 333 | /shift-fuzzer@2.0.0: 334 | resolution: {integrity: sha512-hgV4jqXELYR0UpEDlTnYcv0GXvhXV/y3lyYNyGtTq1Q6EdFIC9LLHjlrfVFt7d1VyzJqPkl/fME31ta0KrHiTQ==} 335 | dependencies: 336 | shift-ast: 6.1.0 337 | dev: true 338 | 339 | /shift-parser@7.0.3: 340 | resolution: {integrity: sha512-uYX2ORyZfKZrUc4iKKkO9KOhzUSxCrSBk7QK6ZmShId+BOo1gh1IwecVy97ynyOTpmhPWUttjC8BzsnQl65Zew==} 341 | dependencies: 342 | multimap: 1.1.0 343 | shift-ast: 6.0.0 344 | shift-reducer: 6.0.0 345 | shift-regexp-acceptor: 2.0.3 346 | dev: true 347 | 348 | /shift-reducer@6.0.0: 349 | resolution: {integrity: sha512-2rJraRP8drIOjvaE/sALa+0tGJmMVUzlmS3wIJerJbaYuCjpFAiF0WjkTOFVtz1144Nm/ECmqeG+7yRhuMVsMg==} 350 | dependencies: 351 | shift-ast: 6.0.0 352 | dev: true 353 | 354 | /shift-regexp-acceptor@2.0.3: 355 | resolution: {integrity: sha512-sxL7e5JNUFxm+gutFRXktX2D6KVgDAHNuDsk5XHB9Z+N5yXooZG6pdZ1GEbo3Jz6lF7ETYLBC4WAjIFm2RKTmA==} 356 | dependencies: 357 | unicode-match-property-ecmascript: 1.0.4 358 | unicode-match-property-value-ecmascript: 1.0.2 359 | unicode-property-aliases-ecmascript: 1.0.4 360 | dev: true 361 | 362 | /supports-color@5.5.0: 363 | resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} 364 | engines: {node: '>=4'} 365 | dependencies: 366 | has-flag: 3.0.0 367 | dev: true 368 | 369 | /tiny-glob@0.2.9: 370 | resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} 371 | dependencies: 372 | globalyzer: 0.1.0 373 | globrex: 0.1.2 374 | dev: true 375 | 376 | /ts-api-utils@0.0.46(typescript@5.1.6): 377 | resolution: {integrity: sha512-YKJeSx39n0mMk+hrpyHKyTgxA3s7Pz/j1cXYR+t8HcwwZupzOR5xDGKnOEw3gmLaUeFUQt3FJD39AH9Ajn/mdA==} 378 | engines: {node: '>=16.13.0'} 379 | peerDependencies: 380 | typescript: '>=4.2.0' 381 | dependencies: 382 | typescript: 5.1.6 383 | dev: true 384 | 385 | /type-check@0.4.0: 386 | resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} 387 | engines: {node: '>= 0.8.0'} 388 | dependencies: 389 | prelude-ls: 1.2.1 390 | dev: true 391 | 392 | /typescript@5.1.6: 393 | resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} 394 | engines: {node: '>=14.17'} 395 | hasBin: true 396 | dev: true 397 | 398 | /unicode-canonical-property-names-ecmascript@1.0.4: 399 | resolution: {integrity: sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==} 400 | engines: {node: '>=4'} 401 | dev: true 402 | 403 | /unicode-match-property-ecmascript@1.0.4: 404 | resolution: {integrity: sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==} 405 | engines: {node: '>=4'} 406 | dependencies: 407 | unicode-canonical-property-names-ecmascript: 1.0.4 408 | unicode-property-aliases-ecmascript: 1.0.4 409 | dev: true 410 | 411 | /unicode-match-property-value-ecmascript@1.0.2: 412 | resolution: {integrity: sha512-Rx7yODZC1L/T8XKo/2kNzVAQaRE88AaMvI1EF/Xnj3GW2wzN6fop9DDWuFAKUVFH7vozkz26DzP0qyWLKLIVPQ==} 413 | engines: {node: '>=4'} 414 | dev: true 415 | 416 | /unicode-property-aliases-ecmascript@1.0.4: 417 | resolution: {integrity: sha512-2WSLa6OdYd2ng8oqiGIWnJqyFArvhn+5vgx5GTxMbUYjCYKUcuKS62YLFF0R/BDGlB1yzXjQOLtPAfHsgirEpg==} 418 | engines: {node: '>=4'} 419 | dev: true 420 | 421 | /uvu@0.5.6: 422 | resolution: {integrity: sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==} 423 | engines: {node: '>=8'} 424 | hasBin: true 425 | dependencies: 426 | dequal: 2.0.3 427 | diff: 5.1.0 428 | kleur: 4.1.5 429 | sade: 1.8.1 430 | dev: true 431 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import * as acorn from 'acorn'; 2 | import { walk } from 'estree-walker'; 3 | import { id, re } from './utils/id.js'; 4 | import { get_comment_handlers } from './utils/comments.js'; 5 | 6 | /** @typedef {import('estree').Expression} Expression */ 7 | /** @typedef {import('estree').Node} Node */ 8 | /** @typedef {import('estree').ObjectExpression} ObjectExpression */ 9 | /** @typedef {import('estree').Property} Property */ 10 | /** @typedef {import('estree').SpreadElement} SpreadElement */ 11 | 12 | /** @typedef {import('./utils/comments').CommentWithLocation} CommentWithLocation */ 13 | 14 | /** @type {Record} */ 15 | const sigils = { 16 | '@': 'AT', 17 | '#': 'HASH' 18 | }; 19 | 20 | /** @param {TemplateStringsArray} strings */ 21 | const join = (strings) => { 22 | let str = strings[0]; 23 | for (let i = 1; i < strings.length; i += 1) { 24 | str += `_${id}_${i - 1}_${strings[i]}`; 25 | } 26 | return str.replace( 27 | /([@#])(\w+)/g, 28 | (_m, sigil, name) => `_${id}_${sigils[sigil]}_${name}` 29 | ); 30 | }; 31 | 32 | /** 33 | * @param {any[]} array 34 | * @param {any[]} target 35 | */ 36 | const flatten_body = (array, target) => { 37 | for (let i = 0; i < array.length; i += 1) { 38 | const statement = array[i]; 39 | if (Array.isArray(statement)) { 40 | flatten_body(statement, target); 41 | continue; 42 | } 43 | 44 | if (statement.type === 'ExpressionStatement') { 45 | if (statement.expression === EMPTY) continue; 46 | 47 | if (Array.isArray(statement.expression)) { 48 | // TODO this is hacktacular 49 | let node = statement.expression[0]; 50 | while (Array.isArray(node)) node = node[0]; 51 | if (node) node.leadingComments = statement.leadingComments; 52 | 53 | flatten_body(statement.expression, target); 54 | continue; 55 | } 56 | 57 | if (/(Expression|Literal)$/.test(statement.expression.type)) { 58 | target.push(statement); 59 | continue; 60 | } 61 | 62 | if (statement.leadingComments) 63 | statement.expression.leadingComments = statement.leadingComments; 64 | if (statement.trailingComments) 65 | statement.expression.trailingComments = statement.trailingComments; 66 | 67 | target.push(statement.expression); 68 | continue; 69 | } 70 | 71 | target.push(statement); 72 | } 73 | 74 | return target; 75 | }; 76 | 77 | /** 78 | * @param {any[]} array 79 | * @param {any[]} target 80 | */ 81 | const flatten_properties = (array, target) => { 82 | for (let i = 0; i < array.length; i += 1) { 83 | const property = array[i]; 84 | 85 | if (property.value === EMPTY) continue; 86 | 87 | if (property.key === property.value && Array.isArray(property.key)) { 88 | flatten_properties(property.key, target); 89 | continue; 90 | } 91 | 92 | target.push(property); 93 | } 94 | 95 | return target; 96 | }; 97 | 98 | /** 99 | * @param {any[]} nodes 100 | * @param {any[]} target 101 | */ 102 | const flatten = (nodes, target) => { 103 | for (let i = 0; i < nodes.length; i += 1) { 104 | const node = nodes[i]; 105 | 106 | if (node === EMPTY) continue; 107 | 108 | if (Array.isArray(node)) { 109 | flatten(node, target); 110 | continue; 111 | } 112 | 113 | target.push(node); 114 | } 115 | 116 | return target; 117 | }; 118 | 119 | const EMPTY = { type: 'Empty' }; 120 | 121 | /** 122 | * 123 | * @param {CommentWithLocation[]} comments 124 | * @param {string} raw 125 | * @returns {any} 126 | */ 127 | const acorn_opts = (comments, raw) => { 128 | const { onComment } = get_comment_handlers(comments, raw); 129 | return { 130 | ecmaVersion: 2022, 131 | sourceType: 'module', 132 | allowAwaitOutsideFunction: true, 133 | allowImportExportEverywhere: true, 134 | allowReturnOutsideFunction: true, 135 | onComment 136 | }; 137 | }; 138 | 139 | /** 140 | * @param {string} raw 141 | * @param {Node} node 142 | * @param {any[]} values 143 | * @param {CommentWithLocation[]} comments 144 | */ 145 | const inject = (raw, node, values, comments) => { 146 | comments.forEach((comment) => { 147 | comment.value = comment.value.replace(re, (m, i) => 148 | +i in values ? values[+i] : m 149 | ); 150 | }); 151 | 152 | const { enter, leave } = get_comment_handlers(comments, raw); 153 | 154 | return walk(node, { 155 | enter, 156 | 157 | /** @param {any} node */ 158 | leave(node) { 159 | if (node.type === 'Identifier') { 160 | re.lastIndex = 0; 161 | const match = re.exec(node.name); 162 | 163 | if (match) { 164 | if (match[1]) { 165 | if (+match[1] in values) { 166 | let value = values[+match[1]]; 167 | 168 | if (typeof value === 'string') { 169 | value = { 170 | type: 'Identifier', 171 | name: value, 172 | leadingComments: node.leadingComments, 173 | trailingComments: node.trailingComments 174 | }; 175 | } else if (typeof value === 'number') { 176 | value = { 177 | type: 'Literal', 178 | value, 179 | leadingComments: node.leadingComments, 180 | trailingComments: node.trailingComments 181 | }; 182 | } 183 | 184 | this.replace(value || EMPTY); 185 | } 186 | } else { 187 | node.name = `${match[2] ? `@` : `#`}${match[4]}`; 188 | } 189 | } 190 | } 191 | 192 | if (node.type === 'Literal') { 193 | if (typeof node.value === 'string') { 194 | re.lastIndex = 0; 195 | const new_value = /** @type {string} */ (node.value).replace( 196 | re, 197 | (m, i) => (+i in values ? values[+i] : m) 198 | ); 199 | const has_changed = new_value !== node.value; 200 | node.value = new_value; 201 | if (has_changed && node.raw) { 202 | // preserve the quotes 203 | node.raw = `${node.raw[0]}${JSON.stringify(node.value).slice( 204 | 1, 205 | -1 206 | )}${node.raw[node.raw.length - 1]}`; 207 | } 208 | } 209 | } 210 | 211 | if (node.type === 'TemplateElement') { 212 | re.lastIndex = 0; 213 | node.value.raw = /** @type {string} */ (node.value.raw).replace( 214 | re, 215 | (m, i) => (+i in values ? values[+i] : m) 216 | ); 217 | } 218 | 219 | if (node.type === 'Program' || node.type === 'BlockStatement') { 220 | node.body = flatten_body(node.body, []); 221 | } 222 | 223 | if (node.type === 'ObjectExpression' || node.type === 'ObjectPattern') { 224 | node.properties = flatten_properties(node.properties, []); 225 | } 226 | 227 | if (node.type === 'ArrayExpression' || node.type === 'ArrayPattern') { 228 | node.elements = flatten(node.elements, []); 229 | } 230 | 231 | if ( 232 | node.type === 'FunctionExpression' || 233 | node.type === 'FunctionDeclaration' || 234 | node.type === 'ArrowFunctionExpression' 235 | ) { 236 | node.params = flatten(node.params, []); 237 | } 238 | 239 | if (node.type === 'CallExpression' || node.type === 'NewExpression') { 240 | node.arguments = flatten(node.arguments, []); 241 | } 242 | 243 | if ( 244 | node.type === 'ImportDeclaration' || 245 | node.type === 'ExportNamedDeclaration' 246 | ) { 247 | node.specifiers = flatten(node.specifiers, []); 248 | } 249 | 250 | if (node.type === 'ForStatement') { 251 | node.init = node.init === EMPTY ? null : node.init; 252 | node.test = node.test === EMPTY ? null : node.test; 253 | node.update = node.update === EMPTY ? null : node.update; 254 | } 255 | 256 | leave(node); 257 | } 258 | }); 259 | }; 260 | 261 | /** 262 | * 263 | * @param {TemplateStringsArray} strings 264 | * @param {any[]} values 265 | * @returns {Node[]} 266 | */ 267 | export function b(strings, ...values) { 268 | const str = join(strings); 269 | 270 | /** @type {CommentWithLocation[]} */ 271 | const comments = []; 272 | 273 | try { 274 | let ast = /** @type {any} */ (acorn.parse(str, acorn_opts(comments, str))); 275 | 276 | ast = inject(str, ast, values, comments); 277 | 278 | return ast.body; 279 | } catch (err) { 280 | handle_error(str, err); 281 | } 282 | } 283 | 284 | /** 285 | * 286 | * @param {TemplateStringsArray} strings 287 | * @param {any[]} values 288 | * @returns {Expression & { start: Number, end: number }} 289 | */ 290 | export function x(strings, ...values) { 291 | const str = join(strings); 292 | 293 | /** @type {CommentWithLocation[]} */ 294 | const comments = []; 295 | 296 | try { 297 | let expression = 298 | /** @type {Expression & { start: Number, end: number }} */ ( 299 | acorn.parseExpressionAt(str, 0, acorn_opts(comments, str)) 300 | ); 301 | const match = /\S+/.exec(str.slice(expression.end)); 302 | if (match) { 303 | throw new Error(`Unexpected token '${match[0]}'`); 304 | } 305 | 306 | expression = /** @type {Expression & { start: Number, end: number }} */ ( 307 | inject(str, expression, values, comments) 308 | ); 309 | 310 | return expression; 311 | } catch (err) { 312 | handle_error(str, err); 313 | } 314 | } 315 | 316 | /** 317 | * 318 | * @param {TemplateStringsArray} strings 319 | * @param {any[]} values 320 | * @returns {(Property | SpreadElement) & { start: Number, end: number }} 321 | */ 322 | export function p(strings, ...values) { 323 | const str = `{${join(strings)}}`; 324 | 325 | /** @type {CommentWithLocation[]} */ 326 | const comments = []; 327 | 328 | try { 329 | let expression = /** @type {any} */ ( 330 | acorn.parseExpressionAt(str, 0, acorn_opts(comments, str)) 331 | ); 332 | 333 | expression = inject(str, expression, values, comments); 334 | 335 | return expression.properties[0]; 336 | } catch (err) { 337 | handle_error(str, err); 338 | } 339 | } 340 | 341 | /** 342 | * @param {string} str 343 | * @param {Error} err 344 | */ 345 | function handle_error(str, err) { 346 | // TODO location/code frame 347 | 348 | re.lastIndex = 0; 349 | 350 | str = str.replace(re, (m, i, at, hash, name) => { 351 | if (at) return `@${name}`; 352 | if (hash) return `#${name}`; 353 | 354 | return '${...}'; 355 | }); 356 | 357 | console.log(`failed to parse:\n${str}`); 358 | throw err; 359 | } 360 | 361 | export { print } from './print/index.js'; 362 | 363 | /** 364 | * @param {string} source 365 | * @param {any} opts 366 | */ 367 | export const parse = (source, opts) => { 368 | /** @type {CommentWithLocation[]} */ 369 | const comments = []; 370 | const { onComment, enter, leave } = get_comment_handlers(comments, source); 371 | const ast = /** @type {any} */ (acorn.parse(source, { onComment, ...opts })); 372 | walk(ast, { enter, leave }); 373 | return ast; 374 | }; 375 | 376 | /** 377 | * @param {string} source 378 | * @param {number} index 379 | * @param {any} opts 380 | */ 381 | export const parseExpressionAt = (source, index, opts) => { 382 | /** @type {CommentWithLocation[]} */ 383 | const comments = []; 384 | const { onComment, enter, leave } = get_comment_handlers(comments, source); 385 | const ast = /** @type {any} */ ( 386 | acorn.parseExpressionAt(source, index, { onComment, ...opts }) 387 | ); 388 | walk(ast, { enter, leave }); 389 | return ast; 390 | }; 391 | -------------------------------------------------------------------------------- /src/print/handlers.js: -------------------------------------------------------------------------------- 1 | // heavily based on https://github.com/davidbonnet/astring 2 | // released under MIT license https://github.com/davidbonnet/astring/blob/master/LICENSE 3 | 4 | import { re } from '../utils/id.js'; 5 | import { push_array } from '../utils/push_array.js'; 6 | 7 | /** @typedef {import('estree').ArrowFunctionExpression} ArrowFunctionExpression */ 8 | /** @typedef {import('estree').BinaryExpression} BinaryExpression */ 9 | /** @typedef {import('estree').CallExpression} CallExpression */ 10 | /** @typedef {import('estree').Comment} Comment */ 11 | /** @typedef {import('estree').ExportSpecifier} ExportSpecifier */ 12 | /** @typedef {import('estree').Expression} Expression */ 13 | /** @typedef {import('estree').FunctionDeclaration} FunctionDeclaration */ 14 | /** @typedef {import('estree').ImportDeclaration} ImportDeclaration */ 15 | /** @typedef {import('estree').ImportSpecifier} ImportSpecifier */ 16 | /** @typedef {import('estree').Literal} Literal */ 17 | /** @typedef {import('estree').LogicalExpression} LogicalExpression */ 18 | /** @typedef {import('estree').NewExpression} NewExpression */ 19 | /** @typedef {import('estree').Node} Node */ 20 | /** @typedef {import('estree').ObjectExpression} ObjectExpression */ 21 | /** @typedef {import('estree').Pattern} Pattern */ 22 | /** @typedef {import('estree').Property} Property */ 23 | /** @typedef {import('estree').PropertyDefinition} PropertyDefinition */ 24 | /** @typedef {import('estree').SequenceExpression} SequenceExpression */ 25 | /** @typedef {import('estree').SimpleCallExpression} SimpleCallExpression */ 26 | /** @typedef {import('estree').SwitchStatement} SwitchStatement */ 27 | /** @typedef {import('estree').VariableDeclaration} VariableDeclaration */ 28 | /** @typedef {import('estree').StaticBlock} StaticBlock */ 29 | /** @typedef {import('estree').PrivateIdentifier} PrivateIdenifier*/ 30 | 31 | /** 32 | * @typedef {{ 33 | * content: string; 34 | * loc?: { 35 | * start: { line: number; column: number; }; 36 | * end: { line: number; column: number; }; 37 | * }; 38 | * has_newline: boolean; 39 | * }} Chunk 40 | */ 41 | 42 | /** 43 | * @typedef {(node: any, state: State) => Chunk[]} Handler 44 | */ 45 | 46 | /** 47 | * @typedef {{ 48 | * indent: string; 49 | * scope: any; // TODO import from periscopic 50 | * scope_map: WeakMap; 51 | * getName: (name: string) => string; 52 | * deconflicted: WeakMap>; 53 | * comments: Comment[]; 54 | * }} State 55 | */ 56 | 57 | /** 58 | * @param {Node} node 59 | * @param {State} state 60 | * @returns {Chunk[]} 61 | */ 62 | export function handle(node, state) { 63 | const handler = handlers[node.type]; 64 | 65 | if (!handler) { 66 | throw new Error(`Not implemented ${node.type}`); 67 | } 68 | 69 | const result = handler(node, state); 70 | 71 | if (node.leadingComments) { 72 | result.unshift( 73 | c( 74 | node.leadingComments 75 | .map((comment) => 76 | comment.type === 'Block' 77 | ? `/*${comment.value}*/${ 78 | /** @type {any} */ (comment).has_trailing_newline 79 | ? `\n${state.indent}` 80 | : ` ` 81 | }` 82 | : `//${comment.value}${ 83 | /** @type {any} */ (comment).has_trailing_newline 84 | ? `\n${state.indent}` 85 | : ` ` 86 | }` 87 | ) 88 | .join(``) 89 | ) 90 | ); 91 | } 92 | 93 | if (node.trailingComments) { 94 | state.comments.push(node.trailingComments[0]); // there is only ever one 95 | } 96 | 97 | return result; 98 | } 99 | 100 | /** 101 | * @param {string} content 102 | * @param {Node} [node] 103 | * @returns {Chunk} 104 | */ 105 | function c(content, node) { 106 | return { 107 | content, 108 | loc: node && node.loc, 109 | has_newline: /\n/.test(content) 110 | }; 111 | } 112 | 113 | const OPERATOR_PRECEDENCE = { 114 | '||': 2, 115 | '&&': 3, 116 | '??': 4, 117 | '|': 5, 118 | '^': 6, 119 | '&': 7, 120 | '==': 8, 121 | '!=': 8, 122 | '===': 8, 123 | '!==': 8, 124 | '<': 9, 125 | '>': 9, 126 | '<=': 9, 127 | '>=': 9, 128 | in: 9, 129 | instanceof: 9, 130 | '<<': 10, 131 | '>>': 10, 132 | '>>>': 10, 133 | '+': 11, 134 | '-': 11, 135 | '*': 12, 136 | '%': 12, 137 | '/': 12, 138 | '**': 13 139 | }; 140 | 141 | /** @type {Record} */ 142 | const EXPRESSIONS_PRECEDENCE = { 143 | ArrayExpression: 20, 144 | TaggedTemplateExpression: 20, 145 | ThisExpression: 20, 146 | Identifier: 20, 147 | Literal: 18, 148 | TemplateLiteral: 20, 149 | Super: 20, 150 | SequenceExpression: 20, 151 | MemberExpression: 19, 152 | CallExpression: 19, 153 | NewExpression: 19, 154 | AwaitExpression: 17, 155 | ClassExpression: 17, 156 | FunctionExpression: 17, 157 | ObjectExpression: 17, 158 | UpdateExpression: 16, 159 | UnaryExpression: 15, 160 | BinaryExpression: 14, 161 | LogicalExpression: 13, 162 | ConditionalExpression: 4, 163 | ArrowFunctionExpression: 3, 164 | AssignmentExpression: 3, 165 | YieldExpression: 2, 166 | RestElement: 1 167 | }; 168 | 169 | /** 170 | * 171 | * @param {Expression} node 172 | * @param {BinaryExpression | LogicalExpression} parent 173 | * @param {boolean} is_right 174 | * @returns 175 | */ 176 | function needs_parens(node, parent, is_right) { 177 | // special case where logical expressions and coalesce expressions cannot be mixed, 178 | // either of them need to be wrapped with parentheses 179 | if ( 180 | node.type === 'LogicalExpression' && 181 | parent.type === 'LogicalExpression' && 182 | ((parent.operator === '??' && node.operator !== '??') || 183 | (parent.operator !== '??' && node.operator === '??')) 184 | ) { 185 | return true; 186 | } 187 | 188 | const precedence = EXPRESSIONS_PRECEDENCE[node.type]; 189 | const parent_precedence = EXPRESSIONS_PRECEDENCE[parent.type]; 190 | 191 | if (precedence !== parent_precedence) { 192 | // Different node types 193 | return ( 194 | (!is_right && 195 | precedence === 15 && 196 | parent_precedence === 14 && 197 | parent.operator === '**') || 198 | precedence < parent_precedence 199 | ); 200 | } 201 | 202 | if (precedence !== 13 && precedence !== 14) { 203 | // Not a `LogicalExpression` or `BinaryExpression` 204 | return false; 205 | } 206 | 207 | if ( 208 | /** @type {BinaryExpression} */ (node).operator === '**' && 209 | parent.operator === '**' 210 | ) { 211 | // Exponentiation operator has right-to-left associativity 212 | return !is_right; 213 | } 214 | 215 | if (is_right) { 216 | // Parenthesis are used if both operators have the same precedence 217 | return ( 218 | OPERATOR_PRECEDENCE[/** @type {BinaryExpression} */ (node).operator] <= 219 | OPERATOR_PRECEDENCE[parent.operator] 220 | ); 221 | } 222 | 223 | return ( 224 | OPERATOR_PRECEDENCE[/** @type {BinaryExpression} */ (node).operator] < 225 | OPERATOR_PRECEDENCE[parent.operator] 226 | ); 227 | } 228 | 229 | /** @param {Node} node */ 230 | function has_call_expression(node) { 231 | while (node) { 232 | if (node.type[0] === 'CallExpression') { 233 | return true; 234 | } else if (node.type === 'MemberExpression') { 235 | node = node.object; 236 | } else { 237 | return false; 238 | } 239 | } 240 | } 241 | 242 | /** @param {Chunk[]} chunks */ 243 | const has_newline = (chunks) => { 244 | for (let i = 0; i < chunks.length; i += 1) { 245 | if (chunks[i].has_newline) return true; 246 | } 247 | return false; 248 | }; 249 | 250 | /** @param {Chunk[]} chunks */ 251 | const get_length = (chunks) => { 252 | let total = 0; 253 | for (let i = 0; i < chunks.length; i += 1) { 254 | total += chunks[i].content.length; 255 | } 256 | return total; 257 | }; 258 | 259 | /** 260 | * @param {number} a 261 | * @param {number} b 262 | */ 263 | const sum = (a, b) => a + b; 264 | 265 | /** 266 | * @param {Chunk[][]} nodes 267 | * @param {Chunk} separator 268 | * @returns {Chunk[]} 269 | */ 270 | const join = (nodes, separator) => { 271 | if (nodes.length === 0) return []; 272 | 273 | const joined = [...nodes[0]]; 274 | for (let i = 1; i < nodes.length; i += 1) { 275 | joined.push(separator); 276 | push_array(joined, nodes[i]); 277 | } 278 | return joined; 279 | }; 280 | 281 | /** 282 | * @param {(node: any, state: State) => Chunk[]} fn 283 | */ 284 | const scoped = (fn) => { 285 | /** 286 | * @param {any} node 287 | * @param {State} state 288 | */ 289 | const scoped_fn = (node, state) => { 290 | return fn(node, { 291 | ...state, 292 | scope: state.scope_map.get(node) 293 | }); 294 | }; 295 | 296 | return scoped_fn; 297 | }; 298 | 299 | /** 300 | * @param {string} name 301 | * @param {Set} names 302 | */ 303 | const deconflict = (name, names) => { 304 | const original = name; 305 | let i = 1; 306 | 307 | while (names.has(name)) { 308 | name = `${original}$${i++}`; 309 | } 310 | 311 | return name; 312 | }; 313 | 314 | /** 315 | * @param {Node[]} nodes 316 | * @param {State} state 317 | */ 318 | const handle_body = (nodes, state) => { 319 | const chunks = []; 320 | 321 | const body = nodes.map((statement) => { 322 | const chunks = handle(statement, { 323 | ...state, 324 | indent: state.indent 325 | }); 326 | 327 | let add_newline = false; 328 | 329 | while (state.comments.length) { 330 | const comment = state.comments.shift(); 331 | const prefix = add_newline ? `\n${state.indent}` : ` `; 332 | 333 | chunks.push( 334 | c( 335 | comment.type === 'Block' 336 | ? `${prefix}/*${comment.value}*/` 337 | : `${prefix}//${comment.value}` 338 | ) 339 | ); 340 | 341 | add_newline = comment.type === 'Line'; 342 | } 343 | 344 | return chunks; 345 | }); 346 | 347 | let needed_padding = false; 348 | 349 | for (let i = 0; i < body.length; i += 1) { 350 | const needs_padding = has_newline(body[i]); 351 | 352 | if (i > 0) { 353 | chunks.push( 354 | c( 355 | needs_padding || needed_padding 356 | ? `\n\n${state.indent}` 357 | : `\n${state.indent}` 358 | ) 359 | ); 360 | } 361 | 362 | push_array(chunks, body[i]); 363 | 364 | needed_padding = needs_padding; 365 | } 366 | 367 | return chunks; 368 | }; 369 | 370 | /** 371 | * @param {VariableDeclaration} node 372 | * @param {State} state 373 | */ 374 | const handle_var_declaration = (node, state) => { 375 | const chunks = [c(`${node.kind} `)]; 376 | 377 | const declarators = node.declarations.map((d) => 378 | handle(d, { 379 | ...state, 380 | indent: state.indent + (node.declarations.length === 1 ? '' : '\t') 381 | }) 382 | ); 383 | 384 | const multiple_lines = 385 | declarators.some(has_newline) || 386 | declarators.map(get_length).reduce(sum, 0) + 387 | (state.indent.length + declarators.length - 1) * 2 > 388 | 80; 389 | 390 | const separator = c(multiple_lines ? `,\n${state.indent}\t` : ', '); 391 | 392 | push_array(chunks, join(declarators, separator)); 393 | 394 | return chunks; 395 | }; 396 | 397 | /** @type {Record} */ 398 | const handlers = { 399 | Program(node, state) { 400 | return handle_body(node.body, state); 401 | }, 402 | 403 | BlockStatement: scoped((node, state) => { 404 | return [ 405 | c(`{\n${state.indent}\t`), 406 | ...handle_body(node.body, { ...state, indent: state.indent + '\t' }), 407 | c(`\n${state.indent}}`) 408 | ]; 409 | }), 410 | 411 | EmptyStatement(node, state) { 412 | return [c(';')]; 413 | }, 414 | 415 | ParenthesizedExpression(node, state) { 416 | return handle(node.expression, state); 417 | }, 418 | 419 | ExpressionStatement(node, state) { 420 | if ( 421 | node.expression.type === 'AssignmentExpression' && 422 | node.expression.left.type === 'ObjectPattern' 423 | ) { 424 | // is an AssignmentExpression to an ObjectPattern 425 | return [c('('), ...handle(node.expression, state), c(');')]; 426 | } 427 | 428 | return [...handle(node.expression, state), c(';')]; 429 | }, 430 | 431 | IfStatement(node, state) { 432 | const chunks = [ 433 | c('if ('), 434 | ...handle(node.test, state), 435 | c(') '), 436 | ...handle(node.consequent, state) 437 | ]; 438 | 439 | if (node.alternate) { 440 | chunks.push(c(' else ')); 441 | push_array(chunks, handle(node.alternate, state)); 442 | } 443 | 444 | return chunks; 445 | }, 446 | 447 | LabeledStatement(node, state) { 448 | return [...handle(node.label, state), c(': '), ...handle(node.body, state)]; 449 | }, 450 | 451 | BreakStatement(node, state) { 452 | return node.label 453 | ? [c('break '), ...handle(node.label, state), c(';')] 454 | : [c('break;')]; 455 | }, 456 | 457 | ContinueStatement(node, state) { 458 | return node.label 459 | ? [c('continue '), ...handle(node.label, state), c(';')] 460 | : [c('continue;')]; 461 | }, 462 | 463 | WithStatement(node, state) { 464 | return [ 465 | c('with ('), 466 | ...handle(node.object, state), 467 | c(') '), 468 | ...handle(node.body, state) 469 | ]; 470 | }, 471 | 472 | SwitchStatement(/** @type {SwitchStatement} */ node, state) { 473 | const chunks = [ 474 | c('switch ('), 475 | ...handle(node.discriminant, state), 476 | c(') {') 477 | ]; 478 | 479 | node.cases.forEach((block) => { 480 | if (block.test) { 481 | chunks.push(c(`\n${state.indent}\tcase `)); 482 | push_array( 483 | chunks, 484 | handle(block.test, { ...state, indent: `${state.indent}\t` }) 485 | ); 486 | chunks.push(c(':')); 487 | } else { 488 | chunks.push(c(`\n${state.indent}\tdefault:`)); 489 | } 490 | 491 | block.consequent.forEach((statement) => { 492 | chunks.push(c(`\n${state.indent}\t\t`)); 493 | push_array( 494 | chunks, 495 | handle(statement, { ...state, indent: `${state.indent}\t\t` }) 496 | ); 497 | }); 498 | }); 499 | 500 | chunks.push(c(`\n${state.indent}}`)); 501 | 502 | return chunks; 503 | }, 504 | 505 | ReturnStatement(node, state) { 506 | if (node.argument) { 507 | const contains_comment = 508 | node.argument.leadingComments && 509 | node.argument.leadingComments.some( 510 | ( 511 | /** @type import('../utils/comments.js').CommentWithLocation */ comment 512 | ) => comment.has_trailing_newline 513 | ); 514 | return [ 515 | c(contains_comment ? 'return (' : 'return '), 516 | ...handle(node.argument, state), 517 | c(contains_comment ? ');' : ';') 518 | ]; 519 | } else { 520 | return [c('return;')]; 521 | } 522 | }, 523 | 524 | ThrowStatement(node, state) { 525 | return [c('throw '), ...handle(node.argument, state), c(';')]; 526 | }, 527 | 528 | TryStatement(node, state) { 529 | const chunks = [c('try '), ...handle(node.block, state)]; 530 | 531 | if (node.handler) { 532 | if (node.handler.param) { 533 | chunks.push(c(' catch(')); 534 | push_array(chunks, handle(node.handler.param, state)); 535 | chunks.push(c(') ')); 536 | } else { 537 | chunks.push(c(' catch ')); 538 | } 539 | 540 | push_array(chunks, handle(node.handler.body, state)); 541 | } 542 | 543 | if (node.finalizer) { 544 | chunks.push(c(' finally ')); 545 | push_array(chunks, handle(node.finalizer, state)); 546 | } 547 | 548 | return chunks; 549 | }, 550 | 551 | WhileStatement(node, state) { 552 | return [ 553 | c('while ('), 554 | ...handle(node.test, state), 555 | c(') '), 556 | ...handle(node.body, state) 557 | ]; 558 | }, 559 | 560 | DoWhileStatement(node, state) { 561 | return [ 562 | c('do '), 563 | ...handle(node.body, state), 564 | c(' while ('), 565 | ...handle(node.test, state), 566 | c(');') 567 | ]; 568 | }, 569 | 570 | ForStatement: scoped((node, state) => { 571 | const chunks = [c('for (')]; 572 | 573 | if (node.init) { 574 | if (node.init.type === 'VariableDeclaration') { 575 | push_array(chunks, handle_var_declaration(node.init, state)); 576 | } else { 577 | push_array(chunks, handle(node.init, state)); 578 | } 579 | } 580 | 581 | chunks.push(c('; ')); 582 | if (node.test) push_array(chunks, handle(node.test, state)); 583 | chunks.push(c('; ')); 584 | if (node.update) push_array(chunks, handle(node.update, state)); 585 | 586 | chunks.push(c(') ')); 587 | push_array(chunks, handle(node.body, state)); 588 | 589 | return chunks; 590 | }), 591 | 592 | ForInStatement: scoped((node, state) => { 593 | const chunks = [c(`for ${node.await ? 'await ' : ''}(`)]; 594 | 595 | if (node.left.type === 'VariableDeclaration') { 596 | push_array(chunks, handle_var_declaration(node.left, state)); 597 | } else { 598 | push_array(chunks, handle(node.left, state)); 599 | } 600 | 601 | chunks.push(c(node.type === 'ForInStatement' ? ` in ` : ` of `)); 602 | push_array(chunks, handle(node.right, state)); 603 | chunks.push(c(') ')); 604 | push_array(chunks, handle(node.body, state)); 605 | 606 | return chunks; 607 | }), 608 | 609 | DebuggerStatement(node, state) { 610 | return [c('debugger', node), c(';')]; 611 | }, 612 | 613 | FunctionDeclaration: scoped( 614 | (/** @type {FunctionDeclaration} */ node, state) => { 615 | const chunks = []; 616 | 617 | if (node.async) chunks.push(c('async ')); 618 | chunks.push(c(node.generator ? 'function* ' : 'function ')); 619 | if (node.id) push_array(chunks, handle(node.id, state)); 620 | chunks.push(c('(')); 621 | 622 | const params = node.params.map((p) => 623 | handle(p, { 624 | ...state, 625 | indent: state.indent + '\t' 626 | }) 627 | ); 628 | 629 | const multiple_lines = 630 | params.some(has_newline) || 631 | params.map(get_length).reduce(sum, 0) + 632 | (state.indent.length + params.length - 1) * 2 > 633 | 80; 634 | 635 | const separator = c(multiple_lines ? `,\n${state.indent}` : ', '); 636 | 637 | if (multiple_lines) { 638 | chunks.push(c(`\n${state.indent}\t`)); 639 | push_array(chunks, join(params, separator)); 640 | chunks.push(c(`\n${state.indent}`)); 641 | } else { 642 | push_array(chunks, join(params, separator)); 643 | } 644 | 645 | chunks.push(c(') ')); 646 | push_array(chunks, handle(node.body, state)); 647 | 648 | return chunks; 649 | } 650 | ), 651 | 652 | VariableDeclaration(node, state) { 653 | return handle_var_declaration(node, state).concat(c(';')); 654 | }, 655 | 656 | VariableDeclarator(node, state) { 657 | if (node.init) { 658 | return [...handle(node.id, state), c(' = '), ...handle(node.init, state)]; 659 | } else { 660 | return handle(node.id, state); 661 | } 662 | }, 663 | 664 | ClassDeclaration(node, state) { 665 | const chunks = [c('class ')]; 666 | 667 | if (node.id) { 668 | push_array(chunks, handle(node.id, state)); 669 | chunks.push(c(' ')); 670 | } 671 | 672 | if (node.superClass) { 673 | chunks.push(c('extends ')); 674 | push_array(chunks, handle(node.superClass, state)); 675 | chunks.push(c(' ')); 676 | } 677 | 678 | push_array(chunks, handle(node.body, state)); 679 | 680 | return chunks; 681 | }, 682 | 683 | ImportDeclaration(/** @type {ImportDeclaration} */ node, state) { 684 | const chunks = [c('import ')]; 685 | 686 | const { length } = node.specifiers; 687 | const source = handle(node.source, state); 688 | 689 | if (length > 0) { 690 | let i = 0; 691 | 692 | while (i < length) { 693 | if (i > 0) { 694 | chunks.push(c(', ')); 695 | } 696 | 697 | const specifier = node.specifiers[i]; 698 | 699 | if (specifier.type === 'ImportDefaultSpecifier') { 700 | chunks.push(c(specifier.local.name, specifier)); 701 | i += 1; 702 | } else if (specifier.type === 'ImportNamespaceSpecifier') { 703 | chunks.push(c('* as ' + specifier.local.name, specifier)); 704 | i += 1; 705 | } else { 706 | break; 707 | } 708 | } 709 | 710 | if (i < length) { 711 | // we have named specifiers 712 | const specifiers = node.specifiers 713 | .slice(i) 714 | .map((/** @type {ImportSpecifier} */ specifier) => { 715 | const name = handle(specifier.imported, state)[0]; 716 | const as = handle(specifier.local, state)[0]; 717 | 718 | if (name.content === as.content) { 719 | return [as]; 720 | } 721 | 722 | return [name, c(' as '), as]; 723 | }); 724 | 725 | const width = 726 | get_length(chunks) + 727 | specifiers.map(get_length).reduce(sum, 0) + 728 | 2 * specifiers.length + 729 | 6 + 730 | get_length(source); 731 | 732 | if (width > 80) { 733 | chunks.push(c(`{\n\t`)); 734 | push_array(chunks, join(specifiers, c(',\n\t'))); 735 | chunks.push(c('\n}')); 736 | } else { 737 | chunks.push(c(`{ `)); 738 | push_array(chunks, join(specifiers, c(', '))); 739 | chunks.push(c(' }')); 740 | } 741 | } 742 | 743 | chunks.push(c(' from ')); 744 | } 745 | 746 | push_array(chunks, source); 747 | chunks.push(c(';')); 748 | 749 | return chunks; 750 | }, 751 | 752 | ImportExpression(node, state) { 753 | return [c('import('), ...handle(node.source, state), c(')')]; 754 | }, 755 | 756 | ExportDefaultDeclaration(node, state) { 757 | const chunks = [c(`export default `), ...handle(node.declaration, state)]; 758 | 759 | if (node.declaration.type !== 'FunctionDeclaration') { 760 | chunks.push(c(';')); 761 | } 762 | 763 | return chunks; 764 | }, 765 | 766 | ExportNamedDeclaration(node, state) { 767 | const chunks = [c('export ')]; 768 | 769 | if (node.declaration) { 770 | push_array(chunks, handle(node.declaration, state)); 771 | } else { 772 | const specifiers = node.specifiers.map( 773 | (/** @type {ExportSpecifier} */ specifier) => { 774 | const name = handle(specifier.local, state)[0]; 775 | const as = handle(specifier.exported, state)[0]; 776 | 777 | if (name.content === as.content) { 778 | return [name]; 779 | } 780 | 781 | return [name, c(' as '), as]; 782 | } 783 | ); 784 | 785 | const width = 786 | 7 + specifiers.map(get_length).reduce(sum, 0) + 2 * specifiers.length; 787 | 788 | if (width > 80) { 789 | chunks.push(c('{\n\t')); 790 | push_array(chunks, join(specifiers, c(',\n\t'))); 791 | chunks.push(c('\n}')); 792 | } else { 793 | chunks.push(c('{ ')); 794 | push_array(chunks, join(specifiers, c(', '))); 795 | chunks.push(c(' }')); 796 | } 797 | 798 | if (node.source) { 799 | chunks.push(c(' from ')); 800 | push_array(chunks, handle(node.source, state)); 801 | } 802 | } 803 | 804 | chunks.push(c(';')); 805 | 806 | return chunks; 807 | }, 808 | 809 | ExportAllDeclaration(node, state) { 810 | return [c(`export * from `), ...handle(node.source, state), c(`;`)]; 811 | }, 812 | 813 | MethodDefinition(node, state) { 814 | const chunks = []; 815 | 816 | if (node.static) { 817 | chunks.push(c('static ')); 818 | } 819 | 820 | if (node.kind === 'get' || node.kind === 'set') { 821 | // Getter or setter 822 | chunks.push(c(node.kind + ' ')); 823 | } 824 | 825 | if (node.value.async) { 826 | chunks.push(c('async ')); 827 | } 828 | 829 | if (node.value.generator) { 830 | chunks.push(c('*')); 831 | } 832 | 833 | if (node.computed) { 834 | chunks.push(c('[')); 835 | push_array(chunks, handle(node.key, state)); 836 | chunks.push(c(']')); 837 | } else { 838 | push_array(chunks, handle(node.key, state)); 839 | } 840 | 841 | chunks.push(c('(')); 842 | 843 | const { params } = node.value; 844 | for (let i = 0; i < params.length; i += 1) { 845 | push_array(chunks, handle(params[i], state)); 846 | if (i < params.length - 1) chunks.push(c(', ')); 847 | } 848 | 849 | chunks.push(c(') ')); 850 | push_array(chunks, handle(node.value.body, state)); 851 | 852 | return chunks; 853 | }, 854 | 855 | ArrowFunctionExpression: scoped( 856 | (/** @type {ArrowFunctionExpression} */ node, state) => { 857 | const chunks = []; 858 | 859 | if (node.async) chunks.push(c('async ')); 860 | 861 | if (node.params.length === 1 && node.params[0].type === 'Identifier') { 862 | push_array(chunks, handle(node.params[0], state)); 863 | } else { 864 | const params = node.params.map((param) => 865 | handle(param, { 866 | ...state, 867 | indent: state.indent + '\t' 868 | }) 869 | ); 870 | 871 | chunks.push(c('(')); 872 | push_array(chunks, join(params, c(', '))); 873 | chunks.push(c(')')); 874 | } 875 | 876 | chunks.push(c(' => ')); 877 | 878 | if ( 879 | node.body.type === 'ObjectExpression' || 880 | (node.body.type === 'AssignmentExpression' && 881 | node.body.left.type === 'ObjectPattern') 882 | ) { 883 | chunks.push(c('(')); 884 | push_array(chunks, handle(node.body, state)); 885 | chunks.push(c(')')); 886 | } else { 887 | push_array(chunks, handle(node.body, state)); 888 | } 889 | 890 | return chunks; 891 | } 892 | ), 893 | 894 | ThisExpression(node, state) { 895 | return [c('this', node)]; 896 | }, 897 | 898 | Super(node, state) { 899 | return [c('super', node)]; 900 | }, 901 | 902 | RestElement(node, state) { 903 | return [c('...'), ...handle(node.argument, state)]; 904 | }, 905 | 906 | YieldExpression(node, state) { 907 | if (node.argument) { 908 | return [ 909 | c(node.delegate ? `yield* ` : `yield `), 910 | ...handle(node.argument, state) 911 | ]; 912 | } 913 | 914 | return [c(node.delegate ? `yield*` : `yield`)]; 915 | }, 916 | 917 | AwaitExpression(node, state) { 918 | if (node.argument) { 919 | const precedence = EXPRESSIONS_PRECEDENCE[node.argument.type]; 920 | 921 | if (precedence && precedence < EXPRESSIONS_PRECEDENCE.AwaitExpression) { 922 | return [c('await ('), ...handle(node.argument, state), c(')')]; 923 | } else { 924 | return [c('await '), ...handle(node.argument, state)]; 925 | } 926 | } 927 | 928 | return [c('await')]; 929 | }, 930 | 931 | TemplateLiteral(node, state) { 932 | const chunks = [c('`')]; 933 | 934 | const { quasis, expressions } = node; 935 | 936 | for (let i = 0; i < expressions.length; i++) { 937 | chunks.push(c(quasis[i].value.raw), c('${')); 938 | push_array(chunks, handle(expressions[i], state)); 939 | chunks.push(c('}')); 940 | } 941 | 942 | chunks.push(c(quasis[quasis.length - 1].value.raw), c('`')); 943 | 944 | return chunks; 945 | }, 946 | 947 | TaggedTemplateExpression(node, state) { 948 | return handle(node.tag, state).concat(handle(node.quasi, state)); 949 | }, 950 | 951 | ArrayExpression(node, state) { 952 | const chunks = [c('[')]; 953 | 954 | /** @type {Chunk[][]} */ 955 | const elements = []; 956 | 957 | /** @type {Chunk[]} */ 958 | let sparse_commas = []; 959 | 960 | for (let i = 0; i < node.elements.length; i += 1) { 961 | // can't use map/forEach because of sparse arrays 962 | const element = node.elements[i]; 963 | if (element) { 964 | elements.push([ 965 | ...sparse_commas, 966 | ...handle(element, { 967 | ...state, 968 | indent: state.indent + '\t' 969 | }) 970 | ]); 971 | sparse_commas = []; 972 | } else { 973 | sparse_commas.push(c(',')); 974 | } 975 | } 976 | 977 | const multiple_lines = 978 | elements.some(has_newline) || 979 | elements.map(get_length).reduce(sum, 0) + 980 | (state.indent.length + elements.length - 1) * 2 > 981 | 80; 982 | 983 | if (multiple_lines) { 984 | chunks.push(c(`\n${state.indent}\t`)); 985 | push_array(chunks, join(elements, c(`,\n${state.indent}\t`))); 986 | chunks.push(c(`\n${state.indent}`)); 987 | push_array(chunks, sparse_commas); 988 | } else { 989 | push_array(chunks, join(elements, c(', '))); 990 | push_array(chunks, sparse_commas); 991 | } 992 | 993 | chunks.push(c(']')); 994 | 995 | return chunks; 996 | }, 997 | 998 | ObjectExpression(/** @type {ObjectExpression} */ node, state) { 999 | if (node.properties.length === 0) { 1000 | return [c('{}')]; 1001 | } 1002 | 1003 | let has_inline_comment = false; 1004 | 1005 | /** @type {Chunk[]} */ 1006 | const chunks = []; 1007 | const separator = c(', '); 1008 | 1009 | node.properties.forEach((p, i) => { 1010 | push_array( 1011 | chunks, 1012 | handle(p, { 1013 | ...state, 1014 | indent: state.indent + '\t' 1015 | }) 1016 | ); 1017 | 1018 | if (state.comments.length) { 1019 | // TODO generalise this, so it works with ArrayExpressions and other things. 1020 | // At present, stuff will just get appended to the closest statement/declaration 1021 | chunks.push(c(', ')); 1022 | 1023 | while (state.comments.length) { 1024 | const comment = state.comments.shift(); 1025 | 1026 | chunks.push( 1027 | c( 1028 | comment.type === 'Block' 1029 | ? `/*${comment.value}*/\n${state.indent}\t` 1030 | : `//${comment.value}\n${state.indent}\t` 1031 | ) 1032 | ); 1033 | 1034 | if (comment.type === 'Line') { 1035 | has_inline_comment = true; 1036 | } 1037 | } 1038 | } else { 1039 | if (i < node.properties.length - 1) { 1040 | chunks.push(separator); 1041 | } 1042 | } 1043 | }); 1044 | 1045 | const multiple_lines = 1046 | has_inline_comment || has_newline(chunks) || get_length(chunks) > 40; 1047 | 1048 | if (multiple_lines) { 1049 | separator.content = `,\n${state.indent}\t`; 1050 | } 1051 | 1052 | return [ 1053 | c(multiple_lines ? `{\n${state.indent}\t` : `{ `), 1054 | ...chunks, 1055 | c(multiple_lines ? `\n${state.indent}}` : ` }`) 1056 | ]; 1057 | }, 1058 | 1059 | Property(node, state) { 1060 | const value = handle(node.value, state); 1061 | 1062 | if (node.key === node.value) { 1063 | return value; 1064 | } 1065 | 1066 | // special case 1067 | if ( 1068 | !node.computed && 1069 | node.value.type === 'AssignmentPattern' && 1070 | node.value.left.type === 'Identifier' && 1071 | node.value.left.name === node.key.name 1072 | ) { 1073 | return value; 1074 | } 1075 | 1076 | if ( 1077 | !node.computed && 1078 | node.value.type === 'Identifier' && 1079 | ((node.key.type === 'Identifier' && node.key.name === value[0].content) || 1080 | (node.key.type === 'Literal' && node.key.value === value[0].content)) 1081 | ) { 1082 | return value; 1083 | } 1084 | 1085 | const key = handle(node.key, state); 1086 | 1087 | if (node.value.type === 'FunctionExpression' && !node.value.id) { 1088 | state = { 1089 | ...state, 1090 | scope: state.scope_map.get(node.value) 1091 | }; 1092 | 1093 | const chunks = node.kind !== 'init' ? [c(`${node.kind} `)] : []; 1094 | 1095 | if (node.value.async) { 1096 | chunks.push(c('async ')); 1097 | } 1098 | if (node.value.generator) { 1099 | chunks.push(c('*')); 1100 | } 1101 | 1102 | push_array(chunks, node.computed ? [c('['), ...key, c(']')] : key); 1103 | chunks.push(c('(')); 1104 | push_array( 1105 | chunks, 1106 | join( 1107 | node.value.params.map((/** @type {Pattern} */ param) => 1108 | handle(param, state) 1109 | ), 1110 | c(', ') 1111 | ) 1112 | ); 1113 | chunks.push(c(') ')); 1114 | push_array(chunks, handle(node.value.body, state)); 1115 | 1116 | return chunks; 1117 | } 1118 | 1119 | if (node.computed) { 1120 | return [c('['), ...key, c(']: '), ...value]; 1121 | } 1122 | 1123 | return [...key, c(': '), ...value]; 1124 | }, 1125 | 1126 | ObjectPattern(node, state) { 1127 | const chunks = [c('{ ')]; 1128 | 1129 | for (let i = 0; i < node.properties.length; i += 1) { 1130 | push_array(chunks, handle(node.properties[i], state)); 1131 | if (i < node.properties.length - 1) chunks.push(c(', ')); 1132 | } 1133 | 1134 | chunks.push(c(' }')); 1135 | 1136 | return chunks; 1137 | }, 1138 | 1139 | SequenceExpression(/** @type {SequenceExpression} */ node, state) { 1140 | const expressions = node.expressions.map((e) => handle(e, state)); 1141 | 1142 | return [c('('), ...join(expressions, c(', ')), c(')')]; 1143 | }, 1144 | 1145 | UnaryExpression(node, state) { 1146 | const chunks = [c(node.operator)]; 1147 | 1148 | if (node.operator.length > 1) { 1149 | chunks.push(c(' ')); 1150 | } 1151 | 1152 | if ( 1153 | EXPRESSIONS_PRECEDENCE[node.argument.type] < 1154 | EXPRESSIONS_PRECEDENCE.UnaryExpression 1155 | ) { 1156 | chunks.push(c('(')); 1157 | push_array(chunks, handle(node.argument, state)); 1158 | chunks.push(c(')')); 1159 | } else { 1160 | push_array(chunks, handle(node.argument, state)); 1161 | } 1162 | 1163 | return chunks; 1164 | }, 1165 | 1166 | UpdateExpression(node, state) { 1167 | return node.prefix 1168 | ? [c(node.operator), ...handle(node.argument, state)] 1169 | : [...handle(node.argument, state), c(node.operator)]; 1170 | }, 1171 | 1172 | AssignmentExpression(node, state) { 1173 | return [ 1174 | ...handle(node.left, state), 1175 | c(` ${node.operator || '='} `), 1176 | ...handle(node.right, state) 1177 | ]; 1178 | }, 1179 | 1180 | BinaryExpression(node, state) { 1181 | /** 1182 | * @type any[] 1183 | */ 1184 | const chunks = []; 1185 | 1186 | // TODO 1187 | // const is_in = node.operator === 'in'; 1188 | // if (is_in) { 1189 | // // Avoids confusion in `for` loops initializers 1190 | // chunks.push(c('(')); 1191 | // } 1192 | 1193 | if (needs_parens(node.left, node, false)) { 1194 | chunks.push(c('(')); 1195 | push_array(chunks, handle(node.left, state)); 1196 | chunks.push(c(')')); 1197 | } else { 1198 | push_array(chunks, handle(node.left, state)); 1199 | } 1200 | 1201 | chunks.push(c(` ${node.operator} `)); 1202 | 1203 | if (needs_parens(node.right, node, true)) { 1204 | chunks.push(c('(')); 1205 | push_array(chunks, handle(node.right, state)); 1206 | chunks.push(c(')')); 1207 | } else { 1208 | push_array(chunks, handle(node.right, state)); 1209 | } 1210 | 1211 | return chunks; 1212 | }, 1213 | 1214 | ConditionalExpression(node, state) { 1215 | /** 1216 | * @type any[] 1217 | */ 1218 | const chunks = []; 1219 | 1220 | if ( 1221 | EXPRESSIONS_PRECEDENCE[node.test.type] > 1222 | EXPRESSIONS_PRECEDENCE.ConditionalExpression 1223 | ) { 1224 | push_array(chunks, handle(node.test, state)); 1225 | } else { 1226 | chunks.push(c('(')); 1227 | push_array(chunks, handle(node.test, state)); 1228 | chunks.push(c(')')); 1229 | } 1230 | 1231 | const child_state = { ...state, indent: state.indent + '\t' }; 1232 | 1233 | const consequent = handle(node.consequent, child_state); 1234 | const alternate = handle(node.alternate, child_state); 1235 | 1236 | const multiple_lines = 1237 | has_newline(consequent) || 1238 | has_newline(alternate) || 1239 | get_length(chunks) + get_length(consequent) + get_length(alternate) > 50; 1240 | 1241 | if (multiple_lines) { 1242 | chunks.push(c(`\n${state.indent}? `)); 1243 | push_array(chunks, consequent); 1244 | chunks.push(c(`\n${state.indent}: `)); 1245 | push_array(chunks, alternate); 1246 | } else { 1247 | chunks.push(c(` ? `)); 1248 | push_array(chunks, consequent); 1249 | chunks.push(c(` : `)); 1250 | push_array(chunks, alternate); 1251 | } 1252 | 1253 | return chunks; 1254 | }, 1255 | 1256 | NewExpression(/** @type {NewExpression} */ node, state) { 1257 | const chunks = [c('new ')]; 1258 | 1259 | if ( 1260 | EXPRESSIONS_PRECEDENCE[node.callee.type] < 1261 | EXPRESSIONS_PRECEDENCE.CallExpression || 1262 | has_call_expression(node.callee) 1263 | ) { 1264 | chunks.push(c('(')); 1265 | push_array(chunks, handle(node.callee, state)); 1266 | chunks.push(c(')')); 1267 | } else { 1268 | push_array(chunks, handle(node.callee, state)); 1269 | } 1270 | 1271 | // TODO this is copied from CallExpression — DRY it out 1272 | const args = node.arguments.map((arg) => 1273 | handle(arg, { 1274 | ...state, 1275 | indent: state.indent + '\t' 1276 | }) 1277 | ); 1278 | 1279 | const separator = args.some(has_newline) // TODO or length exceeds 80 1280 | ? c(',\n' + state.indent) 1281 | : c(', '); 1282 | 1283 | chunks.push(c('(')); 1284 | push_array(chunks, join(args, separator)); 1285 | chunks.push(c(')')); 1286 | 1287 | return chunks; 1288 | }, 1289 | 1290 | ChainExpression(node, state) { 1291 | return handle(node.expression, state); 1292 | }, 1293 | 1294 | CallExpression(/** @type {CallExpression} */ node, state) { 1295 | /** 1296 | * @type any[] 1297 | */ 1298 | const chunks = []; 1299 | 1300 | if ( 1301 | EXPRESSIONS_PRECEDENCE[node.callee.type] < 1302 | EXPRESSIONS_PRECEDENCE.CallExpression 1303 | ) { 1304 | chunks.push(c('(')); 1305 | push_array(chunks, handle(node.callee, state)); 1306 | chunks.push(c(')')); 1307 | } else { 1308 | push_array(chunks, handle(node.callee, state)); 1309 | } 1310 | 1311 | if (/** @type {SimpleCallExpression} */ (node).optional) { 1312 | chunks.push(c('?.')); 1313 | } 1314 | 1315 | let has_inline_comment = false; 1316 | let arg_chunks = []; 1317 | outer: for (const arg of node.arguments) { 1318 | const chunks = []; 1319 | while (state.comments.length) { 1320 | const comment = state.comments.shift(); 1321 | if (comment.type === 'Line') { 1322 | has_inline_comment = true; 1323 | break outer; 1324 | } 1325 | chunks.push( 1326 | c( 1327 | comment.type === 'Block' 1328 | ? `/*${comment.value}*/ ` 1329 | : `//${comment.value}` 1330 | ) 1331 | ); 1332 | } 1333 | push_array(chunks, handle(arg, state)); 1334 | arg_chunks.push(chunks); 1335 | } 1336 | 1337 | const multiple_lines = 1338 | has_inline_comment || arg_chunks.slice(0, -1).some(has_newline); // TODO or length exceeds 80 1339 | if (multiple_lines) { 1340 | // need to handle args again. TODO find alternative approach? 1341 | const args = node.arguments.map((arg, i) => { 1342 | const chunks = handle(arg, { 1343 | ...state, 1344 | indent: `${state.indent}\t` 1345 | }); 1346 | if (i < node.arguments.length - 1) chunks.push(c(',')); 1347 | while (state.comments.length) { 1348 | const comment = state.comments.shift(); 1349 | chunks.push( 1350 | c( 1351 | comment.type === 'Block' 1352 | ? ` /*${comment.value}*/ ` 1353 | : ` //${comment.value}` 1354 | ) 1355 | ); 1356 | } 1357 | return chunks; 1358 | }); 1359 | 1360 | chunks.push(c(`(\n${state.indent}\t`)); 1361 | push_array(chunks, join(args, c(`\n${state.indent}\t`))); 1362 | chunks.push(c(`\n${state.indent})`)); 1363 | } else { 1364 | chunks.push(c('(')); 1365 | push_array(chunks, join(arg_chunks, c(', '))); 1366 | chunks.push(c(')')); 1367 | } 1368 | 1369 | return chunks; 1370 | }, 1371 | 1372 | MemberExpression(node, state) { 1373 | /** 1374 | * @type any[] 1375 | */ 1376 | const chunks = []; 1377 | 1378 | if ( 1379 | EXPRESSIONS_PRECEDENCE[node.object.type] < 1380 | EXPRESSIONS_PRECEDENCE.MemberExpression 1381 | ) { 1382 | chunks.push(c('(')); 1383 | push_array(chunks, handle(node.object, state)); 1384 | chunks.push(c(')')); 1385 | } else { 1386 | push_array(chunks, handle(node.object, state)); 1387 | } 1388 | 1389 | if (node.computed) { 1390 | if (node.optional) { 1391 | chunks.push(c('?.')); 1392 | } 1393 | chunks.push(c('[')); 1394 | push_array(chunks, handle(node.property, state)); 1395 | chunks.push(c(']')); 1396 | } else { 1397 | chunks.push(c(node.optional ? '?.' : '.')); 1398 | push_array(chunks, handle(node.property, state)); 1399 | } 1400 | 1401 | return chunks; 1402 | }, 1403 | 1404 | MetaProperty(node, state) { 1405 | return [ 1406 | ...handle(node.meta, state), 1407 | c('.'), 1408 | ...handle(node.property, state) 1409 | ]; 1410 | }, 1411 | 1412 | Identifier(node, state) { 1413 | let name = node.name; 1414 | 1415 | if (name[0] === '@') { 1416 | name = state.getName(name.slice(1)); 1417 | } else if (node.name[0] === '#') { 1418 | const owner = state.scope.find_owner(node.name); 1419 | 1420 | if (!owner) { 1421 | throw new Error(`Could not find owner for node`); 1422 | } 1423 | 1424 | if (!state.deconflicted.has(owner)) { 1425 | state.deconflicted.set(owner, new Map()); 1426 | } 1427 | 1428 | const deconflict_map = state.deconflicted.get(owner); 1429 | 1430 | if (!deconflict_map.has(node.name)) { 1431 | deconflict_map.set( 1432 | node.name, 1433 | deconflict(node.name.slice(1), owner.references) 1434 | ); 1435 | } 1436 | 1437 | name = deconflict_map.get(node.name); 1438 | } 1439 | 1440 | return [c(name, node)]; 1441 | }, 1442 | 1443 | Literal(/** @type {Literal} */ node, state) { 1444 | if (typeof node.value === 'string') { 1445 | return [ 1446 | // TODO do we need to handle weird unicode characters somehow? 1447 | // str.replace(/\\u(\d{4})/g, (m, n) => String.fromCharCode(+n)) 1448 | c( 1449 | (node.raw || JSON.stringify(node.value)).replace( 1450 | re, 1451 | (_m, _i, at, hash, name) => { 1452 | if (at) return '@' + name; 1453 | if (hash) return '#' + name; 1454 | throw new Error(`this shouldn't happen`); 1455 | } 1456 | ), 1457 | node 1458 | ) 1459 | ]; 1460 | } 1461 | 1462 | return [c(node.raw || String(node.value), node)]; 1463 | }, 1464 | 1465 | PropertyDefinition(/** @type {PropertyDefinition} */ node, state) { 1466 | const chunks = []; 1467 | 1468 | if (node.static) { 1469 | chunks.push(c('static ')); 1470 | } 1471 | 1472 | if (node.computed) { 1473 | chunks.push(c('['), ...handle(node.key, state), c(']')); 1474 | } else { 1475 | chunks.push(...handle(node.key, state)); 1476 | } 1477 | 1478 | if (node.value) { 1479 | chunks.push(c(' = ')); 1480 | 1481 | chunks.push(...handle(node.value, state)); 1482 | } 1483 | 1484 | chunks.push(c(';')); 1485 | 1486 | return chunks; 1487 | }, 1488 | 1489 | StaticBlock(/** @type {StaticBlock} */ node, state) { 1490 | const chunks = [c('static ')]; 1491 | 1492 | push_array(chunks, handlers.BlockStatement(node, state)); 1493 | 1494 | return chunks; 1495 | }, 1496 | 1497 | PrivateIdentifier(/** @type {PrivateIdenifier} */ node, state) { 1498 | const chunks = [c('#')]; 1499 | 1500 | push_array(chunks, [c(node.name, node)]); 1501 | 1502 | return chunks; 1503 | } 1504 | }; 1505 | 1506 | handlers.ForOfStatement = handlers.ForInStatement; 1507 | handlers.FunctionExpression = handlers.FunctionDeclaration; 1508 | handlers.ClassExpression = handlers.ClassDeclaration; 1509 | handlers.ClassBody = handlers.BlockStatement; 1510 | handlers.SpreadElement = handlers.RestElement; 1511 | handlers.ArrayPattern = handlers.ArrayExpression; 1512 | handlers.LogicalExpression = handlers.BinaryExpression; 1513 | handlers.AssignmentPattern = handlers.AssignmentExpression; 1514 | -------------------------------------------------------------------------------- /src/print/index.js: -------------------------------------------------------------------------------- 1 | import * as perisopic from 'periscopic'; 2 | import { handle } from './handlers.js'; 3 | import { encode } from '@jridgewell/sourcemap-codec'; 4 | 5 | /** @type {(str?: string) => string} str */ 6 | let btoa = () => { 7 | throw new Error( 8 | 'Unsupported environment: `window.btoa` or `Buffer` should be supported.' 9 | ); 10 | }; 11 | 12 | if (typeof window !== 'undefined' && typeof window.btoa === 'function') { 13 | btoa = (str) => window.btoa(unescape(encodeURIComponent(str))); 14 | } else if (typeof Buffer === 'function') { 15 | btoa = (str) => Buffer.from(str, 'utf-8').toString('base64'); 16 | } 17 | 18 | /** @typedef {import('estree').Node} Node */ 19 | 20 | /** 21 | * @typedef {{ 22 | * file?: string; 23 | * sourceMapSource?: string; 24 | * sourceMapContent?: string; 25 | * sourceMapEncodeMappings?: boolean; // default true 26 | * getName?: (name: string) => string; 27 | * }} PrintOptions 28 | */ 29 | 30 | /** 31 | * @param {Node} node 32 | * @param {PrintOptions} opts 33 | * @returns {{ code: string, map: any }} // TODO 34 | */ 35 | export function print(node, opts = {}) { 36 | if (Array.isArray(node)) { 37 | return print( 38 | { 39 | type: 'Program', 40 | body: node, 41 | sourceType: 'module' 42 | }, 43 | opts 44 | ); 45 | } 46 | 47 | const { 48 | getName = /** @param {string} x */ (x) => { 49 | throw new Error(`Unhandled sigil @${x}`); 50 | } 51 | } = opts; 52 | 53 | let { map: scope_map, scope } = perisopic.analyze(node); 54 | const deconflicted = new WeakMap(); 55 | 56 | const chunks = handle(node, { 57 | indent: '', 58 | getName, 59 | scope, 60 | scope_map, 61 | deconflicted, 62 | comments: [] 63 | }); 64 | 65 | /** @typedef {[number, number, number, number]} Segment */ 66 | 67 | let code = ''; 68 | let current_column = 0; 69 | 70 | /** @type {Segment[][]} */ 71 | let mappings = []; 72 | 73 | /** @type {Segment[]} */ 74 | let current_line = []; 75 | 76 | for (let i = 0; i < chunks.length; i += 1) { 77 | const chunk = chunks[i]; 78 | 79 | code += chunk.content; 80 | 81 | if (chunk.loc) { 82 | current_line.push([ 83 | current_column, 84 | 0, // source index is always zero 85 | chunk.loc.start.line - 1, 86 | chunk.loc.start.column 87 | ]); 88 | } 89 | 90 | for (let i = 0; i < chunk.content.length; i += 1) { 91 | if (chunk.content[i] === '\n') { 92 | mappings.push(current_line); 93 | current_line = []; 94 | current_column = 0; 95 | } else { 96 | current_column += 1; 97 | } 98 | } 99 | 100 | if (chunk.loc) { 101 | current_line.push([ 102 | current_column, 103 | 0, // source index is always zero 104 | chunk.loc.end.line - 1, 105 | chunk.loc.end.column 106 | ]); 107 | } 108 | } 109 | 110 | mappings.push(current_line); 111 | 112 | const map = { 113 | version: 3, 114 | /** @type {string[]} */ 115 | names: [], 116 | sources: [opts.sourceMapSource || null], 117 | sourcesContent: [opts.sourceMapContent || null], 118 | mappings: 119 | opts.sourceMapEncodeMappings == undefined || opts.sourceMapEncodeMappings 120 | ? encode(mappings) 121 | : mappings 122 | }; 123 | 124 | Object.defineProperties(map, { 125 | toString: { 126 | enumerable: false, 127 | value: function toString() { 128 | return JSON.stringify(this); 129 | } 130 | }, 131 | toUrl: { 132 | enumerable: false, 133 | value: function toUrl() { 134 | return ( 135 | 'data:application/json;charset=utf-8;base64,' + btoa(this.toString()) 136 | ); 137 | } 138 | } 139 | }); 140 | 141 | return { 142 | code, 143 | map 144 | }; 145 | } 146 | -------------------------------------------------------------------------------- /src/utils/comments.js: -------------------------------------------------------------------------------- 1 | import { re } from './id.js'; 2 | 3 | /** @typedef {import('estree').Comment} Comment */ 4 | /** @typedef {import('estree').Node} Node */ 5 | 6 | /** 7 | * @typedef {Node & { 8 | * start: number; 9 | * end: number; 10 | * has_trailing_newline?: boolean 11 | * }} NodeWithLocation 12 | */ 13 | 14 | /** 15 | * @typedef {Comment & { 16 | * start: number; 17 | * end: number; 18 | * has_trailing_newline?: boolean 19 | * }} CommentWithLocation 20 | */ 21 | 22 | /** 23 | * @param {CommentWithLocation[]} comments 24 | * @param {string} raw 25 | */ 26 | export const get_comment_handlers = (comments, raw) => ({ 27 | // pass to acorn options 28 | /** 29 | * @param {boolean} block 30 | * @param {string} value 31 | * @param {number} start 32 | * @param {number} end 33 | */ 34 | onComment: (block, value, start, end) => { 35 | if (block && /\n/.test(value)) { 36 | let a = start; 37 | while (a > 0 && raw[a - 1] !== '\n') a -= 1; 38 | 39 | let b = a; 40 | while (/[ \t]/.test(raw[b])) b += 1; 41 | 42 | const indentation = raw.slice(a, b); 43 | value = value.replace(new RegExp(`^${indentation}`, 'gm'), ''); 44 | } 45 | 46 | comments.push({ type: block ? 'Block' : 'Line', value, start, end }); 47 | }, 48 | 49 | // pass to estree-walker options 50 | /** @param {NodeWithLocation} node */ 51 | enter(node) { 52 | let comment; 53 | 54 | while (comments[0] && comments[0].start < node.start) { 55 | comment = comments.shift(); 56 | 57 | comment.value = comment.value.replace( 58 | re, 59 | (match, id, at, hash, value) => { 60 | if (hash) return `#${value}`; 61 | if (at) return `@${value}`; 62 | 63 | return match; 64 | } 65 | ); 66 | 67 | const next = comments[0] || node; 68 | comment.has_trailing_newline = 69 | comment.type === 'Line' || 70 | /\n/.test(raw.slice(comment.end, next.start)); 71 | 72 | (node.leadingComments || (node.leadingComments = [])).push(comment); 73 | } 74 | }, 75 | 76 | /** @param {NodeWithLocation} node */ 77 | leave(node) { 78 | if (comments[0]) { 79 | const slice = raw.slice(node.end, comments[0].start); 80 | 81 | if (/^[,) \t]*$/.test(slice)) { 82 | node.trailingComments = [comments.shift()]; 83 | } 84 | } 85 | } 86 | }); 87 | -------------------------------------------------------------------------------- /src/utils/id.js: -------------------------------------------------------------------------------- 1 | // generate an ID that is, to all intents and purposes, unique 2 | export const id = Math.round(Math.random() * 1e20).toString(36); 3 | export const re = new RegExp(`_${id}_(?:(\\d+)|(AT)|(HASH))_(\\w+)?`, 'g'); 4 | -------------------------------------------------------------------------------- /src/utils/push_array.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Does `array.push` for all `items`. Needed because `array.push(...items)` throws 3 | * "Maximum call stack size exceeded" when `items` is too big of an array. 4 | * 5 | * @param {any[]} array 6 | * @param {any[]} items 7 | */ 8 | export function push_array(array, items) { 9 | for (let i = 0; i < items.length; i++) { 10 | array.push(items[i]); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /test/samples/array-expressions/expected.js: -------------------------------------------------------------------------------- 1 | a = [1, 2, 3]; 2 | 3 | b = [ 4 | 'the quick brown fox jumps over the lazy dog', 5 | 'the quick brown fox jumps over the lazy dog', 6 | 'the quick brown fox jumps over the lazy dog', 7 | 4 8 | ]; 9 | 10 | sparse = [,,,]; -------------------------------------------------------------------------------- /test/samples/array-expressions/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;;;;;;;;" 11 | } -------------------------------------------------------------------------------- /test/samples/array-expressions/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | a = [1, 2, 3]; 3 | 4 | b = ['the quick brown fox jumps over the lazy dog', 'the quick brown fox jumps over the lazy dog', 'the quick brown fox jumps over the lazy dog', 4]; 5 | 6 | sparse = [,,,]; 7 | `; -------------------------------------------------------------------------------- /test/samples/arrow-function-as-statement/expected.js: -------------------------------------------------------------------------------- 1 | () => { 2 | 3 | }; -------------------------------------------------------------------------------- /test/samples/arrow-function-as-statement/expected.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"names":[],"sources":["input.js"],"sourcesContent":[null],"mappings":";;"} -------------------------------------------------------------------------------- /test/samples/arrow-function-as-statement/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b`() => {};`; -------------------------------------------------------------------------------- /test/samples/arrow-function-assignment-object-pattern/expected.js: -------------------------------------------------------------------------------- 1 | let a = () => ({ x } = { x: 42 }); -------------------------------------------------------------------------------- /test/samples/arrow-function-assignment-object-pattern/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": "" 11 | } -------------------------------------------------------------------------------- /test/samples/arrow-function-assignment-object-pattern/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b`let a = () => ({ x } = { x: 42 });`; 2 | -------------------------------------------------------------------------------- /test/samples/arrow-function-parenthesized/expected.js: -------------------------------------------------------------------------------- 1 | foo || (bar => bar); -------------------------------------------------------------------------------- /test/samples/arrow-function-parenthesized/expected.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"names":[],"sources":["input.js"],"sourcesContent":[null],"mappings":""} -------------------------------------------------------------------------------- /test/samples/arrow-function-parenthesized/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b, x }) => b`foo || ${x`bar => bar`}`; -------------------------------------------------------------------------------- /test/samples/at-prefix/expected.js: -------------------------------------------------------------------------------- 1 | FOO(bar) -------------------------------------------------------------------------------- /test/samples/at-prefix/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": "" 11 | } -------------------------------------------------------------------------------- /test/samples/at-prefix/input.js: -------------------------------------------------------------------------------- 1 | export default ({ x }) => x`@foo(bar)`; -------------------------------------------------------------------------------- /test/samples/await-precedence/expected.js: -------------------------------------------------------------------------------- 1 | await (a || b); 2 | await c; -------------------------------------------------------------------------------- /test/samples/await-precedence/expected.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"names":[],"sources":["input.js"],"sourcesContent":[null],"mappings":";"} -------------------------------------------------------------------------------- /test/samples/await-precedence/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | await (a || b); 3 | await c; 4 | `; -------------------------------------------------------------------------------- /test/samples/basic/expected.js: -------------------------------------------------------------------------------- 1 | foo({ a: 1 }); 2 | "#foo"; 3 | "@foo"; -------------------------------------------------------------------------------- /test/samples/basic/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;" 11 | } -------------------------------------------------------------------------------- /test/samples/basic/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | foo({ a: 1 }); 3 | "#foo"; 4 | "@foo"; 5 | `; -------------------------------------------------------------------------------- /test/samples/bigint/expected.js: -------------------------------------------------------------------------------- 1 | 12345n -------------------------------------------------------------------------------- /test/samples/bigint/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": "" 11 | } -------------------------------------------------------------------------------- /test/samples/bigint/input.js: -------------------------------------------------------------------------------- 1 | export default ({ x }) => x`12345n`; -------------------------------------------------------------------------------- /test/samples/break-continue/expected.js: -------------------------------------------------------------------------------- 1 | x: for (let i = 0; i < 10; i += 1) { 2 | if (should_break) { 3 | break; 4 | } 5 | 6 | if (should_break_with_label) { 7 | break x; 8 | } 9 | 10 | if (should_continue) { 11 | continue; 12 | } 13 | 14 | if (should_continue_with_label) { 15 | continue x; 16 | } 17 | } -------------------------------------------------------------------------------- /test/samples/break-continue/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;;;;;;;;;;;;;;;" 11 | } -------------------------------------------------------------------------------- /test/samples/break-continue/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | x: for (let i = 0; i < 10; i += 1) { 3 | if (should_break) { 4 | break; 5 | } 6 | 7 | if (should_break_with_label) { 8 | break x; 9 | } 10 | 11 | if (should_continue) { 12 | continue; 13 | } 14 | 15 | if (should_continue_with_label) { 16 | continue x; 17 | } 18 | } 19 | `; -------------------------------------------------------------------------------- /test/samples/call-expressions/expected.js: -------------------------------------------------------------------------------- 1 | x(a, b, c); 2 | 3 | x(a, b, () => { 4 | console.log('c'); 5 | }); 6 | 7 | x( 8 | a, 9 | () => { 10 | console.log('b'); 11 | }, 12 | () => { 13 | console.log('c'); 14 | } 15 | ); -------------------------------------------------------------------------------- /test/samples/call-expressions/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;;;;;;;;;;;;;" 11 | } -------------------------------------------------------------------------------- /test/samples/call-expressions/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | x(a, b, c); 3 | 4 | x(a, b, () => { 5 | console.log('c'); 6 | }); 7 | 8 | x(a, () => { 9 | console.log('b'); 10 | }, () => { 11 | console.log('c'); 12 | }); 13 | `; -------------------------------------------------------------------------------- /test/samples/chain-expressions/expected.js: -------------------------------------------------------------------------------- 1 | foo?.bar.baz; 2 | x?.(a, b, c); 3 | x()?.(); -------------------------------------------------------------------------------- /test/samples/chain-expressions/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;" 11 | } -------------------------------------------------------------------------------- /test/samples/chain-expressions/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | foo?.bar.baz; 3 | x?.(a, b, c); 4 | x()?.() 5 | `; 6 | -------------------------------------------------------------------------------- /test/samples/class-private/expected.js: -------------------------------------------------------------------------------- 1 | class A { 2 | #p; 3 | static #p1; 4 | static #p2; 5 | 6 | static is(obj) { 7 | return #p2 in obj; 8 | } 9 | 10 | get #p3() { 11 | 12 | } 13 | 14 | set #p3(v) { 15 | 16 | } 17 | 18 | #m1() { 19 | return this.#p1; 20 | } 21 | 22 | static #m2() { 23 | 24 | } 25 | 26 | *#m3() { 27 | 28 | } 29 | 30 | async #m4() { 31 | 32 | } 33 | } -------------------------------------------------------------------------------- /test/samples/class-private/expected.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"names":[],"sources":["input.js"],"sourcesContent":[null],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"} -------------------------------------------------------------------------------- /test/samples/class-private/input.js: -------------------------------------------------------------------------------- 1 | export default ({ parse }) => parse(`class A { 2 | #p 3 | static #p1 4 | static #p2 5 | 6 | static is(obj) { 7 | return #p2 in obj 8 | } 9 | 10 | get #p3() {} 11 | set #p3(v) {} 12 | #m1() { 13 | return this.#p1 14 | } 15 | static #m2() {} 16 | *#m3() {} 17 | async #m4() {} 18 | }`); -------------------------------------------------------------------------------- /test/samples/class-property/expected.js: -------------------------------------------------------------------------------- 1 | class Foo { 2 | hi; 3 | static foo = 1; 4 | [KEY] = 2; 5 | } -------------------------------------------------------------------------------- /test/samples/class-property/expected.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"names":[],"sources":["input.js"],"sourcesContent":[null],"mappings":";;;;"} -------------------------------------------------------------------------------- /test/samples/class-property/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | class Foo { 3 | hi 4 | static foo = 1; 5 | [KEY] = 2 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /test/samples/class-static-block/expected.js: -------------------------------------------------------------------------------- 1 | class Foo { 2 | static { 3 | this.abc = 1; 4 | } 5 | } -------------------------------------------------------------------------------- /test/samples/class-static-block/expected.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"names":[],"sources":["input.js"],"sourcesContent":[null],"mappings":";;;;"} -------------------------------------------------------------------------------- /test/samples/class-static-block/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | class Foo { 3 | static { 4 | this.abc = 1; 5 | } 6 | } 7 | `; -------------------------------------------------------------------------------- /test/samples/comment-block-with-sigil/expected.js: -------------------------------------------------------------------------------- 1 | a = /* #b */ c; -------------------------------------------------------------------------------- /test/samples/comment-block-with-sigil/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": "" 11 | } 12 | -------------------------------------------------------------------------------- /test/samples/comment-block-with-sigil/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | a = /* #b */ c 3 | `; -------------------------------------------------------------------------------- /test/samples/comment-block/expected.js: -------------------------------------------------------------------------------- 1 | /* comment before a node 2 | * second line */ 3 | console.log(1); 4 | 5 | console.log(2); /* comment on same line as node */ 6 | 7 | const obj = { 8 | foo: 1, /* comment in middle of object */ 9 | bar: 2 10 | }; -------------------------------------------------------------------------------- /test/samples/comment-block/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;;;;;;;;" 11 | } -------------------------------------------------------------------------------- /test/samples/comment-block/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | /* comment before a node 3 | * second line */ 4 | console.log(1); 5 | 6 | console.log(2); /* comment on same line as node */ 7 | 8 | const obj = { 9 | foo: 1, /* comment in middle of object */ 10 | bar: 2 11 | }; 12 | `; -------------------------------------------------------------------------------- /test/samples/comment-inline-inserted/expected.js: -------------------------------------------------------------------------------- 1 | // comment before an inserted block 2 | "use strict"; 3 | 4 | // comment before an inserted node 5 | import { foo } from "wherever"; -------------------------------------------------------------------------------- /test/samples/comment-inline-inserted/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;;;" 11 | } -------------------------------------------------------------------------------- /test/samples/comment-inline-inserted/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b, x }) => { 2 | const insert = b`"use strict";`; 3 | 4 | const node = { 5 | type: 'ImportDeclaration', 6 | specifiers: [{ 7 | type: 'ImportSpecifier', 8 | local: { type: 'Identifier', name: 'foo' }, 9 | imported: { type: 'Identifier', name: 'foo' } 10 | }], 11 | source: { type: 'Literal', value: 'wherever' } 12 | }; 13 | 14 | return b` 15 | // comment before an inserted block 16 | ${insert} 17 | 18 | // comment before an inserted node 19 | ${node} 20 | `; 21 | }; -------------------------------------------------------------------------------- /test/samples/comment-inline/expected.js: -------------------------------------------------------------------------------- 1 | // comment before a node 2 | // second line 3 | console.log(1); 4 | 5 | console.log(2); // comment on same line as node 6 | 7 | const obj = { 8 | foo: 1, // comment in middle of object 9 | bar: 2 10 | }; 11 | 12 | function bar() { 13 | return /*result*/ foo; 14 | } -------------------------------------------------------------------------------- /test/samples/comment-inline/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;;;;;;;;;;;;" 11 | } -------------------------------------------------------------------------------- /test/samples/comment-inline/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | // comment before a node 3 | // second line 4 | console.log(1); 5 | 6 | console.log(2); // comment on same line as node 7 | 8 | const obj = { 9 | foo: 1, // comment in middle of object 10 | bar: 2 11 | }; 12 | 13 | function bar () { 14 | return /*result*/ foo; 15 | } 16 | `; -------------------------------------------------------------------------------- /test/samples/comment-interpolated/expected.js: -------------------------------------------------------------------------------- 1 | a = /* the answer */ 42 -------------------------------------------------------------------------------- /test/samples/comment-interpolated/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": "" 11 | } -------------------------------------------------------------------------------- /test/samples/comment-interpolated/input.js: -------------------------------------------------------------------------------- 1 | export default ({ x }) => x`a = /* ${"the answer"} */ ${42}` -------------------------------------------------------------------------------- /test/samples/comment-mixed-trailing/expected.js: -------------------------------------------------------------------------------- 1 | function foo() { 2 | 3 | } // hey1 4 | /* 5 | hey2 6 | */ -------------------------------------------------------------------------------- /test/samples/comment-mixed-trailing/expected.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"names":[],"sources":["input.js"],"sourcesContent":[null],"mappings":";;;;;"} -------------------------------------------------------------------------------- /test/samples/comment-mixed-trailing/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | function foo() { 3 | // hey1 4 | /* 5 | hey2 6 | */ 7 | } 8 | `; -------------------------------------------------------------------------------- /test/samples/comment-within-call-expression/expected.js: -------------------------------------------------------------------------------- 1 | console.log(null, /* xxx */ new Date(), /** zzz */ a.b.c()); // www 2 | 3 | console.log(null, // foo 4 | new Date()); 5 | 6 | console.log( 7 | null, // foo 8 | new Date() 9 | ); 10 | 11 | console.log(null, /* xxx */ function (a, b) { 12 | // yyy 13 | return a + b; 14 | }); 15 | 16 | console.log("1"); -------------------------------------------------------------------------------- /test/samples/comment-within-call-expression/expected.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"names":[],"sources":["input.js"],"sourcesContent":[null],"mappings":";;;;;;;;;;;;;;;"} -------------------------------------------------------------------------------- /test/samples/comment-within-call-expression/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | console.log(null, /* xxx */ new Date(), /** zzz */ a.b.c()); // www 3 | console.log(null, 4 | // foo 5 | new Date() 6 | ); 7 | console.log(null, // foo 8 | new Date() 9 | ); 10 | console.log(null, /* xxx */ function (a, b) { 11 | // yyy 12 | return a + b; 13 | }) 14 | console.log("1"); 15 | `; -------------------------------------------------------------------------------- /test/samples/comment-within-parentheses/expected.js: -------------------------------------------------------------------------------- 1 | function foo() { 2 | return (// hey 3 | abc); 4 | } 5 | 6 | function bar() { 7 | return (/* hey */ 8 | abc); 9 | } -------------------------------------------------------------------------------- /test/samples/comment-within-parentheses/expected.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"names":[],"sources":["input.js"],"sourcesContent":[null],"mappings":";;;;;;;;"} -------------------------------------------------------------------------------- /test/samples/comment-within-parentheses/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | function foo() { 3 | return ( 4 | // hey 5 | abc 6 | ) 7 | } 8 | function bar() { 9 | return ( 10 | /* hey */ 11 | abc 12 | ) 13 | } 14 | `; -------------------------------------------------------------------------------- /test/samples/deconflict-let/expected.js: -------------------------------------------------------------------------------- 1 | function foo() { 2 | let i = 2; 3 | 4 | for (var i$1 = 0; i$1 < 10; i$1 += 1) { 5 | console.log(i$1 * i); 6 | } 7 | } -------------------------------------------------------------------------------- /test/samples/deconflict-let/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;;;;;" 11 | } -------------------------------------------------------------------------------- /test/samples/deconflict-let/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | function foo() { 3 | let i = 2; 4 | 5 | for (var #i = 0; #i <10; #i += 1) { 6 | console.log(#i * i); 7 | } 8 | }`; -------------------------------------------------------------------------------- /test/samples/deconflict-method/expected.js: -------------------------------------------------------------------------------- 1 | obj = { 2 | a(foo) { 3 | 4 | } 5 | }; -------------------------------------------------------------------------------- /test/samples/deconflict-method/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;;;" 11 | } -------------------------------------------------------------------------------- /test/samples/deconflict-method/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b, x }) => b` 2 | obj = { 3 | a(#foo) {} 4 | };`; -------------------------------------------------------------------------------- /test/samples/destructured-declaration/expected.js: -------------------------------------------------------------------------------- 1 | const { answer = 42 } = life_the_universe_and_everything; -------------------------------------------------------------------------------- /test/samples/destructured-declaration/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": "" 11 | } -------------------------------------------------------------------------------- /test/samples/destructured-declaration/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | const { answer = 42 } = life_the_universe_and_everything; 3 | `; -------------------------------------------------------------------------------- /test/samples/empty-body/expected.js: -------------------------------------------------------------------------------- 1 | while (a) ; 2 | do ; while (a); 3 | for (; ; ) ; 4 | if (a) ; 5 | if (a) ; else ; 6 | if (a) ; else if (b) ; else ; -------------------------------------------------------------------------------- /test/samples/empty-body/expected.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"names":[],"sources":["input.js"],"sourcesContent":[null],"mappings":";;;;;"} -------------------------------------------------------------------------------- /test/samples/empty-body/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | while(a); 3 | do;while (a); 4 | for(;;) ; 5 | if (a) ; 6 | if (a) ; else ; 7 | if (a) ; else if (b) ; else ; 8 | `; -------------------------------------------------------------------------------- /test/samples/export/expected.js: -------------------------------------------------------------------------------- 1 | const foo = 42; 2 | export { foo }; -------------------------------------------------------------------------------- /test/samples/export/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";" 11 | } -------------------------------------------------------------------------------- /test/samples/export/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | const #foo = 42; 3 | export { #foo as foo };` -------------------------------------------------------------------------------- /test/samples/function-declaration/expected.js: -------------------------------------------------------------------------------- 1 | function foo() { 2 | bar 3 | } 4 | 5 | function foo$1() { 6 | bar 7 | } -------------------------------------------------------------------------------- /test/samples/function-declaration/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;;;;;" 11 | } -------------------------------------------------------------------------------- /test/samples/function-declaration/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | function foo() { 3 | bar; 4 | } 5 | function #foo() { 6 | bar; 7 | }` -------------------------------------------------------------------------------- /test/samples/hash-prefix-arrow/expected.js: -------------------------------------------------------------------------------- 1 | const foo = bar => bar * 2; -------------------------------------------------------------------------------- /test/samples/hash-prefix-arrow/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": "" 11 | } -------------------------------------------------------------------------------- /test/samples/hash-prefix-arrow/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b`const foo = #bar => #bar * 2`; -------------------------------------------------------------------------------- /test/samples/hash-prefix-for-loop-head/expected.js: -------------------------------------------------------------------------------- 1 | for (let i$1 = 0; i$1 < 10; i$1 += 1) { 2 | console.log(i * i$1); 3 | } -------------------------------------------------------------------------------- /test/samples/hash-prefix-for-loop-head/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;" 11 | } -------------------------------------------------------------------------------- /test/samples/hash-prefix-for-loop-head/input.js: -------------------------------------------------------------------------------- 1 | export default ({ x, b }) => { 2 | const i = x`i`; 3 | 4 | return b` 5 | for (let #i = 0; #i < 10; #i += 1) { 6 | console.log(${i} * #i); 7 | }`; 8 | }; -------------------------------------------------------------------------------- /test/samples/hash-prefix-reused/expected.js: -------------------------------------------------------------------------------- 1 | function foo(bar$1) { 2 | const bar = 'x'; 3 | bar$1 += 1; 4 | 5 | return bar => { 6 | console.log(bar); 7 | }; 8 | } -------------------------------------------------------------------------------- /test/samples/hash-prefix-reused/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;;;;;;" 11 | } -------------------------------------------------------------------------------- /test/samples/hash-prefix-reused/input.js: -------------------------------------------------------------------------------- 1 | export default ({ x }) => { 2 | const bar = x`#bar`; 3 | 4 | return x` 5 | function foo(#bar) { 6 | const bar = 'x'; 7 | 8 | ${bar} += 1; 9 | 10 | return (#bar) => { 11 | console.log(${bar}); 12 | }; 13 | } 14 | `; 15 | }; -------------------------------------------------------------------------------- /test/samples/hash-prefix/expected.js: -------------------------------------------------------------------------------- 1 | function foo(bar$1) { 2 | return bar$1 * bar; 3 | } -------------------------------------------------------------------------------- /test/samples/hash-prefix/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;" 11 | } -------------------------------------------------------------------------------- /test/samples/hash-prefix/input.js: -------------------------------------------------------------------------------- 1 | export default ({ x }) => x` 2 | function foo(#bar) { 3 | return #bar * bar; 4 | }`; -------------------------------------------------------------------------------- /test/samples/import-as/expected.js: -------------------------------------------------------------------------------- 1 | import { foo as bar } from 'x'; -------------------------------------------------------------------------------- /test/samples/import-as/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": "" 11 | } -------------------------------------------------------------------------------- /test/samples/import-as/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | import { foo as bar } from 'x';` -------------------------------------------------------------------------------- /test/samples/import-default-and-named/expected.js: -------------------------------------------------------------------------------- 1 | import a, { b, c as d } from 'x'; -------------------------------------------------------------------------------- /test/samples/import-default-and-named/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": "" 11 | } -------------------------------------------------------------------------------- /test/samples/import-default-and-named/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b`import a, { b, c as d } from 'x';`; -------------------------------------------------------------------------------- /test/samples/import-many/expected.js: -------------------------------------------------------------------------------- 1 | import { 2 | abc, 3 | bcd, 4 | cde, 5 | def, 6 | efg, 7 | fgh, 8 | ghi, 9 | hij, 10 | ijk, 11 | jkl, 12 | klm, 13 | lmn, 14 | mno, 15 | nop, 16 | opq 17 | } from 'x'; -------------------------------------------------------------------------------- /test/samples/import-many/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;;;;;;;;;;;;;;;" 11 | } -------------------------------------------------------------------------------- /test/samples/import-many/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | import { abc, bcd, cde, def, efg, fgh, ghi, hij, ijk, jkl, klm, lmn, mno, nop, opq } from 'x';` -------------------------------------------------------------------------------- /test/samples/import/expected.js: -------------------------------------------------------------------------------- 1 | import { foo } from 'x'; 2 | import { bar } from 'y'; 3 | import('baz').then(blah); -------------------------------------------------------------------------------- /test/samples/import/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;" 11 | } -------------------------------------------------------------------------------- /test/samples/import/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => { 2 | const bar = b`import { bar } from 'y';`[0]; 3 | 4 | return b` 5 | import { foo as #foo } from 'x'; 6 | ${bar}; 7 | 8 | import('baz').then(blah)` 9 | }; -------------------------------------------------------------------------------- /test/samples/inserted-parameter/expected.js: -------------------------------------------------------------------------------- 1 | function foo(bar) { 2 | return bar * 2; 3 | } -------------------------------------------------------------------------------- /test/samples/inserted-parameter/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;" 11 | } -------------------------------------------------------------------------------- /test/samples/inserted-parameter/input.js: -------------------------------------------------------------------------------- 1 | export default ({ x }) => { 2 | const param = x`bar`; 3 | 4 | return x`function foo(${param}) { 5 | return ${param} * 2; 6 | }`; 7 | }; -------------------------------------------------------------------------------- /test/samples/inserted-parameters/expected.js: -------------------------------------------------------------------------------- 1 | function foo(bar, baz) { 2 | return bar * baz; 3 | } -------------------------------------------------------------------------------- /test/samples/inserted-parameters/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;" 11 | } -------------------------------------------------------------------------------- /test/samples/inserted-parameters/input.js: -------------------------------------------------------------------------------- 1 | export default ({ x }) => { 2 | const bar = x`bar`; 3 | const baz = x`baz`; 4 | 5 | const params = [bar, baz]; 6 | 7 | return x`function foo(${params}) { 8 | return ${bar} * ${baz}; 9 | }`; 10 | }; -------------------------------------------------------------------------------- /test/samples/logical-expression/expected.js: -------------------------------------------------------------------------------- 1 | a ?? (b || c); 2 | (a ?? b) || c; -------------------------------------------------------------------------------- /test/samples/logical-expression/expected.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"names":[],"sources":["input.js"],"sourcesContent":[null],"mappings":";"} -------------------------------------------------------------------------------- /test/samples/logical-expression/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => { 2 | return b` 3 | a ?? (b || c); 4 | (a ?? b) || c; 5 | `; 6 | } -------------------------------------------------------------------------------- /test/samples/meta-property/expected.js: -------------------------------------------------------------------------------- 1 | function foo() { 2 | console.log(new.target); 3 | } -------------------------------------------------------------------------------- /test/samples/meta-property/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;" 11 | } -------------------------------------------------------------------------------- /test/samples/meta-property/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | function foo() { 3 | console.log(new.target); 4 | } 5 | `; -------------------------------------------------------------------------------- /test/samples/method/expected.js: -------------------------------------------------------------------------------- 1 | obj = { 2 | foo() { 3 | console.log('foo'); 4 | }, 5 | async bar() { 6 | console.log('bar'); 7 | }, 8 | *baz() { 9 | console.log('baz'); 10 | } 11 | }; -------------------------------------------------------------------------------- /test/samples/method/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;;;;;;;;;" 11 | } -------------------------------------------------------------------------------- /test/samples/method/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b`obj = { 2 | foo() { 3 | console.log('foo'); 4 | }, 5 | async bar() { 6 | console.log('bar'); 7 | }, 8 | baz: function* () { 9 | console.log('baz'); 10 | } 11 | }`; -------------------------------------------------------------------------------- /test/samples/nested-blocks-b/expected.js: -------------------------------------------------------------------------------- 1 | let foo = bar; 2 | return { hello: 'world' }; -------------------------------------------------------------------------------- /test/samples/nested-blocks-b/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";" 11 | } -------------------------------------------------------------------------------- /test/samples/nested-blocks-b/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b, x }) => { 2 | const vars = [{ id: x`foo`, init: x`bar` }]; 3 | 4 | const return_value = x`{ 5 | hello: 'world' 6 | }`; 7 | 8 | const returned = b`return ${return_value};`; 9 | 10 | return b` 11 | ${vars.map(({ id, init }) => { 12 | return init 13 | ? b`let ${id} = ${init}` 14 | : b`let ${id}`; 15 | })} 16 | 17 | ${returned} 18 | `; 19 | }; -------------------------------------------------------------------------------- /test/samples/nested-blocks/expected.js: -------------------------------------------------------------------------------- 1 | console.log(one); 2 | console.log(two); 3 | console.log(three); 4 | console.log(four); -------------------------------------------------------------------------------- /test/samples/nested-blocks/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;;" 11 | } -------------------------------------------------------------------------------- /test/samples/nested-blocks/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => { 2 | const one = b`console.log(one);`; 3 | 4 | const two = b` 5 | ${one} 6 | console.log(two); 7 | `; 8 | 9 | const three = b` 10 | ${two} 11 | console.log(three); 12 | `; 13 | 14 | return b` 15 | ${three} 16 | console.log(four); 17 | `; 18 | }; -------------------------------------------------------------------------------- /test/samples/object-expressions/expected.js: -------------------------------------------------------------------------------- 1 | obj = { foo, bar, baz: qux }; 2 | obj = { "1": "1" }; 3 | obj = { true: true }; 4 | obj = { foo }; 5 | obj = { [foo]: foo }; 6 | obj = { [foo]: "foo" }; 7 | let blah; 8 | obj = { blah }; 9 | obj = { blah }; 10 | obj = { a: b }; 11 | 12 | obj = { 13 | method() { 14 | console.log('hello'); 15 | } 16 | }; 17 | 18 | empty = {}; 19 | opts = opts || {}; 20 | 21 | obj = { 22 | get foo() { 23 | return _foo; 24 | }, 25 | set foo(value) { 26 | _foo = value; 27 | }, 28 | get [foo]() { 29 | return _foo; 30 | }, 31 | set [foo](value) { 32 | _foo = value; 33 | } 34 | }; -------------------------------------------------------------------------------- /test/samples/object-expressions/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;" 11 | } -------------------------------------------------------------------------------- /test/samples/object-expressions/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b, p }) => { 2 | const x = p`a:b`; 3 | 4 | return b` 5 | obj = { 6 | foo: foo, 7 | bar, 8 | baz: qux 9 | }; 10 | 11 | obj = { "1": "1" }; 12 | 13 | obj = { true: true }; 14 | 15 | obj = { "foo": foo }; 16 | 17 | obj = { [foo]: foo }; 18 | 19 | obj = { [foo]: "foo" }; 20 | 21 | let #blah; 22 | obj = { blah: #blah }; 23 | obj = { 'blah': #blah }; 24 | 25 | obj = { ${x} } 26 | 27 | obj = { 28 | method: function() { 29 | console.log('hello'); 30 | } 31 | } 32 | 33 | empty = { } 34 | 35 | opts = opts || {} 36 | 37 | obj = { 38 | get foo() { 39 | return _foo; 40 | }, 41 | 42 | set foo(value) { 43 | _foo = value; 44 | }, 45 | 46 | get [foo]() { 47 | return _foo; 48 | }, 49 | 50 | set [foo](value) { 51 | _foo = value; 52 | } 53 | }`; // TODO would be nice if the {} didn't become ({}) 54 | }; 55 | -------------------------------------------------------------------------------- /test/samples/parenthesized-expression/expected.js: -------------------------------------------------------------------------------- 1 | a + b -------------------------------------------------------------------------------- /test/samples/parenthesized-expression/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": "" 11 | } -------------------------------------------------------------------------------- /test/samples/parenthesized-expression/input.js: -------------------------------------------------------------------------------- 1 | export default ({ x }) => ({ 2 | type: 'ParenthesizedExpression', 3 | expression: x`a + b` 4 | }); -------------------------------------------------------------------------------- /test/samples/regex/expected.js: -------------------------------------------------------------------------------- 1 | /(?:^\xb1\X\u765F)?/ -------------------------------------------------------------------------------- /test/samples/regex/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": "" 11 | } -------------------------------------------------------------------------------- /test/samples/regex/input.js: -------------------------------------------------------------------------------- 1 | export default ({ x }) => x`/(?:^\\xb1\\X\\u765F)?/`; -------------------------------------------------------------------------------- /test/samples/removes-parens/expected.js: -------------------------------------------------------------------------------- 1 | a = b + c -------------------------------------------------------------------------------- /test/samples/removes-parens/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": "" 11 | } -------------------------------------------------------------------------------- /test/samples/removes-parens/input.js: -------------------------------------------------------------------------------- 1 | export default ({ x }) => x` 2 | a = (b + c) 3 | `; -------------------------------------------------------------------------------- /test/samples/sourcemap/expected.js: -------------------------------------------------------------------------------- 1 | const a = 42; 2 | 3 | function foo(value) { 4 | console.log(value); 5 | } 6 | 7 | foo(a); -------------------------------------------------------------------------------- /test/samples/sourcemap/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;SAAS,GAAG,CAAC,KAAK;CAChB,OAAO,CAAC,GAAG,CAAC,KAAK;;;" 11 | } -------------------------------------------------------------------------------- /test/samples/sourcemap/input.js: -------------------------------------------------------------------------------- 1 | import * as acorn from 'acorn'; 2 | 3 | export default ({ b, x }) => { 4 | const decl = acorn.parse(`function foo(value) { 5 | console.log(value); 6 | }`, { 7 | locations: true, 8 | ecmaVersion: 2020 9 | }).body[0]; 10 | 11 | 12 | return b` 13 | const a = 42; 14 | 15 | ${decl} 16 | 17 | foo(a); 18 | `; 19 | } -------------------------------------------------------------------------------- /test/samples/string-literal/expected.js: -------------------------------------------------------------------------------- 1 | let a = 'foo'; 2 | let b = "foo"; 3 | let c = `foo`; 4 | let d = '\n\t\ta\n\t\tb\n\t\tc\n\t'; 5 | let e = "\n\t\ta\n\t\tb\n\t\tc\n\t"; 6 | 7 | let f = ` 8 | a 9 | b 10 | c 11 | `; -------------------------------------------------------------------------------- /test/samples/string-literal/expected.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"names":[],"sources":["input.js"],"sourcesContent":[null],"mappings":";;;;;;;;;;"} -------------------------------------------------------------------------------- /test/samples/string-literal/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => { 2 | const value = 'foo'; 3 | const multiline = ` 4 | a 5 | b 6 | c 7 | `; 8 | 9 | return b` 10 | let a = '${value}'; 11 | let b = "${value}"; 12 | let c = \`${value}\`; 13 | let d = '${multiline}'; 14 | let e = "${multiline}"; 15 | let f = \`${multiline}\`; 16 | `; 17 | }; 18 | -------------------------------------------------------------------------------- /test/samples/switch/expected.js: -------------------------------------------------------------------------------- 1 | switch (foo) { 2 | case 1: 3 | blah(); 4 | break; 5 | case 2: 6 | blah(); 7 | default: 8 | blah(); 9 | } -------------------------------------------------------------------------------- /test/samples/switch/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;;;;;;;" 11 | } -------------------------------------------------------------------------------- /test/samples/switch/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | switch (foo) { 3 | case 1: 4 | blah(); 5 | break; 6 | 7 | case 2: 8 | blah(); 9 | 10 | default: 11 | blah(); 12 | } 13 | `; -------------------------------------------------------------------------------- /test/samples/tagged-template/expected.js: -------------------------------------------------------------------------------- 1 | foo`bar`; -------------------------------------------------------------------------------- /test/samples/tagged-template/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": "" 11 | } -------------------------------------------------------------------------------- /test/samples/tagged-template/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | foo\`bar\`; 3 | `; -------------------------------------------------------------------------------- /test/samples/try-catch/expected.js: -------------------------------------------------------------------------------- 1 | try { 2 | foo(); 3 | } catch { 4 | bar(); 5 | } 6 | 7 | try { 8 | foo(); 9 | } catch(e) { 10 | bar(e); 11 | } finally { 12 | baz(); 13 | } -------------------------------------------------------------------------------- /test/samples/try-catch/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;;;;;;;;;;;" 11 | } -------------------------------------------------------------------------------- /test/samples/try-catch/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | try { 3 | foo(); 4 | } catch { 5 | bar(); 6 | } 7 | 8 | try { 9 | foo(); 10 | } catch(e) { 11 | bar(e); 12 | } finally { 13 | baz(); 14 | } 15 | `; -------------------------------------------------------------------------------- /test/samples/var-declaration/expected.js: -------------------------------------------------------------------------------- 1 | const obj = { 2 | a, 3 | b, 4 | c, 5 | d, 6 | e, 7 | f, 8 | g, 9 | h, 10 | i, 11 | j, 12 | k, 13 | l, 14 | m, 15 | n, 16 | o, 17 | p, 18 | q, 19 | r, 20 | s, 21 | t, 22 | u, 23 | v, 24 | w, 25 | x, 26 | y, 27 | z 28 | }; 29 | 30 | const obj2 = { 31 | a, 32 | b, 33 | c, 34 | d, 35 | e, 36 | f, 37 | g, 38 | h, 39 | i, 40 | j, 41 | k, 42 | l, 43 | m, 44 | n, 45 | o, 46 | p, 47 | q, 48 | r, 49 | s, 50 | t, 51 | u, 52 | v, 53 | w, 54 | x, 55 | y, 56 | z 57 | }, 58 | a = 1, 59 | b = 2; -------------------------------------------------------------------------------- /test/samples/var-declaration/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;" 11 | } -------------------------------------------------------------------------------- /test/samples/var-declaration/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | const obj = { a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z }; 3 | 4 | const obj2 = { a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z }, a = 1, b = 2; 5 | `; -------------------------------------------------------------------------------- /test/samples/with/expected.js: -------------------------------------------------------------------------------- 1 | with (foo) bar(); -------------------------------------------------------------------------------- /test/samples/with/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": "" 11 | } -------------------------------------------------------------------------------- /test/samples/with/input.js: -------------------------------------------------------------------------------- 1 | export default () => ({ 2 | type: 'WithStatement', 3 | start: 0, 4 | end: 17, 5 | object: { 6 | type: 'Identifier', 7 | start: 6, 8 | end: 9, 9 | name: 'foo' 10 | }, 11 | body: { 12 | type: 'ExpressionStatement', 13 | start: 11, 14 | end: 17, 15 | expression: { 16 | type: 'CallExpression', 17 | start: 11, 18 | end: 16, 19 | callee: { 20 | type: 'Identifier', 21 | start: 11, 22 | end: 14, 23 | name: 'bar' 24 | }, 25 | arguments: [] 26 | } 27 | } 28 | }) -------------------------------------------------------------------------------- /test/samples/yield/expected.js: -------------------------------------------------------------------------------- 1 | function* foo() { 2 | yield; 3 | } 4 | 5 | function* bar() { 6 | yield* 1; 7 | } -------------------------------------------------------------------------------- /test/samples/yield/expected.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "names": [], 4 | "sources": [ 5 | "input.js" 6 | ], 7 | "sourcesContent": [ 8 | null 9 | ], 10 | "mappings": ";;;;;;" 11 | } -------------------------------------------------------------------------------- /test/samples/yield/input.js: -------------------------------------------------------------------------------- 1 | export default ({ b }) => b` 2 | function* foo() { 3 | yield; 4 | } 5 | 6 | function* bar() { 7 | yield* 1; 8 | } 9 | `; -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { decode } from '@jridgewell/sourcemap-codec'; 3 | import * as fs from 'fs'; 4 | import * as acorn from 'acorn'; 5 | import * as uvu from 'uvu'; 6 | import * as assert from 'assert'; 7 | import { generateRandomJS } from 'eslump'; 8 | import * as codered from '../src/index.js'; 9 | import { walk } from 'estree-walker'; 10 | import { fileURLToPath } from 'url'; 11 | 12 | /** @typedef {import('estree').Identifier} Identifier */ 13 | /** @typedef {import('estree').Node} Node */ 14 | /** @typedef {import('estree').ObjectExpression} ObjectExpression */ 15 | 16 | /** @param {string} str */ 17 | const d = (str) => str.replace(/^\t{5}/gm, '').trim(); 18 | 19 | // just to make the tests less messy 20 | const remove_ranges = (ast) => { 21 | walk(ast, { 22 | enter(node) { 23 | delete node.start; 24 | delete node.end; 25 | } 26 | }); 27 | return ast; 28 | }; 29 | 30 | const b = (s, ...v) => remove_ranges(codered.b(s, ...v)); 31 | const x = (s, ...v) => remove_ranges(codered.x(s, ...v)); 32 | const p = (s, ...v) => remove_ranges(codered.p(s, ...v)); 33 | const print = codered.print; 34 | const parse = (s) => 35 | codered.parse(s, { 36 | ecmaVersion: 2022, 37 | sourceType: 'module', 38 | allowAwaitOutsideFunction: true, 39 | allowImportExportEverywhere: true, 40 | allowReturnOutsideFunction: true 41 | }); 42 | 43 | /** 44 | * @param {string} name 45 | * @param {(test: uvu.Test) => void} fn 46 | */ 47 | function suite(name, fn) { 48 | const suite = uvu.suite(name); 49 | fn(suite); 50 | suite.run(); 51 | } 52 | 53 | suite('b', (test) => { 54 | test('creates a block of nodes', () => { 55 | assert.deepEqual( 56 | b` 57 | a = b + c; 58 | d = e + f; 59 | `, 60 | [ 61 | { 62 | type: 'ExpressionStatement', 63 | expression: { 64 | type: 'AssignmentExpression', 65 | left: { type: 'Identifier', name: 'a' }, 66 | operator: '=', 67 | right: { 68 | type: 'BinaryExpression', 69 | left: { type: 'Identifier', name: 'b' }, 70 | operator: '+', 71 | right: { type: 'Identifier', name: 'c' } 72 | } 73 | } 74 | }, 75 | { 76 | type: 'ExpressionStatement', 77 | expression: { 78 | type: 'AssignmentExpression', 79 | left: { type: 'Identifier', name: 'd' }, 80 | operator: '=', 81 | right: { 82 | type: 'BinaryExpression', 83 | left: { type: 'Identifier', name: 'e' }, 84 | operator: '+', 85 | right: { type: 'Identifier', name: 'f' } 86 | } 87 | } 88 | } 89 | ] 90 | ); 91 | }); 92 | 93 | test('ignores falsy values', () => { 94 | assert.deepEqual( 95 | b` 96 | a++; 97 | ${false} 98 | b++ 99 | `, 100 | [ 101 | { 102 | type: 'ExpressionStatement', 103 | expression: { 104 | type: 'UpdateExpression', 105 | operator: '++', 106 | prefix: false, 107 | argument: { type: 'Identifier', name: 'a' } 108 | } 109 | }, 110 | { 111 | type: 'ExpressionStatement', 112 | expression: { 113 | type: 'UpdateExpression', 114 | operator: '++', 115 | prefix: false, 116 | argument: { type: 'Identifier', name: 'b' } 117 | } 118 | } 119 | ] 120 | ); 121 | }); 122 | 123 | test('unwraps arrays', () => { 124 | const vars = [x`a`, x`b`, x`c`]; 125 | const declarations = vars.map((v) => b`console.log(${v})`); 126 | 127 | const fn = x`function foo() { 128 | ${declarations} 129 | }`; 130 | 131 | const call = (name) => ({ 132 | type: 'ExpressionStatement', 133 | expression: { 134 | type: 'CallExpression', 135 | callee: { 136 | type: 'MemberExpression', 137 | object: { type: 'Identifier', name: 'console' }, 138 | property: { type: 'Identifier', name: 'log' }, 139 | optional: false, 140 | computed: false 141 | }, 142 | arguments: [{ type: 'Identifier', name }], 143 | optional: false 144 | } 145 | }); 146 | 147 | assert.deepEqual(fn.body.body, [ 148 | { leadingComments: undefined, ...call('a') }, 149 | call('b'), 150 | call('c') 151 | ]); 152 | }); 153 | }); 154 | 155 | suite('x', (test) => { 156 | test('creates a single expression', () => { 157 | assert.deepEqual( 158 | x` 159 | a = b + c 160 | `, 161 | { 162 | type: 'AssignmentExpression', 163 | left: { type: 'Identifier', name: 'a' }, 164 | operator: '=', 165 | right: { 166 | type: 'BinaryExpression', 167 | left: { type: 'Identifier', name: 'b' }, 168 | operator: '+', 169 | right: { type: 'Identifier', name: 'c' } 170 | } 171 | } 172 | ); 173 | }); 174 | 175 | test('inserts values', () => { 176 | const name = { x: 'name' }; 177 | const param = { x: 'param' }; 178 | 179 | const node = x` 180 | function ${name}(${param}) { 181 | return ${param} * 2; 182 | } 183 | `; 184 | 185 | assert.deepEqual(node, { 186 | type: 'FunctionExpression', 187 | id: name, 188 | expression: false, 189 | generator: false, 190 | async: false, 191 | params: [param], 192 | body: { 193 | type: 'BlockStatement', 194 | body: [ 195 | { 196 | type: 'ReturnStatement', 197 | argument: { 198 | type: 'BinaryExpression', 199 | left: param, 200 | operator: '*', 201 | right: { type: 'Literal', value: 2, raw: '2' } 202 | } 203 | } 204 | ] 205 | } 206 | }); 207 | }); 208 | 209 | test('preserves @-prefixed names', () => { 210 | const node = x`@foo(bar)`; 211 | 212 | assert.deepEqual(node, { 213 | type: 'CallExpression', 214 | callee: { type: 'Identifier', name: '@foo' }, 215 | arguments: [{ type: 'Identifier', name: 'bar' }], 216 | optional: false 217 | }); 218 | 219 | const id = x`@foo`; 220 | 221 | assert.deepEqual(id, { 222 | type: 'Identifier', 223 | name: '@foo' 224 | }); 225 | }); 226 | 227 | test('preserves #-prefixed names', () => { 228 | const node = x` 229 | function foo(#bar) { 230 | return #bar * bar; 231 | } 232 | `; 233 | 234 | assert.deepEqual(node, { 235 | type: 'FunctionExpression', 236 | id: { type: 'Identifier', name: 'foo' }, 237 | expression: false, 238 | generator: false, 239 | async: false, 240 | params: [{ type: 'Identifier', name: '#bar' }], 241 | body: { 242 | type: 'BlockStatement', 243 | body: [ 244 | { 245 | type: 'ReturnStatement', 246 | argument: { 247 | type: 'BinaryExpression', 248 | left: { type: 'Identifier', name: '#bar' }, 249 | operator: '*', 250 | right: { type: 'Identifier', name: 'bar' } 251 | } 252 | } 253 | ] 254 | } 255 | }); 256 | }); 257 | 258 | test('flattens parameters', () => { 259 | const args = [x`a`, x`b`]; 260 | 261 | const fn = x`function (${args}) { 262 | return a + b; 263 | }`; 264 | 265 | assert.deepEqual(fn, { 266 | type: 'FunctionExpression', 267 | id: null, 268 | expression: false, 269 | generator: false, 270 | async: false, 271 | params: [ 272 | { type: 'Identifier', name: 'a' }, 273 | { type: 'Identifier', name: 'b' } 274 | ], 275 | body: { 276 | type: 'BlockStatement', 277 | body: [ 278 | { 279 | type: 'ReturnStatement', 280 | argument: { 281 | type: 'BinaryExpression', 282 | left: { type: 'Identifier', name: 'a' }, 283 | operator: '+', 284 | right: { type: 'Identifier', name: 'b' } 285 | } 286 | } 287 | ] 288 | } 289 | }); 290 | }); 291 | 292 | test(`replaces strings`, () => { 293 | const name = 'world'; 294 | const expression = x`hello("${name}")`; 295 | 296 | assert.deepEqual(expression.arguments[0].value, 'world'); 297 | }); 298 | 299 | test(`replaces numbers`, () => { 300 | const answer = 42; 301 | const expression = x`console.log("the answer is", ${answer})`; 302 | 303 | assert.deepEqual(expression.arguments[1], { 304 | type: 'Literal', 305 | value: 42, 306 | leadingComments: undefined, 307 | trailingComments: undefined 308 | }); 309 | }); 310 | 311 | test(`replaces identifier with no parent`, () => { 312 | const answer = { type: 'Identifier', name: 'value' }; 313 | const expression = x`${answer}`; 314 | 315 | assert.deepEqual(expression, { 316 | type: 'Identifier', 317 | name: 'value' 318 | }); 319 | }); 320 | 321 | test(`replaces strings in template literals`, () => { 322 | const foo = 'bar'; 323 | const expression = x`\`${foo}\``; 324 | 325 | assert.deepEqual(expression.quasis[0].value.raw, 'bar'); 326 | }); 327 | 328 | test(`allows strings in place of identifiers`, () => { 329 | const name = 'world'; 330 | const expression = x`hello(${name})`; 331 | 332 | assert.deepEqual(expression.arguments[0], { 333 | type: 'Identifier', 334 | name: 'world', 335 | leadingComments: undefined, 336 | trailingComments: undefined 337 | }); 338 | }); 339 | 340 | test('flattens arrays', () => { 341 | const vars = [x`a`, x`b`, x`c`]; 342 | const arr = x`[${vars}]`; 343 | 344 | assert.deepEqual(arr, { 345 | type: 'ArrayExpression', 346 | elements: ['a', 'b', 'c'].map((name) => ({ 347 | type: 'Identifier', 348 | name 349 | })) 350 | }); 351 | }); 352 | 353 | test('flattens objects', () => { 354 | const props = [p`a`, p`b`, p`c`]; 355 | const obj = x`{${props}}`; 356 | 357 | assert.deepEqual(obj, { 358 | type: 'ObjectExpression', 359 | properties: ['a', 'b', 'c'].map((name) => { 360 | const id = { type: 'Identifier', name }; 361 | return { 362 | type: 'Property', 363 | kind: 'init', 364 | method: false, 365 | shorthand: true, 366 | computed: false, 367 | key: id, 368 | value: id 369 | }; 370 | }) 371 | }); 372 | }); 373 | 374 | test('flattens patterns', () => { 375 | const props = [p`a`, p`b`, p`c`]; 376 | const declaration = b`const { ${props} } = obj;`[0]; 377 | 378 | assert.deepEqual(declaration, { 379 | type: 'VariableDeclaration', 380 | kind: 'const', 381 | declarations: [ 382 | { 383 | type: 'VariableDeclarator', 384 | id: { 385 | type: 'ObjectPattern', 386 | properties: ['a', 'b', 'c'].map((name) => { 387 | const id = { type: 'Identifier', name }; 388 | return { 389 | type: 'Property', 390 | kind: 'init', 391 | method: false, 392 | computed: false, 393 | shorthand: true, 394 | key: id, 395 | value: id 396 | }; 397 | }) 398 | }, 399 | init: { 400 | type: 'Identifier', 401 | name: 'obj' 402 | } 403 | } 404 | ] 405 | }); 406 | }); 407 | 408 | test('removes falsy properties from an object', () => { 409 | const obj = x`{ 410 | a: 1, 411 | b: ${false} 412 | }`; 413 | 414 | assert.deepEqual(obj.properties.length, 1); 415 | assert.deepEqual(obj.properties[0].key.name, 'a'); 416 | }); 417 | 418 | test('preserves locations of original nodes in a sourcemap', () => { 419 | const answer = { 420 | type: 'Literal', 421 | value: 42, 422 | raw: '42', 423 | loc: { 424 | start: { line: 10, column: 5 }, 425 | end: { line: 10, column: 7 } 426 | } 427 | }; 428 | 429 | const expression = x`console.log(${answer})`; 430 | 431 | const { code, map } = print(expression, { 432 | sourceMapSource: 'input.js' 433 | }); 434 | 435 | assert.deepEqual(code, `console.log(42)`); 436 | 437 | assert.deepEqual(map, { 438 | version: 3, 439 | sources: ['input.js'], 440 | sourcesContent: [null], 441 | names: [], 442 | mappings: 'YASK,EAAE' 443 | }); 444 | }); 445 | 446 | test('errors on invalid expressions', () => { 447 | assert.throws(() => { 448 | x`this is broken`; 449 | }, /Unexpected token 'is'/); 450 | }); 451 | }); 452 | 453 | suite('p', (test) => { 454 | test('creates a regular object property', () => { 455 | const obj = x`{}`; 456 | obj.properties.push(p`foo: 'bar'`); 457 | 458 | assert.deepEqual(obj, { 459 | type: 'ObjectExpression', 460 | properties: [ 461 | { 462 | type: 'Property', 463 | kind: 'init', 464 | method: false, 465 | shorthand: false, 466 | computed: false, 467 | key: { type: 'Identifier', name: 'foo' }, 468 | value: { type: 'Literal', value: 'bar', raw: "'bar'" } 469 | } 470 | ] 471 | }); 472 | }); 473 | }); 474 | 475 | suite('print', (test) => { 476 | const read = (file) => 477 | fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : null; 478 | 479 | fs.readdirSync('test/samples').forEach((dir) => { 480 | test(dir, async () => { 481 | if (dir[0] === '.') return; 482 | 483 | const url = new URL(`./samples/${dir}/input.js`, import.meta.url); 484 | const mod = await import(fileURLToPath(url.href)); 485 | const input = mod.default({ b, x, p, parse }); 486 | 487 | const expected = { 488 | code: read(`test/samples/${dir}/expected.js`), 489 | map: JSON.parse(read(`test/samples/${dir}/expected.js.map`) || '{}') 490 | }; 491 | 492 | const actual = print(input, { 493 | sourceMapSource: 'input.js', 494 | getName: (name) => name.toUpperCase() 495 | }); 496 | 497 | fs.writeFileSync(`test/samples/${dir}/_actual.js`, actual.code); 498 | fs.writeFileSync( 499 | `test/samples/${dir}/_actual.js.map`, 500 | actual.map.toString() 501 | ); 502 | 503 | assert.deepEqual( 504 | actual.code.replace(/\t+$/gm, ''), 505 | expected.code.replace(/\t+$/gm, '') 506 | ); 507 | assert.deepEqual(actual.map, expected.map); 508 | }); 509 | }); 510 | 511 | test('throws on unhandled sigils', () => { 512 | assert.throws(() => print(b`let foo = @bar;`), { 513 | message: 'Unhandled sigil @bar' 514 | }); 515 | }); 516 | 517 | test('can return sourcemap with decoded mappings', async () => { 518 | const url = new URL(`./samples/sourcemap/input.js`, import.meta.url); 519 | const mod = await import(fileURLToPath(url.href)); 520 | const input = mod.default({ b, x, p }); 521 | 522 | const expected = { 523 | code: read(`test/samples/sourcemap/expected.js`), 524 | map: JSON.parse(read(`test/samples/sourcemap/expected.js.map`) || '{}') 525 | }; 526 | if (expected.map && expected.map.mappings) { 527 | expected.map.mappings = decode(expected.map.mappings); 528 | } 529 | 530 | const actual = print(input, { 531 | sourceMapSource: 'input.js', 532 | getName: (name) => name.toUpperCase(), 533 | sourceMapEncodeMappings: false 534 | }); 535 | 536 | assert.deepEqual(actual.map, expected.map); 537 | }); 538 | 539 | test.skip('passes fuzz testing', () => { 540 | for (let i = 0; i < 100; i += 1) { 541 | const js = generateRandomJS({ 542 | sourceType: 'module', 543 | maxDepth: 7, 544 | comments: false 545 | }); 546 | 547 | let ast1; 548 | try { 549 | ast1 = acorn.parse(js, { 550 | sourceType: 'module', 551 | ecmaVersion: 2020 552 | }); 553 | } catch { 554 | continue; 555 | } 556 | 557 | let printed; 558 | 559 | try { 560 | printed = print(ast1); 561 | } catch (err) { 562 | fs.writeFileSync(`test/fuzz/report.js`, js); 563 | throw err; 564 | } 565 | 566 | const ast2 = acorn.parse(printed.code, { 567 | sourceType: 'module', 568 | ecmaVersion: 2019 569 | }); 570 | 571 | [ast1, ast2].forEach((ast) => { 572 | walk(ast, { 573 | enter(node) { 574 | delete node.start; 575 | delete node.end; 576 | } 577 | }); 578 | }); 579 | 580 | try { 581 | assert.deepEqual(ast1, ast2); 582 | } catch (err) { 583 | fs.writeFileSync( 584 | `test/fuzz/report.js`, 585 | `// input\n${js}\n\n// output\n${printed.code}` 586 | ); 587 | throw err; 588 | } 589 | } 590 | }); 591 | }); 592 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "declaration": true, 6 | "emitDeclarationOnly": true, 7 | "declarationDir": "types", 8 | "noImplicitAny": true, 9 | "diagnostics": true, 10 | "noImplicitThis": true, 11 | "noEmitOnError": true, 12 | "lib": ["es5", "es6", "dom"] 13 | }, 14 | "target": "ES5", 15 | "module": "ES6", 16 | "include": [ 17 | "src" 18 | ], 19 | "exclude": [ 20 | "node_modules" 21 | ] 22 | } --------------------------------------------------------------------------------