├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .prettierrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── package.json ├── pnpm-lock.yaml ├── src ├── index.d.ts ├── types.d.ts └── walk.js ├── test ├── transformation.js ├── traversal.js └── types.d.ts ├── tsconfig.json └── vitest.config.js /.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 (gitub.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 | include: 22 | - node-version: 20 23 | os: [ubuntu-latest] 24 | - node-version: 20 25 | os: windows-latest 26 | steps: 27 | - run: git config --global core.autocrlf false 28 | - uses: actions/checkout@v4 29 | - uses: pnpm/action-setup@v4 30 | - uses: actions/setup-node@v4 31 | with: 32 | node-version: ${{ matrix.node-version }} 33 | cache: pnpm 34 | - run: pnpm install --frozen-lockfile 35 | - run: pnpm test 36 | Check: 37 | runs-on: ${{ matrix.os }} 38 | timeout-minutes: 30 39 | strategy: 40 | fail-fast: false 41 | matrix: 42 | include: 43 | - node-version: 20 44 | os: [ubuntu-latest] 45 | - node-version: 20 46 | os: windows-latest 47 | steps: 48 | - run: git config --global core.autocrlf false 49 | - uses: actions/checkout@v4 50 | - uses: pnpm/action-setup@v4 51 | - uses: actions/setup-node@v4 52 | with: 53 | node-version: ${{ matrix.node-version }} 54 | cache: pnpm 55 | - run: pnpm install --frozen-lockfile 56 | - run: pnpm check 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /node_modules 3 | /types -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": true, 3 | "singleQuote": true, 4 | "trailingComma": "none", 5 | "overrides": [ 6 | { 7 | "files": [ 8 | "*.md" 9 | ], 10 | "options": { 11 | "useTabs": false, 12 | "tabWidth": 2 13 | } 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # zimmerframe changelog 2 | 3 | ## 1.1.2 4 | 5 | - Keep non-enumerable properties non-enumerable ([#20](https://github.com/Rich-Harris/zimmerframe/pull/20)) 6 | 7 | ## 1.1.1 8 | 9 | - Prevent false positive mutations ([#18](https://github.com/Rich-Harris/zimmerframe/pull/18)) 10 | - Keep non-enumerable properties when cloning nodes ([#19](https://github.com/Rich-Harris/zimmerframe/pull/19)) 11 | 12 | ## 1.1.0 13 | 14 | - Return transformed node from `context.next()` ([#17](https://github.com/Rich-Harris/zimmerframe/pull/17)) 15 | 16 | ## 1.0.0 17 | 18 | - Stable release 19 | 20 | ## 0.2.1 21 | 22 | - Push current node to path when calling `visit` 23 | 24 | ## 0.2.0 25 | 26 | - Require `next()` to be called to visit children 27 | 28 | ## 0.1.2 29 | 30 | - Forward state from universal visitors 31 | 32 | ## 0.1.1 33 | 34 | - Skip children after calling `visit` 35 | 36 | ## 0.1.0 37 | 38 | - Rename `context.transform` to `context.visit` 39 | 40 | ## 0.0.11 41 | 42 | - Respect individual visitor transformations if universal visitors calls `next(...)` 43 | 44 | ## 0.0.10 45 | 46 | - Simplify `Context` type arguments 47 | 48 | ## 0.0.9 49 | 50 | - Skip children when transforming 51 | 52 | ## 0.0.8 53 | 54 | - Fix `path` type 55 | 56 | ## 0.0.7 57 | 58 | - Fix package.json 59 | 60 | ## 0.0.6 61 | 62 | - Export types 63 | 64 | ## 0.0.5 65 | 66 | - Fix some type issues 67 | 68 | ## 0.0.4 69 | 70 | - Make visitor signature more forgiving 71 | 72 | ## 0.0.3 73 | 74 | - Allow state to be `null` 75 | 76 | ## 0.0.2 77 | 78 | - Add `pkg.files` 79 | 80 | ## 0.0.1 81 | 82 | - First release 83 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 [these people](https://github.com/Rich-Harris/zimmerframe/graphs/contributors) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # zimmerframe 2 | 3 | A tool for walking. 4 | 5 | Specifically, it's a tool for walking an abstract syntax tree (AST), where every node is an object with a `type: string`. This includes [ESTree](https://github.com/estree/estree) nodes, such as you might generate with [Acorn](https://github.com/acornjs/acorn) or [Meriyah](https://github.com/meriyah/meriyah), but also includes things like [CSSTree](https://github.com/csstree/csstree) or an arbitrary AST of your own devising. 6 | 7 | ## Usage 8 | 9 | ```ts 10 | import { walk } from 'zimmerframe'; 11 | import { parse } from 'acorn'; 12 | import { Node } from 'estree'; 13 | 14 | const program = parse(` 15 | let message = 'hello'; 16 | console.log(message); 17 | 18 | if (true) { 19 | let answer = 42; 20 | console.log(answer); 21 | } 22 | `); 23 | 24 | // You can pass in arbitrary state 25 | const state = { 26 | declarations: [], 27 | depth: 0 28 | }; 29 | 30 | const transformed = walk(program as Node, state, { 31 | _(node, { state, next }) { 32 | // the `_` visitor is 'universal' — if provided, 33 | // it will run for every node, before deferring 34 | // to specialised visitors. you can pass a new 35 | // `state` object to `next` 36 | next({ ...state, answer: 42 }); 37 | }, 38 | VariableDeclarator(node, { state }) { 39 | // `state` is passed into each visitor 40 | if (node.id.type === 'Identifier') { 41 | state.declarations.push({ 42 | depth: state.depth, 43 | name: node.id.name 44 | }); 45 | } 46 | }, 47 | BlockStatement(node, { state, next, stop }) { 48 | // you must call `next()` or `next(childState)` 49 | // to visit child nodes 50 | console.log('entering BlockStatement'); 51 | next({ ...state, depth: state.depth + 1 }); 52 | console.log('leaving BlockStatement'); 53 | }, 54 | Literal(node) { 55 | // if you return something, it will replace 56 | // the current node 57 | if (node.value === 'hello') { 58 | return { 59 | ...node, 60 | value: 'goodbye' 61 | }; 62 | } 63 | }, 64 | IfStatement(node, { visit }) { 65 | // normally, returning a value will halt 66 | // traversal into child nodes. you can 67 | // transform children with the current 68 | // visitors using `visit(node, state?)` 69 | if (node.test.type === 'Literal' && node.test.value === true) { 70 | return visit(node.consequent); 71 | } 72 | } 73 | }); 74 | ``` 75 | 76 | The `transformed` AST would look like this: 77 | 78 | ```js 79 | let message = 'goodbye'; 80 | console.log(message); 81 | 82 | { 83 | let answer = 42; 84 | console.log(answer); 85 | } 86 | ``` 87 | 88 | ## Types 89 | 90 | The type of `node` in each visitor is inferred from the visitor's name. For example: 91 | 92 | ```ts 93 | walk(ast as estree.Node, state, { 94 | ArrowFunctionExpression(node) { 95 | // `node` is of type estree.ArrowFunctionExpression 96 | } 97 | }); 98 | ``` 99 | 100 | For this to work, the first argument should be casted to an union of all the types you plan to visit. 101 | 102 | You can import types from 'zimmerframe': 103 | 104 | ```ts 105 | import { 106 | walk, 107 | type Visitor, 108 | type Visitors, 109 | type Context 110 | } from 'zimmerframe'; 111 | import type { Node } from 'estree'; 112 | 113 | interface State {...} 114 | 115 | const node: Node = {...}; 116 | const state: State = {...}; 117 | const visitors: Visitors = {...} 118 | 119 | walk(node, state, visitors); 120 | ``` 121 | 122 | ## Context 123 | 124 | Each visitor receives a second argument, `context`, which is an object with the following properties and methods: 125 | 126 | - `next(state?: State): void` — a function that allows you to control when child nodes are visited, and which state they are visited with. If child visitors transform their inputs, this will return the transformed node (if not, returns `undefined`) 127 | - `path: Node[]` — an array of parent nodes. For example, to get the root node you would do `path.at(0)`; to get the current node's immediate parent you would do `path.at(-1)` 128 | - `state: State` — an object of the same type as the second argument to `walk`. Visitors can pass new state objects to their children with `next(childState)` or `visit(node, childState)` 129 | - `stop(): void` — prevents any subsequent traversal 130 | - `visit(node: Node, state?: State): Node` — returns the result of visiting `node` with the current set of visitors. If no `state` is provided, children will inherit the current state 131 | 132 | ## Immutability 133 | 134 | ASTs are regarded as immutable. If you return a transformed node from a visitor, then all parents of the node will be replaced with clones, but unchanged subtrees will reuse the existing nodes. 135 | 136 | For example in this case, no transformation takes place, meaning that the returned value is identical to the original AST: 137 | 138 | ```js 139 | const transformed = walk(original, state, { 140 | Literal(node) { 141 | console.log(node.value); 142 | } 143 | }); 144 | 145 | transformed === original; // true 146 | ``` 147 | 148 | In this case, however, we replace one of the nodes: 149 | 150 | ```js 151 | const original = { 152 | type: 'BinaryExpression', 153 | operator: '+', 154 | left: { 155 | type: 'Identifier', 156 | name: 'foo' 157 | }, 158 | right: { 159 | type: 'Identifier', 160 | name: 'bar' 161 | } 162 | }; 163 | 164 | const transformed = walk(original, state, { 165 | Identifier(node) { 166 | if (node.name === 'bar') { 167 | return { ...node, name: 'baz' }; 168 | } 169 | } 170 | }); 171 | 172 | transformed === original; // false, the BinaryExpression node is cloned 173 | transformed.left === original.left; // true, we can safely reuse this node 174 | ``` 175 | 176 | This makes it very easy to transform parts of your AST without incurring the performance and memory overhead of cloning the entire thing, and without the footgun of mutating it in place. 177 | 178 | ## License 179 | 180 | MIT 181 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zimmerframe", 3 | "description": "A tool for walking ASTs", 4 | "version": "1.1.2", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/Rich-Harris/zimmerframe" 8 | }, 9 | "type": "module", 10 | "exports": { 11 | ".": { 12 | "types": "./types/index.d.ts", 13 | "import": "./src/walk.js" 14 | } 15 | }, 16 | "types": "./types/index.d.ts", 17 | "files": [ 18 | "src", 19 | "types" 20 | ], 21 | "devDependencies": { 22 | "dts-buddy": "^0.1.13", 23 | "typescript": "^5.1.6", 24 | "vitest": "^0.34.2" 25 | }, 26 | "scripts": { 27 | "prepublishOnly": "dts-buddy -m zimmerframe:src/index.d.ts", 28 | "check": "tsc", 29 | "test": "vitest --run", 30 | "test:watch": "vitest" 31 | }, 32 | "license": "MIT", 33 | "packageManager": "pnpm@8.6.12" 34 | } 35 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | devDependencies: 8 | dts-buddy: 9 | specifier: ^0.1.13 10 | version: 0.1.13 11 | typescript: 12 | specifier: ^5.1.6 13 | version: 5.1.6 14 | vitest: 15 | specifier: ^0.34.2 16 | version: 0.34.2 17 | 18 | packages: 19 | 20 | /@esbuild/android-arm64@0.18.20: 21 | resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==} 22 | engines: {node: '>=12'} 23 | cpu: [arm64] 24 | os: [android] 25 | requiresBuild: true 26 | dev: true 27 | optional: true 28 | 29 | /@esbuild/android-arm@0.18.20: 30 | resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==} 31 | engines: {node: '>=12'} 32 | cpu: [arm] 33 | os: [android] 34 | requiresBuild: true 35 | dev: true 36 | optional: true 37 | 38 | /@esbuild/android-x64@0.18.20: 39 | resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==} 40 | engines: {node: '>=12'} 41 | cpu: [x64] 42 | os: [android] 43 | requiresBuild: true 44 | dev: true 45 | optional: true 46 | 47 | /@esbuild/darwin-arm64@0.18.20: 48 | resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==} 49 | engines: {node: '>=12'} 50 | cpu: [arm64] 51 | os: [darwin] 52 | requiresBuild: true 53 | dev: true 54 | optional: true 55 | 56 | /@esbuild/darwin-x64@0.18.20: 57 | resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==} 58 | engines: {node: '>=12'} 59 | cpu: [x64] 60 | os: [darwin] 61 | requiresBuild: true 62 | dev: true 63 | optional: true 64 | 65 | /@esbuild/freebsd-arm64@0.18.20: 66 | resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==} 67 | engines: {node: '>=12'} 68 | cpu: [arm64] 69 | os: [freebsd] 70 | requiresBuild: true 71 | dev: true 72 | optional: true 73 | 74 | /@esbuild/freebsd-x64@0.18.20: 75 | resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==} 76 | engines: {node: '>=12'} 77 | cpu: [x64] 78 | os: [freebsd] 79 | requiresBuild: true 80 | dev: true 81 | optional: true 82 | 83 | /@esbuild/linux-arm64@0.18.20: 84 | resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==} 85 | engines: {node: '>=12'} 86 | cpu: [arm64] 87 | os: [linux] 88 | requiresBuild: true 89 | dev: true 90 | optional: true 91 | 92 | /@esbuild/linux-arm@0.18.20: 93 | resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==} 94 | engines: {node: '>=12'} 95 | cpu: [arm] 96 | os: [linux] 97 | requiresBuild: true 98 | dev: true 99 | optional: true 100 | 101 | /@esbuild/linux-ia32@0.18.20: 102 | resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==} 103 | engines: {node: '>=12'} 104 | cpu: [ia32] 105 | os: [linux] 106 | requiresBuild: true 107 | dev: true 108 | optional: true 109 | 110 | /@esbuild/linux-loong64@0.18.20: 111 | resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==} 112 | engines: {node: '>=12'} 113 | cpu: [loong64] 114 | os: [linux] 115 | requiresBuild: true 116 | dev: true 117 | optional: true 118 | 119 | /@esbuild/linux-mips64el@0.18.20: 120 | resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==} 121 | engines: {node: '>=12'} 122 | cpu: [mips64el] 123 | os: [linux] 124 | requiresBuild: true 125 | dev: true 126 | optional: true 127 | 128 | /@esbuild/linux-ppc64@0.18.20: 129 | resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==} 130 | engines: {node: '>=12'} 131 | cpu: [ppc64] 132 | os: [linux] 133 | requiresBuild: true 134 | dev: true 135 | optional: true 136 | 137 | /@esbuild/linux-riscv64@0.18.20: 138 | resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==} 139 | engines: {node: '>=12'} 140 | cpu: [riscv64] 141 | os: [linux] 142 | requiresBuild: true 143 | dev: true 144 | optional: true 145 | 146 | /@esbuild/linux-s390x@0.18.20: 147 | resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==} 148 | engines: {node: '>=12'} 149 | cpu: [s390x] 150 | os: [linux] 151 | requiresBuild: true 152 | dev: true 153 | optional: true 154 | 155 | /@esbuild/linux-x64@0.18.20: 156 | resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==} 157 | engines: {node: '>=12'} 158 | cpu: [x64] 159 | os: [linux] 160 | requiresBuild: true 161 | dev: true 162 | optional: true 163 | 164 | /@esbuild/netbsd-x64@0.18.20: 165 | resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==} 166 | engines: {node: '>=12'} 167 | cpu: [x64] 168 | os: [netbsd] 169 | requiresBuild: true 170 | dev: true 171 | optional: true 172 | 173 | /@esbuild/openbsd-x64@0.18.20: 174 | resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==} 175 | engines: {node: '>=12'} 176 | cpu: [x64] 177 | os: [openbsd] 178 | requiresBuild: true 179 | dev: true 180 | optional: true 181 | 182 | /@esbuild/sunos-x64@0.18.20: 183 | resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==} 184 | engines: {node: '>=12'} 185 | cpu: [x64] 186 | os: [sunos] 187 | requiresBuild: true 188 | dev: true 189 | optional: true 190 | 191 | /@esbuild/win32-arm64@0.18.20: 192 | resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==} 193 | engines: {node: '>=12'} 194 | cpu: [arm64] 195 | os: [win32] 196 | requiresBuild: true 197 | dev: true 198 | optional: true 199 | 200 | /@esbuild/win32-ia32@0.18.20: 201 | resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==} 202 | engines: {node: '>=12'} 203 | cpu: [ia32] 204 | os: [win32] 205 | requiresBuild: true 206 | dev: true 207 | optional: true 208 | 209 | /@esbuild/win32-x64@0.18.20: 210 | resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==} 211 | engines: {node: '>=12'} 212 | cpu: [x64] 213 | os: [win32] 214 | requiresBuild: true 215 | dev: true 216 | optional: true 217 | 218 | /@jest/schemas@29.6.0: 219 | resolution: {integrity: sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==} 220 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 221 | dependencies: 222 | '@sinclair/typebox': 0.27.8 223 | dev: true 224 | 225 | /@jridgewell/gen-mapping@0.3.3: 226 | resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} 227 | engines: {node: '>=6.0.0'} 228 | dependencies: 229 | '@jridgewell/set-array': 1.1.2 230 | '@jridgewell/sourcemap-codec': 1.4.15 231 | '@jridgewell/trace-mapping': 0.3.19 232 | dev: true 233 | 234 | /@jridgewell/resolve-uri@3.1.1: 235 | resolution: {integrity: sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==} 236 | engines: {node: '>=6.0.0'} 237 | dev: true 238 | 239 | /@jridgewell/set-array@1.1.2: 240 | resolution: {integrity: sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==} 241 | engines: {node: '>=6.0.0'} 242 | dev: true 243 | 244 | /@jridgewell/source-map@0.3.5: 245 | resolution: {integrity: sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==} 246 | dependencies: 247 | '@jridgewell/gen-mapping': 0.3.3 248 | '@jridgewell/trace-mapping': 0.3.19 249 | dev: true 250 | 251 | /@jridgewell/sourcemap-codec@1.4.15: 252 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 253 | dev: true 254 | 255 | /@jridgewell/trace-mapping@0.3.19: 256 | resolution: {integrity: sha512-kf37QtfW+Hwx/buWGMPcR60iF9ziHa6r/CZJIHbmcm4+0qrXiVdxegAH0F6yddEVQ7zdkjcGCgCzUu+BcbhQxw==} 257 | dependencies: 258 | '@jridgewell/resolve-uri': 3.1.1 259 | '@jridgewell/sourcemap-codec': 1.4.15 260 | dev: true 261 | 262 | /@sinclair/typebox@0.27.8: 263 | resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} 264 | dev: true 265 | 266 | /@types/chai-subset@1.3.3: 267 | resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} 268 | dependencies: 269 | '@types/chai': 4.3.5 270 | dev: true 271 | 272 | /@types/chai@4.3.5: 273 | resolution: {integrity: sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==} 274 | dev: true 275 | 276 | /@types/node@20.5.0: 277 | resolution: {integrity: sha512-Mgq7eCtoTjT89FqNoTzzXg2XvCi5VMhRV6+I2aYanc6kQCBImeNaAYRs/DyoVqk1YEUJK5gN9VO7HRIdz4Wo3Q==} 278 | dev: true 279 | 280 | /@vitest/expect@0.34.2: 281 | resolution: {integrity: sha512-EZm2dMNlLyIfDMha17QHSQcg2KjeAZaXd65fpPzXY5bvnfx10Lcaz3N55uEe8PhF+w4pw+hmrlHLLlRn9vkBJg==} 282 | dependencies: 283 | '@vitest/spy': 0.34.2 284 | '@vitest/utils': 0.34.2 285 | chai: 4.3.7 286 | dev: true 287 | 288 | /@vitest/runner@0.34.2: 289 | resolution: {integrity: sha512-8ydGPACVX5tK3Dl0SUwxfdg02h+togDNeQX3iXVFYgzF5odxvaou7HnquALFZkyVuYskoaHUOqOyOLpOEj5XTA==} 290 | dependencies: 291 | '@vitest/utils': 0.34.2 292 | p-limit: 4.0.0 293 | pathe: 1.1.1 294 | dev: true 295 | 296 | /@vitest/snapshot@0.34.2: 297 | resolution: {integrity: sha512-qhQ+xy3u4mwwLxltS4Pd4SR+XHv4EajiTPNY3jkIBLUApE6/ce72neJPSUQZ7bL3EBuKI+NhvzhGj3n5baRQUQ==} 298 | dependencies: 299 | magic-string: 0.30.2 300 | pathe: 1.1.1 301 | pretty-format: 29.6.2 302 | dev: true 303 | 304 | /@vitest/spy@0.34.2: 305 | resolution: {integrity: sha512-yd4L9OhfH6l0Av7iK3sPb3MykhtcRN5c5K5vm1nTbuN7gYn+yvUVVsyvzpHrjqS7EWqn9WsPJb7+0c3iuY60tA==} 306 | dependencies: 307 | tinyspy: 2.1.1 308 | dev: true 309 | 310 | /@vitest/utils@0.34.2: 311 | resolution: {integrity: sha512-Lzw+kAsTPubhoQDp1uVAOP6DhNia1GMDsI9jgB0yMn+/nDaPieYQ88lKqz/gGjSHL4zwOItvpehec9OY+rS73w==} 312 | dependencies: 313 | diff-sequences: 29.4.3 314 | loupe: 2.3.6 315 | pretty-format: 29.6.2 316 | dev: true 317 | 318 | /acorn-walk@8.2.0: 319 | resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} 320 | engines: {node: '>=0.4.0'} 321 | dev: true 322 | 323 | /acorn@8.10.0: 324 | resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} 325 | engines: {node: '>=0.4.0'} 326 | hasBin: true 327 | dev: true 328 | 329 | /ansi-styles@5.2.0: 330 | resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} 331 | engines: {node: '>=10'} 332 | dev: true 333 | 334 | /assertion-error@1.1.0: 335 | resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} 336 | dev: true 337 | 338 | /cac@6.7.14: 339 | resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 340 | engines: {node: '>=8'} 341 | dev: true 342 | 343 | /chai@4.3.7: 344 | resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} 345 | engines: {node: '>=4'} 346 | dependencies: 347 | assertion-error: 1.1.0 348 | check-error: 1.0.2 349 | deep-eql: 4.1.3 350 | get-func-name: 2.0.0 351 | loupe: 2.3.6 352 | pathval: 1.1.1 353 | type-detect: 4.0.8 354 | dev: true 355 | 356 | /check-error@1.0.2: 357 | resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} 358 | dev: true 359 | 360 | /debug@4.3.4: 361 | resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} 362 | engines: {node: '>=6.0'} 363 | peerDependencies: 364 | supports-color: '*' 365 | peerDependenciesMeta: 366 | supports-color: 367 | optional: true 368 | dependencies: 369 | ms: 2.1.2 370 | dev: true 371 | 372 | /deep-eql@4.1.3: 373 | resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} 374 | engines: {node: '>=6'} 375 | dependencies: 376 | type-detect: 4.0.8 377 | dev: true 378 | 379 | /diff-sequences@29.4.3: 380 | resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==} 381 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 382 | dev: true 383 | 384 | /dts-buddy@0.1.13: 385 | resolution: {integrity: sha512-5IZvzhiyKcnQ7UKeLAhHREF5lxKMBB2z0v+2UCLMa/cNlTRCbZB7CXpqmDyZu7cTwaky8IXuzGvvWLXeE+d83Q==} 386 | hasBin: true 387 | dependencies: 388 | '@jridgewell/source-map': 0.3.5 389 | '@jridgewell/sourcemap-codec': 1.4.15 390 | globrex: 0.1.2 391 | kleur: 4.1.5 392 | locate-character: 3.0.0 393 | magic-string: 0.30.2 394 | sade: 1.8.1 395 | tiny-glob: 0.2.9 396 | ts-api-utils: 0.0.46(typescript@5.1.6) 397 | typescript: 5.1.6 398 | dev: true 399 | 400 | /esbuild@0.18.20: 401 | resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==} 402 | engines: {node: '>=12'} 403 | hasBin: true 404 | requiresBuild: true 405 | optionalDependencies: 406 | '@esbuild/android-arm': 0.18.20 407 | '@esbuild/android-arm64': 0.18.20 408 | '@esbuild/android-x64': 0.18.20 409 | '@esbuild/darwin-arm64': 0.18.20 410 | '@esbuild/darwin-x64': 0.18.20 411 | '@esbuild/freebsd-arm64': 0.18.20 412 | '@esbuild/freebsd-x64': 0.18.20 413 | '@esbuild/linux-arm': 0.18.20 414 | '@esbuild/linux-arm64': 0.18.20 415 | '@esbuild/linux-ia32': 0.18.20 416 | '@esbuild/linux-loong64': 0.18.20 417 | '@esbuild/linux-mips64el': 0.18.20 418 | '@esbuild/linux-ppc64': 0.18.20 419 | '@esbuild/linux-riscv64': 0.18.20 420 | '@esbuild/linux-s390x': 0.18.20 421 | '@esbuild/linux-x64': 0.18.20 422 | '@esbuild/netbsd-x64': 0.18.20 423 | '@esbuild/openbsd-x64': 0.18.20 424 | '@esbuild/sunos-x64': 0.18.20 425 | '@esbuild/win32-arm64': 0.18.20 426 | '@esbuild/win32-ia32': 0.18.20 427 | '@esbuild/win32-x64': 0.18.20 428 | dev: true 429 | 430 | /fsevents@2.3.2: 431 | resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} 432 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 433 | os: [darwin] 434 | requiresBuild: true 435 | dev: true 436 | optional: true 437 | 438 | /get-func-name@2.0.0: 439 | resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} 440 | dev: true 441 | 442 | /globalyzer@0.1.0: 443 | resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} 444 | dev: true 445 | 446 | /globrex@0.1.2: 447 | resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} 448 | dev: true 449 | 450 | /jsonc-parser@3.2.0: 451 | resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} 452 | dev: true 453 | 454 | /kleur@4.1.5: 455 | resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} 456 | engines: {node: '>=6'} 457 | dev: true 458 | 459 | /local-pkg@0.4.3: 460 | resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} 461 | engines: {node: '>=14'} 462 | dev: true 463 | 464 | /locate-character@3.0.0: 465 | resolution: {integrity: sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==} 466 | dev: true 467 | 468 | /loupe@2.3.6: 469 | resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} 470 | dependencies: 471 | get-func-name: 2.0.0 472 | dev: true 473 | 474 | /magic-string@0.30.2: 475 | resolution: {integrity: sha512-lNZdu7pewtq/ZvWUp9Wpf/x7WzMTsR26TWV03BRZrXFsv+BI6dy8RAiKgm1uM/kyR0rCfUcqvOlXKG66KhIGug==} 476 | engines: {node: '>=12'} 477 | dependencies: 478 | '@jridgewell/sourcemap-codec': 1.4.15 479 | dev: true 480 | 481 | /mlly@1.4.0: 482 | resolution: {integrity: sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==} 483 | dependencies: 484 | acorn: 8.10.0 485 | pathe: 1.1.1 486 | pkg-types: 1.0.3 487 | ufo: 1.2.0 488 | dev: true 489 | 490 | /mri@1.2.0: 491 | resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} 492 | engines: {node: '>=4'} 493 | dev: true 494 | 495 | /ms@2.1.2: 496 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 497 | dev: true 498 | 499 | /nanoid@3.3.6: 500 | resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} 501 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 502 | hasBin: true 503 | dev: true 504 | 505 | /p-limit@4.0.0: 506 | resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} 507 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 508 | dependencies: 509 | yocto-queue: 1.0.0 510 | dev: true 511 | 512 | /pathe@1.1.1: 513 | resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} 514 | dev: true 515 | 516 | /pathval@1.1.1: 517 | resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} 518 | dev: true 519 | 520 | /picocolors@1.0.0: 521 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 522 | dev: true 523 | 524 | /pkg-types@1.0.3: 525 | resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} 526 | dependencies: 527 | jsonc-parser: 3.2.0 528 | mlly: 1.4.0 529 | pathe: 1.1.1 530 | dev: true 531 | 532 | /postcss@8.4.28: 533 | resolution: {integrity: sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==} 534 | engines: {node: ^10 || ^12 || >=14} 535 | dependencies: 536 | nanoid: 3.3.6 537 | picocolors: 1.0.0 538 | source-map-js: 1.0.2 539 | dev: true 540 | 541 | /pretty-format@29.6.2: 542 | resolution: {integrity: sha512-1q0oC8eRveTg5nnBEWMXAU2qpv65Gnuf2eCQzSjxpWFkPaPARwqZZDGuNE0zPAZfTCHzIk3A8dIjwlQKKLphyg==} 543 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 544 | dependencies: 545 | '@jest/schemas': 29.6.0 546 | ansi-styles: 5.2.0 547 | react-is: 18.2.0 548 | dev: true 549 | 550 | /react-is@18.2.0: 551 | resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} 552 | dev: true 553 | 554 | /rollup@3.28.0: 555 | resolution: {integrity: sha512-d7zhvo1OUY2SXSM6pfNjgD5+d0Nz87CUp4mt8l/GgVP3oBsPwzNvSzyu1me6BSG9JIgWNTVcafIXBIyM8yQ3yw==} 556 | engines: {node: '>=14.18.0', npm: '>=8.0.0'} 557 | hasBin: true 558 | optionalDependencies: 559 | fsevents: 2.3.2 560 | dev: true 561 | 562 | /sade@1.8.1: 563 | resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} 564 | engines: {node: '>=6'} 565 | dependencies: 566 | mri: 1.2.0 567 | dev: true 568 | 569 | /siginfo@2.0.0: 570 | resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 571 | dev: true 572 | 573 | /source-map-js@1.0.2: 574 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} 575 | engines: {node: '>=0.10.0'} 576 | dev: true 577 | 578 | /stackback@0.0.2: 579 | resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 580 | dev: true 581 | 582 | /std-env@3.4.0: 583 | resolution: {integrity: sha512-YqHeQIIQ8r1VtUZOTOyjsAXAsjr369SplZ5rlQaiJTBsvodvPSCME7vuz8pnQltbQ0Cw0lyFo5Q8uyNwYQ58Xw==} 584 | dev: true 585 | 586 | /strip-literal@1.3.0: 587 | resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} 588 | dependencies: 589 | acorn: 8.10.0 590 | dev: true 591 | 592 | /tiny-glob@0.2.9: 593 | resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} 594 | dependencies: 595 | globalyzer: 0.1.0 596 | globrex: 0.1.2 597 | dev: true 598 | 599 | /tinybench@2.5.0: 600 | resolution: {integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==} 601 | dev: true 602 | 603 | /tinypool@0.7.0: 604 | resolution: {integrity: sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==} 605 | engines: {node: '>=14.0.0'} 606 | dev: true 607 | 608 | /tinyspy@2.1.1: 609 | resolution: {integrity: sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==} 610 | engines: {node: '>=14.0.0'} 611 | dev: true 612 | 613 | /ts-api-utils@0.0.46(typescript@5.1.6): 614 | resolution: {integrity: sha512-YKJeSx39n0mMk+hrpyHKyTgxA3s7Pz/j1cXYR+t8HcwwZupzOR5xDGKnOEw3gmLaUeFUQt3FJD39AH9Ajn/mdA==} 615 | engines: {node: '>=16.13.0'} 616 | peerDependencies: 617 | typescript: '>=4.2.0' 618 | dependencies: 619 | typescript: 5.1.6 620 | dev: true 621 | 622 | /type-detect@4.0.8: 623 | resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} 624 | engines: {node: '>=4'} 625 | dev: true 626 | 627 | /typescript@5.1.6: 628 | resolution: {integrity: sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==} 629 | engines: {node: '>=14.17'} 630 | hasBin: true 631 | dev: true 632 | 633 | /ufo@1.2.0: 634 | resolution: {integrity: sha512-RsPyTbqORDNDxqAdQPQBpgqhWle1VcTSou/FraClYlHf6TZnQcGslpLcAphNR+sQW4q5lLWLbOsRlh9j24baQg==} 635 | dev: true 636 | 637 | /vite-node@0.34.2(@types/node@20.5.0): 638 | resolution: {integrity: sha512-JtW249Zm3FB+F7pQfH56uWSdlltCo1IOkZW5oHBzeQo0iX4jtC7o1t9aILMGd9kVekXBP2lfJBEQt9rBh07ebA==} 639 | engines: {node: '>=v14.18.0'} 640 | hasBin: true 641 | dependencies: 642 | cac: 6.7.14 643 | debug: 4.3.4 644 | mlly: 1.4.0 645 | pathe: 1.1.1 646 | picocolors: 1.0.0 647 | vite: 4.4.9(@types/node@20.5.0) 648 | transitivePeerDependencies: 649 | - '@types/node' 650 | - less 651 | - lightningcss 652 | - sass 653 | - stylus 654 | - sugarss 655 | - supports-color 656 | - terser 657 | dev: true 658 | 659 | /vite@4.4.9(@types/node@20.5.0): 660 | resolution: {integrity: sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==} 661 | engines: {node: ^14.18.0 || >=16.0.0} 662 | hasBin: true 663 | peerDependencies: 664 | '@types/node': '>= 14' 665 | less: '*' 666 | lightningcss: ^1.21.0 667 | sass: '*' 668 | stylus: '*' 669 | sugarss: '*' 670 | terser: ^5.4.0 671 | peerDependenciesMeta: 672 | '@types/node': 673 | optional: true 674 | less: 675 | optional: true 676 | lightningcss: 677 | optional: true 678 | sass: 679 | optional: true 680 | stylus: 681 | optional: true 682 | sugarss: 683 | optional: true 684 | terser: 685 | optional: true 686 | dependencies: 687 | '@types/node': 20.5.0 688 | esbuild: 0.18.20 689 | postcss: 8.4.28 690 | rollup: 3.28.0 691 | optionalDependencies: 692 | fsevents: 2.3.2 693 | dev: true 694 | 695 | /vitest@0.34.2: 696 | resolution: {integrity: sha512-WgaIvBbjsSYMq/oiMlXUI7KflELmzM43BEvkdC/8b5CAod4ryAiY2z8uR6Crbi5Pjnu5oOmhKa9sy7uk6paBxQ==} 697 | engines: {node: '>=v14.18.0'} 698 | hasBin: true 699 | peerDependencies: 700 | '@edge-runtime/vm': '*' 701 | '@vitest/browser': '*' 702 | '@vitest/ui': '*' 703 | happy-dom: '*' 704 | jsdom: '*' 705 | playwright: '*' 706 | safaridriver: '*' 707 | webdriverio: '*' 708 | peerDependenciesMeta: 709 | '@edge-runtime/vm': 710 | optional: true 711 | '@vitest/browser': 712 | optional: true 713 | '@vitest/ui': 714 | optional: true 715 | happy-dom: 716 | optional: true 717 | jsdom: 718 | optional: true 719 | playwright: 720 | optional: true 721 | safaridriver: 722 | optional: true 723 | webdriverio: 724 | optional: true 725 | dependencies: 726 | '@types/chai': 4.3.5 727 | '@types/chai-subset': 1.3.3 728 | '@types/node': 20.5.0 729 | '@vitest/expect': 0.34.2 730 | '@vitest/runner': 0.34.2 731 | '@vitest/snapshot': 0.34.2 732 | '@vitest/spy': 0.34.2 733 | '@vitest/utils': 0.34.2 734 | acorn: 8.10.0 735 | acorn-walk: 8.2.0 736 | cac: 6.7.14 737 | chai: 4.3.7 738 | debug: 4.3.4 739 | local-pkg: 0.4.3 740 | magic-string: 0.30.2 741 | pathe: 1.1.1 742 | picocolors: 1.0.0 743 | std-env: 3.4.0 744 | strip-literal: 1.3.0 745 | tinybench: 2.5.0 746 | tinypool: 0.7.0 747 | vite: 4.4.9(@types/node@20.5.0) 748 | vite-node: 0.34.2(@types/node@20.5.0) 749 | why-is-node-running: 2.2.2 750 | transitivePeerDependencies: 751 | - less 752 | - lightningcss 753 | - sass 754 | - stylus 755 | - sugarss 756 | - supports-color 757 | - terser 758 | dev: true 759 | 760 | /why-is-node-running@2.2.2: 761 | resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} 762 | engines: {node: '>=8'} 763 | hasBin: true 764 | dependencies: 765 | siginfo: 2.0.0 766 | stackback: 0.0.2 767 | dev: true 768 | 769 | /yocto-queue@1.0.0: 770 | resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} 771 | engines: {node: '>=12.20'} 772 | dev: true 773 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './walk.js'; 2 | export * from './types.d.ts'; 3 | -------------------------------------------------------------------------------- /src/types.d.ts: -------------------------------------------------------------------------------- 1 | type BaseNode = { type: string }; 2 | 3 | type NodeOf = X extends { type: T } ? X : never; 4 | 5 | type SpecialisedVisitors = { 6 | [K in T['type']]?: Visitor, U, T>; 7 | }; 8 | 9 | export type Visitor = (node: T, context: Context) => V | void; 10 | 11 | export type Visitors = T['type'] extends '_' 12 | ? never 13 | : SpecialisedVisitors & { _?: Visitor }; 14 | 15 | export interface Context { 16 | next: (state?: U) => T | void; 17 | path: T[]; 18 | state: U; 19 | stop: () => void; 20 | visit: (node: T, state?: U) => T; 21 | } 22 | -------------------------------------------------------------------------------- /src/walk.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @template {{type: string}} T 3 | * @template {Record | null} U 4 | * @param {T} node 5 | * @param {U} state 6 | * @param {import('./types').Visitors} visitors 7 | */ 8 | export function walk(node, state, visitors) { 9 | const universal = visitors._; 10 | 11 | let stopped = false; 12 | 13 | /** @type {import('./types').Visitor} _ */ 14 | function default_visitor(_, { next, state }) { 15 | next(state); 16 | } 17 | 18 | /** 19 | * @param {T} node 20 | * @param {T[]} path 21 | * @param {U} state 22 | * @returns {T | undefined} 23 | */ 24 | function visit(node, path, state) { 25 | // Don't return the node here or it could lead to false-positive mutation detection 26 | if (stopped) return; 27 | if (!node.type) return; 28 | 29 | /** @type {T | void} */ 30 | let result; 31 | 32 | /** @type {Record} */ 33 | const mutations = {}; 34 | 35 | /** @type {import('./types').Context} */ 36 | const context = { 37 | path, 38 | state, 39 | next: (next_state = state) => { 40 | path.push(node); 41 | for (const key in node) { 42 | if (key === 'type') continue; 43 | 44 | const child_node = node[key]; 45 | if (child_node && typeof child_node === 'object') { 46 | if (Array.isArray(child_node)) { 47 | /** @type {Record} */ 48 | const array_mutations = {}; 49 | 50 | child_node.forEach((node, i) => { 51 | if (node && typeof node === 'object') { 52 | const result = visit(node, path, next_state); 53 | if (result) array_mutations[i] = result; 54 | } 55 | }); 56 | 57 | if (Object.keys(array_mutations).length > 0) { 58 | mutations[key] = child_node.map( 59 | (node, i) => array_mutations[i] ?? node 60 | ); 61 | } 62 | } else { 63 | const result = visit( 64 | /** @type {T} */ (child_node), 65 | path, 66 | next_state 67 | ); 68 | 69 | // @ts-ignore 70 | if (result) { 71 | mutations[key] = result; 72 | } 73 | } 74 | } 75 | } 76 | path.pop(); 77 | 78 | if (Object.keys(mutations).length > 0) { 79 | return apply_mutations(node, mutations); 80 | } 81 | }, 82 | stop: () => { 83 | stopped = true; 84 | }, 85 | visit: (next_node, next_state = state) => { 86 | path.push(node); 87 | const result = visit(next_node, path, next_state) ?? next_node; 88 | path.pop(); 89 | return result; 90 | } 91 | }; 92 | 93 | let visitor = /** @type {import('./types').Visitor} */ ( 94 | visitors[/** @type {T['type']} */ (node.type)] ?? default_visitor 95 | ); 96 | 97 | if (universal) { 98 | /** @type {T | void} */ 99 | let inner_result; 100 | 101 | result = universal(node, { 102 | ...context, 103 | /** @param {U} next_state */ 104 | next: (next_state = state) => { 105 | state = next_state; // make it the default for subsequent specialised visitors 106 | 107 | inner_result = visitor(node, { 108 | ...context, 109 | state: next_state 110 | }); 111 | 112 | return inner_result; 113 | } 114 | }); 115 | 116 | // @ts-expect-error TypeScript doesn't understand that `context.next(...)` is called immediately 117 | if (!result && inner_result) { 118 | result = inner_result; 119 | } 120 | } else { 121 | result = visitor(node, context); 122 | } 123 | 124 | if (!result) { 125 | if (Object.keys(mutations).length > 0) { 126 | result = apply_mutations(node, mutations); 127 | } 128 | } 129 | 130 | if (result) { 131 | return result; 132 | } 133 | } 134 | 135 | return visit(node, [], state) ?? node; 136 | } 137 | 138 | /** 139 | * @template {Record} T 140 | * @param {T} node 141 | * @param {Record} mutations 142 | * @returns {T} 143 | */ 144 | function apply_mutations(node, mutations) { 145 | /** @type {Record} */ 146 | const obj = {}; 147 | 148 | const descriptors = Object.getOwnPropertyDescriptors(node); 149 | 150 | for (const key in descriptors) { 151 | Object.defineProperty(obj, key, descriptors[key]); 152 | } 153 | 154 | for (const key in mutations) { 155 | obj[key] = mutations[key]; 156 | } 157 | 158 | return /** @type {T} */ (obj); 159 | } 160 | -------------------------------------------------------------------------------- /test/transformation.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest'; 2 | import { walk } from '../src/walk.js'; 3 | 4 | test('transforms a tree', () => { 5 | /** @type {import('./types').TestNode} */ 6 | const tree = { 7 | type: 'Root', 8 | children: [{ type: 'A' }, { type: 'B' }, { type: 'C' }] 9 | }; 10 | 11 | let count = 0; 12 | 13 | const transformed = /** @type {import('./types').TransformedRoot} */ ( 14 | walk(/** @type {import('./types').TestNode} */ (tree), null, { 15 | Root: (node, { visit }) => { 16 | return { 17 | type: 'TransformedRoot', 18 | elements: node.children.map((child) => visit(child)) 19 | }; 20 | }, 21 | A: (node) => { 22 | count += 1; 23 | return { 24 | type: 'TransformedA' 25 | }; 26 | }, 27 | C: (node) => { 28 | count += 1; 29 | return { 30 | type: 'TransformedC' 31 | }; 32 | } 33 | }) 34 | ); 35 | 36 | expect(count).toBe(2); 37 | 38 | // check that `tree` wasn't mutated 39 | expect(tree).toEqual({ 40 | type: 'Root', 41 | children: [{ type: 'A' }, { type: 'B' }, { type: 'C' }] 42 | }); 43 | 44 | expect(transformed).toEqual({ 45 | type: 'TransformedRoot', 46 | elements: [ 47 | { type: 'TransformedA' }, 48 | { type: 'B' }, 49 | { type: 'TransformedC' } 50 | ] 51 | }); 52 | 53 | // expect the `B` node to have been preserved 54 | expect(transformed.elements[1]).toBe(tree.children[1]); 55 | }); 56 | 57 | test('respects individual visitors if universal visitor calls next()', () => { 58 | /** @type {import('./types').TestNode} */ 59 | const tree = { 60 | type: 'Root', 61 | children: [{ type: 'A' }, { type: 'B' }, { type: 'C' }] 62 | }; 63 | 64 | const transformed = walk( 65 | /** @type {import('./types').TestNode} */ (tree), 66 | null, 67 | { 68 | _(node, { state, next }) { 69 | next(state); 70 | }, 71 | A(node) { 72 | return { 73 | type: 'TransformedA' 74 | }; 75 | } 76 | } 77 | ); 78 | 79 | expect(transformed).toEqual({ 80 | type: 'Root', 81 | children: [{ type: 'TransformedA' }, { type: 'B' }, { type: 'C' }] 82 | }); 83 | }); 84 | 85 | test('returns the result of child transforms when calling next', () => { 86 | /** @type {import('./types').TestNode} */ 87 | const tree = { 88 | type: 'Root', 89 | children: [{ type: 'A' }, { type: 'B' }, { type: 'C' }] 90 | }; 91 | 92 | let count = 0; 93 | let children; 94 | 95 | const transformed = /** @type {import('./types').TestNode} */ ( 96 | walk(/** @type {import('./types').TestNode} */ (tree), null, { 97 | Root: (node, { next }) => { 98 | const result = /** @type {import('./types').Root} */ (next()); 99 | children = result.children; 100 | return node; 101 | }, 102 | A: (node) => { 103 | count += 1; 104 | return { 105 | type: 'TransformedA' 106 | }; 107 | }, 108 | C: (node) => { 109 | count += 1; 110 | return { 111 | type: 'TransformedC' 112 | }; 113 | } 114 | }) 115 | ); 116 | 117 | expect(count).toBe(2); 118 | 119 | // check that `tree` wasn't mutated 120 | expect(tree).toEqual({ 121 | type: 'Root', 122 | children: [{ type: 'A' }, { type: 'B' }, { type: 'C' }] 123 | }); 124 | 125 | expect(transformed).toBe(tree); 126 | 127 | expect(children).toEqual([ 128 | { type: 'TransformedA' }, 129 | { type: 'B' }, 130 | { type: 'TransformedC' } 131 | ]); 132 | }); 133 | 134 | test('returns undefined if there are no child transformations', () => { 135 | /** @type {import('./types').TestNode} */ 136 | const tree = { 137 | type: 'Root', 138 | children: [{ type: 'A' }, { type: 'B' }, { type: 'C' }] 139 | }; 140 | 141 | let result; 142 | 143 | const transformed = /** @type {import('./types').TestNode} */ ( 144 | walk(/** @type {import('./types').TestNode} */ (tree), null, { 145 | Root: (node, { next }) => { 146 | result = next(); 147 | } 148 | }) 149 | ); 150 | 151 | expect(transformed).toBe(tree); 152 | 153 | expect(result).toBe(undefined); 154 | }); 155 | 156 | test('keeps non-enumerable properties', () => { 157 | /** @type {import('./types').TestNode} */ 158 | const tree = { 159 | type: 'Root', 160 | children: [ 161 | { 162 | type: 'Root', 163 | children: [{ type: 'A' }, { type: 'B' }] 164 | }, 165 | { type: 'B' } 166 | ] 167 | }; 168 | 169 | Object.defineProperty(tree.children[0], 'metadata', { 170 | value: { foo: true }, 171 | enumerable: false 172 | }); 173 | 174 | const transformed = walk( 175 | /** @type {import('./types').TestNode} */ (tree), 176 | null, 177 | { 178 | A() { 179 | return { 180 | type: 'TransformedA' 181 | }; 182 | } 183 | } 184 | ); 185 | 186 | // @ts-ignore 187 | const { metadata } = transformed.children[0]; 188 | 189 | expect(metadata).toEqual({ 190 | foo: true 191 | }); 192 | }); 193 | 194 | test('doesnt mutate tree with non-type objects', () => { 195 | const tree = { 196 | type: 'Root', 197 | children: [{ type: 'A', metadata: { foo: true } }, { type: 'B' }] 198 | }; 199 | 200 | const transformed = walk(tree, null, {}); 201 | 202 | expect(transformed).toBe(tree); 203 | }); 204 | -------------------------------------------------------------------------------- /test/traversal.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest'; 2 | import { walk } from '../src/walk.js'; 3 | 4 | test('traverses a tree', () => { 5 | /** @type {import('./types').TestNode} */ 6 | const tree = { 7 | type: 'Root', 8 | children: [{ type: 'A' }, { type: 'B' }, { type: 'C' }] 9 | }; 10 | 11 | const state = { 12 | /** @type {string[]} */ 13 | visited: [], 14 | depth: 0 15 | }; 16 | 17 | walk(/** @type {import('./types').TestNode} */ (tree), state, { 18 | Root: (node, { state, next }) => { 19 | expect(node.type).toBe('Root'); 20 | state.visited.push(state.depth + 'root'); 21 | 22 | next({ ...state, depth: state.depth + 1 }); 23 | }, 24 | A: (node, { state }) => { 25 | expect(node.type).toBe('A'); 26 | state.visited.push(state.depth + 'a'); 27 | }, 28 | B: (node, { state }) => { 29 | expect(node.type).toBe('B'); 30 | state.visited.push(state.depth + 'b'); 31 | }, 32 | C: (node, { state }) => { 33 | expect(node.type).toBe('C'); 34 | state.visited.push(state.depth + 'c'); 35 | } 36 | }); 37 | 38 | expect(state.visited).toEqual(['0root', '1a', '1b', '1c']); 39 | }); 40 | 41 | test('visits all nodes with _', () => { 42 | /** @type {import('./types').TestNode} */ 43 | const tree = { 44 | type: 'Root', 45 | children: [{ type: 'A' }, { type: 'B' }, { type: 'C' }] 46 | }; 47 | 48 | let uid = 1; 49 | 50 | const state = { 51 | id: uid, 52 | depth: 0 53 | }; 54 | 55 | /** @type {string[]} */ 56 | const log = []; 57 | 58 | walk(/** @type {import('./types').TestNode} */ (tree), state, { 59 | _: (node, { state, next }) => { 60 | log.push(`${state.depth} ${state.id} enter ${node.type}`); 61 | next({ ...state, id: ++uid }); 62 | log.push(`${state.depth} ${state.id} leave ${node.type}`); 63 | }, 64 | Root: (node, { state, next }) => { 65 | log.push(`${state.depth} ${state.id} visit ${node.type}`); 66 | next({ ...state, depth: state.depth + 1 }); 67 | }, 68 | A: (node, { state }) => { 69 | log.push(`${state.depth} ${state.id} visit ${node.type}`); 70 | }, 71 | B: (node, { state }) => { 72 | log.push(`${state.depth} ${state.id} visit ${node.type}`); 73 | }, 74 | C: (node, { state }) => { 75 | log.push(`${state.depth} ${state.id} visit ${node.type}`); 76 | } 77 | }); 78 | 79 | expect(log).toEqual([ 80 | '0 1 enter Root', 81 | '0 2 visit Root', 82 | '1 2 enter A', 83 | '1 3 visit A', 84 | '1 2 leave A', 85 | '1 2 enter B', 86 | '1 4 visit B', 87 | '1 2 leave B', 88 | '1 2 enter C', 89 | '1 5 visit C', 90 | '1 2 leave C', 91 | '0 1 leave Root' 92 | ]); 93 | }); 94 | 95 | test('state is passed to children of specialized visitor when using universal visitor', () => { 96 | /** @type {import('./types').TestNode} */ 97 | const tree = { 98 | type: 'Root', 99 | children: [{ type: 'A' }, { type: 'B' }, { type: 'C' }] 100 | }; 101 | 102 | const state = { 103 | universal: false 104 | }; 105 | 106 | /** @type {string[]} */ 107 | const log = []; 108 | 109 | walk(/** @type {import('./types').TestNode} */ (tree), state, { 110 | _(node, { next }) { 111 | if (node.type === 'Root') { 112 | next({ universal: true }); 113 | } else { 114 | next(); 115 | } 116 | }, 117 | Root(node, { state, visit }) { 118 | log.push(`visited ${node.type} ${state.universal}`); 119 | 120 | for (const child of node.children) { 121 | visit(child); 122 | } 123 | }, 124 | A(node, { state, visit }) { 125 | log.push(`visited ${node.type} ${state.universal}`); 126 | }, 127 | B(node, { state, visit }) { 128 | log.push(`visited ${node.type} ${state.universal}`); 129 | }, 130 | C(node, { state, visit }) { 131 | log.push(`visited ${node.type} ${state.universal}`); 132 | } 133 | }); 134 | 135 | expect(log).toEqual([ 136 | 'visited Root true', 137 | 'visited A true', 138 | 'visited B true', 139 | 'visited C true' 140 | ]); 141 | }); 142 | 143 | test('path is pushed and popped correctly using next', () => { 144 | /** @type {import('./types').TestNode} */ 145 | const tree = { 146 | type: 'Root', 147 | children: [ 148 | { type: 'Root', children: [{ type: 'A' }] }, 149 | { type: 'B' }, 150 | { type: 'C' } 151 | ] 152 | }; 153 | 154 | /** @type {string[]} */ 155 | const log = []; 156 | 157 | /** 158 | * @param {import('./types').TestNode} node 159 | * @param {import('./types').TestNode[]} path 160 | */ 161 | function log_path(node, path) { 162 | log.push(`visited ${node.type} ${JSON.stringify(path.map((n) => n.type))}`); 163 | } 164 | 165 | walk( 166 | /** @type {import('./types').TestNode} */ (tree), 167 | {}, 168 | { 169 | Root(node, { path, next }) { 170 | log_path(node, path); 171 | next(); 172 | }, 173 | A(node, { path }) { 174 | log_path(node, path); 175 | }, 176 | B(node, { path }) { 177 | log_path(node, path); 178 | }, 179 | C(node, { path }) { 180 | log_path(node, path); 181 | } 182 | } 183 | ); 184 | 185 | expect(log).toEqual([ 186 | 'visited Root []', 187 | 'visited Root ["Root"]', 188 | 'visited A ["Root","Root"]', 189 | 'visited B ["Root"]', 190 | 'visited C ["Root"]' 191 | ]); 192 | }); 193 | 194 | test('path is pushed and popped correctly using visit', () => { 195 | /** @type {import('./types').TestNode} */ 196 | const tree = { 197 | type: 'Root', 198 | children: [ 199 | { type: 'Root', children: [{ type: 'A' }] }, 200 | { type: 'B' }, 201 | { type: 'C' } 202 | ] 203 | }; 204 | 205 | /** @type {string[]} */ 206 | const log = []; 207 | 208 | /** 209 | * @param {import('./types').TestNode} node 210 | * @param {import('./types').TestNode[]} path 211 | */ 212 | function log_path(node, path) { 213 | log.push(`visited ${node.type} ${JSON.stringify(path.map((n) => n.type))}`); 214 | } 215 | 216 | walk( 217 | /** @type {import('./types').TestNode} */ (tree), 218 | {}, 219 | { 220 | Root(node, { path, visit }) { 221 | log_path(node, path); 222 | 223 | for (const child of node.children) { 224 | if (child.type !== 'C') { 225 | visit(child); 226 | } 227 | } 228 | }, 229 | A(node, { path }) { 230 | log_path(node, path); 231 | }, 232 | B(node, { path }) { 233 | log_path(node, path); 234 | }, 235 | C(node, { path }) { 236 | log_path(node, path); 237 | } 238 | } 239 | ); 240 | 241 | expect(log).toEqual([ 242 | 'visited Root []', 243 | 'visited Root ["Root"]', 244 | 'visited A ["Root","Root"]', 245 | 'visited B ["Root"]' 246 | ]); 247 | }); 248 | -------------------------------------------------------------------------------- /test/types.d.ts: -------------------------------------------------------------------------------- 1 | export interface Root { 2 | type: 'Root'; 3 | children: TestNode[]; 4 | metadata?: any; 5 | } 6 | 7 | export interface A { 8 | type: 'A'; 9 | metadata?: any; 10 | } 11 | 12 | export interface B { 13 | type: 'B'; 14 | } 15 | 16 | export interface C { 17 | type: 'C'; 18 | } 19 | 20 | export interface TransformedRoot { 21 | type: 'TransformedRoot'; 22 | elements: TestNode[]; 23 | } 24 | 25 | export interface TransformedA { 26 | type: 'TransformedA'; 27 | } 28 | 29 | export interface TransformedB { 30 | type: 'TransformedB'; 31 | } 32 | 33 | export interface TransformedC { 34 | type: 'TransformedC'; 35 | } 36 | 37 | export type TestNode = 38 | | Root 39 | | A 40 | | B 41 | | C 42 | | TransformedRoot 43 | | TransformedA 44 | | TransformedB 45 | | TransformedC; 46 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "checkJs": true, 5 | "module": "esnext", 6 | "target": "esnext", 7 | "moduleResolution": "bundler", 8 | "noEmit": true, 9 | "strict": true, 10 | "skipLibCheck": true 11 | }, 12 | "include": ["./src", "./test", "./vitest.config.js"] 13 | } -------------------------------------------------------------------------------- /vitest.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vitest/config'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | include: ['./test/*.js'] 6 | } 7 | }); 8 | --------------------------------------------------------------------------------