├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── .husky └── pre-commit ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── eslint.config.js ├── package-lock.json ├── package.json ├── src ├── arborist.js ├── flast.js ├── index.js ├── types.js └── utils │ ├── applyIteratively.js │ ├── index.js │ ├── logger.js │ └── treeModifier.js └── tests ├── arborist.test.js ├── functionality.test.js ├── parsing.test.js └── utils.test.js /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Run Tests 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [18.x, 20.x, 22.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm install 30 | - run: npm run test:coverage 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .cursor 3 | node_modules/ 4 | .idea/ 5 | *tmp*/ 6 | *tmp*.* 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm test 2 | npx eslint . -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | When contributing to this repository, please first discuss the change you wish to make via issue, 4 | email, or any other method with the owners of this repository before making a change. 5 | 6 | Please note we have a code of conduct, please follow it in all your interactions with the project. 7 | 8 | ## Pull Request Process 9 | 10 | 1. Ensure any install or build dependencies are removed before the end of the layer when doing a 11 | build. 12 | 2. Update the README.md with details of changes to the interface, this includes new environment 13 | variables, exposed ports, useful file locations and container parameters. 14 | 3. Increase the version numbers in any examples files and the README.md to the new version that this 15 | Pull Request would represent. The versioning scheme we use is [SemVer](http://semver.org/). 16 | 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you 17 | do not have permission to do that, you may request the second reviewer to merge it for you. 18 | 19 | ## Code of Conduct 20 | 21 | ### Our Pledge 22 | 23 | We as members, contributors, and leaders pledge to make participation in our 24 | community a harassment-free experience for everyone, regardless of age, body 25 | size, visible or invisible disability, ethnicity, sex characteristics, gender 26 | identity and expression, level of experience, education, socio-economic status, 27 | nationality, personal appearance, race, caste, color, religion, or sexual identity 28 | and orientation. 29 | 30 | We pledge to act and interact in ways that contribute to an open, welcoming, 31 | diverse, inclusive, and healthy community. 32 | 33 | ### Our Standards 34 | 35 | Examples of behavior that contributes to a positive environment for our 36 | community include: 37 | 38 | * Demonstrating empathy and kindness toward other people 39 | * Being respectful of differing opinions, viewpoints, and experiences 40 | * Giving and gracefully accepting constructive feedback 41 | * Accepting responsibility and apologizing to those affected by our mistakes, 42 | and learning from the experience 43 | * Focusing on what is best not just for us as individuals, but for the 44 | overall community 45 | 46 | Examples of unacceptable behavior include: 47 | 48 | * The use of sexualized language or imagery, and sexual attention or 49 | advances of any kind 50 | * Trolling, insulting or derogatory comments, and personal or political attacks 51 | * Public or private harassment 52 | * Publishing others' private information, such as a physical or email 53 | address, without their explicit permission 54 | * Other conduct which could reasonably be considered inappropriate in a 55 | professional setting 56 | 57 | ### Enforcement Responsibilities 58 | 59 | Community leaders are responsible for clarifying and enforcing our standards of 60 | acceptable behavior and will take appropriate and fair corrective action in 61 | response to any behavior that they deem inappropriate, threatening, offensive, 62 | or harmful. 63 | 64 | Community leaders have the right and responsibility to remove, edit, or reject 65 | comments, commits, code, wiki edits, issues, and other contributions that are 66 | not aligned to this Code of Conduct, and will communicate reasons for moderation 67 | decisions when appropriate. 68 | 69 | ### Scope 70 | 71 | This Code of Conduct applies within all community spaces, and also applies when 72 | an individual is officially representing the community in public spaces. 73 | Examples of representing our community include using an official e-mail address, 74 | posting via an official social media account, or acting as an appointed 75 | representative at an online or offline event. 76 | 77 | ### Enforcement 78 | 79 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 80 | reported to the community leaders responsible for enforcement at 81 | [ben.baryo@humansecurity.com](mailto:ben.baryo@humansecurity.com). 82 | All complaints will be reviewed and investigated promptly and fairly. 83 | 84 | All community leaders are obligated to respect the privacy and security of the 85 | reporter of any incident. 86 | 87 | ### Enforcement Guidelines 88 | 89 | Community leaders will follow these Community Impact Guidelines in determining 90 | the consequences for any action they deem in violation of this Code of Conduct: 91 | 92 | #### 1. Correction 93 | 94 | **Community Impact**: Use of inappropriate language or other behavior deemed 95 | unprofessional or unwelcome in the community. 96 | 97 | **Consequence**: A private, written warning from community leaders, providing 98 | clarity around the nature of the violation and an explanation of why the 99 | behavior was inappropriate. A public apology may be requested. 100 | 101 | #### 2. Warning 102 | 103 | **Community Impact**: A violation through a single incident or series 104 | of actions. 105 | 106 | **Consequence**: A warning with consequences for continued behavior. No 107 | interaction with the people involved, including unsolicited interaction with 108 | those enforcing the Code of Conduct, for a specified period of time. This 109 | includes avoiding interactions in community spaces as well as external channels 110 | like social media. Violating these terms may lead to a temporary or 111 | permanent ban. 112 | 113 | #### 3. Temporary Ban 114 | 115 | **Community Impact**: A serious violation of community standards, including 116 | sustained inappropriate behavior. 117 | 118 | **Consequence**: A temporary ban from any sort of interaction or public 119 | communication with the community for a specified period of time. No public or 120 | private interaction with the people involved, including unsolicited interaction 121 | with those enforcing the Code of Conduct, is allowed during this period. 122 | Violating these terms may lead to a permanent ban. 123 | 124 | #### 4. Permanent Ban 125 | 126 | **Community Impact**: Demonstrating a pattern of violation of community 127 | standards, including sustained inappropriate behavior, harassment of an 128 | individual, or aggression toward or disparagement of classes of individuals. 129 | 130 | **Consequence**: A permanent ban from any sort of public interaction within 131 | the community. 132 | 133 | ### Attribution 134 | 135 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 136 | version 2.0, available at 137 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 138 | 139 | Community Impact Guidelines were inspired by 140 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 141 | 142 | For answers to common questions about this code of conduct, see the FAQ at 143 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 144 | at [https://www.contributor-covenant.org/translations][translations]. 145 | 146 | [homepage]: https://www.contributor-covenant.org 147 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 148 | [Mozilla CoC]: https://github.com/mozilla/diversity 149 | [FAQ]: https://www.contributor-covenant.org/faq 150 | [translations]: https://www.contributor-covenant.org/translations -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 PerimeterX 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 | # flAST - FLat Abstract Syntax Tree 2 | [![Run Tests](https://github.com/PerimeterX/flast/actions/workflows/node.js.yml/badge.svg?branch=main)](https://github.com/PerimeterX/flast/actions/workflows/node.js.yml) 3 | [![Downloads](https://img.shields.io/npm/dm/flast.svg?maxAge=43200)](https://www.npmjs.com/package/flast) 4 | 5 | Efficient, flat, and richly-annotated JavaScript AST manipulation for code transformation, analysis, and more. 6 | 7 | For comments and suggestions feel free to open an issue or find me on Twitter/X - [@ctrl__esc](https://x.com/ctrl__esc) 8 | 9 | ## Table of Contents 10 | * [Installation](#installation) 11 | * [Features](#features) 12 | * [Usage Examples](#usage-examples) 13 | * [How to Contribute](#how-to-contribute) 14 | * [Projects Using flAST](#projects-using-flast) 15 | 16 | --- 17 | 18 | ## Installation 19 | Requires Node 18 or newer. 20 | 21 | ### npm 22 | ```bash 23 | npm install flast 24 | ``` 25 | 26 | ### Clone the Repo 27 | ```bash 28 | git clone git@github.com:PerimeterX/flast.git 29 | cd flast 30 | npm install 31 | ``` 32 | --- 33 | 34 | ## Features 35 | 36 | ### Parsing and Code Generation 37 | - **Code to AST:** Parse JavaScript code into a flat, richly annotated AST. 38 | - **AST to Code:** Generate code from any AST node, supporting round-trip transformations. 39 | 40 | ### Flat AST Structure 41 | - **Flat AST (`generateFlatAST`):** All nodes are in a single array, allowing direct access and efficient traversal without recursive tree-walking. 42 | - **Type Map:** `ast[0].typeMap` provides fast lookup of all nodes by type. 43 | - **Scopes:** `ast[0].allScopes` gives direct access to all lexical scopes. 44 | 45 | ### Node Richness 46 | Each node in the flat AST includes: 47 | - `src`: The original code for this node. 48 | - `parentNode` and `childNodes`: Easy navigation and context. 49 | - `parentKey`: The property name this node occupies in its parent. 50 | - `declNode`: For variables, a reference to their declaration node. 51 | - `references`: For declarations, a list of all reference nodes. 52 | - `lineage`: Traceable ancestry of scopes for each node. 53 | - `nodeId`: Unique identifier for each node. 54 | - `scope`, `scopeId`, and more for advanced analysis. 55 | 56 | ### Arborist: AST Modification 57 | - **Delete nodes:** Mark nodes for removal and apply changes safely. 58 | - **Replace nodes:** Mark nodes for replacement, with all changes validated and applied in a single pass. 59 | 60 | ### Utilities 61 | - **applyIteratively:** Apply a series of transformation functions (using Arborist) to the AST/code, iterating until no further changes are made. Automatically reverts changes that break the code. 62 | - **logger:** Simple log utility that can be controlled downstream and used for debugging or custom output. 63 | - **treeModifier:** (Deprecated) Simple wrapper for AST iteration. 64 | --- 65 | 66 | ## Usage Examples 67 | 68 | > **Tip:** 69 | > For best performance, always iterate over only the relevant node types using `ast[0].typeMap`. For example, to process all identifiers and variable declarations: 70 | > ```js 71 | > const relevantNodes = [ 72 | > ...ast[0].typeMap.Identifier, 73 | > ...ast[0].typeMap.VariableDeclarator, 74 | > ]; 75 | > for (let i = 0; i < relevantNodes.length; i++) { 76 | > const n = relevantNodes[i]; 77 | > // ... process n ... 78 | > } 79 | > ``` 80 | > Only iterate over the entire AST as a last resort. 81 | 82 | ### Basic Example 83 | ```js 84 | import {Arborist} from 'flast'; 85 | 86 | const replacements = {'Hello': 'General', 'there!': 'Kenobi'}; 87 | 88 | const arb = new Arborist(`console.log('Hello' + ' ' + 'there!');`); 89 | // This is equivalent to: 90 | // const ast = generateFlatAST(`console.log('Hello' + ' ' + 'there!');`); 91 | // const arb = new Arborist(ast); 92 | // Since the Arborist accepts either code as a string or a flat AST object. 93 | 94 | for (let i = 0; i < arb.ast.length; i++) { 95 | const n = arb.ast[i]; 96 | if (n.type === 'Literal' && replacements[n.value]) { 97 | arb.markNode(n, { 98 | type: 'Literal', 99 | value: replacements[n.value], 100 | raw: `'${replacements[n.value]}'`, 101 | }); 102 | } 103 | } 104 | arb.applyChanges(); 105 | console.log(arb.script); // console.log('General' + ' ' + 'Kenobi'); 106 | ``` 107 | --- 108 | 109 | ### Example 1: Numeric Calculation Simplification 110 | Replace constant numeric expressions with their computed value. 111 | ```js 112 | import {applyIteratively} from 'flast'; 113 | 114 | function simplifyNumericExpressions(arb) { 115 | const binaryNodes = arb.ast[0].typeMap.BinaryExpression || []; 116 | for (let i = 0; i < binaryNodes.length; i++) { 117 | const n = binaryNodes[i]; 118 | if (n.left.type === 'Literal' && typeof n.left.value === 'number' && 119 | n.right.type === 'Literal' && typeof n.right.value === 'number') { 120 | let result; 121 | switch (n.operator) { 122 | case '+': result = n.left.value + n.right.value; break; 123 | case '-': result = n.left.value - n.right.value; break; 124 | case '*': result = n.left.value * n.right.value; break; 125 | case '/': result = n.left.value / n.right.value; break; 126 | default: continue; 127 | } 128 | arb.markNode(n, {type: 'Literal', value: result, raw: String(result)}); 129 | } 130 | } 131 | return arb; 132 | } 133 | 134 | const script = 'let x = 5 * 3 + 1;'; 135 | const result = applyIteratively(script, [simplifyNumericExpressions]); 136 | console.log(result); // let x = 16; 137 | ``` 138 | --- 139 | 140 | ### Example 2: Transform Arrow Function to Regular Function 141 | ```js 142 | import {applyIteratively} from 'flast'; 143 | 144 | function arrowToFunction(arb) { 145 | const arrowNodes = arb.ast[0].typeMap.ArrowFunctionExpression || []; 146 | for (let i = 0; i < arrowNodes.length; i++) { 147 | const n = arrowNodes[i]; 148 | arb.markNode(n, { 149 | type: 'FunctionExpression', 150 | id: null, 151 | params: n.params, 152 | body: n.body.type === 'BlockStatement' ? n.body : {type: 'BlockStatement', body: [{ type: 'ReturnStatement', argument: n.body }] }, 153 | generator: false, 154 | async: n.async, 155 | expression: false, 156 | }); 157 | } 158 | return arb; 159 | } 160 | 161 | const script = 'const f = (a, b) => a + b;'; 162 | const result = applyIteratively(script, [arrowToFunction]); 163 | console.log(result); 164 | /* 165 | const f = function(a, b) { 166 | return a + b; 167 | }; 168 | */ 169 | ``` 170 | --- 171 | 172 | ### Example 3: Modify Nodes Based on Attached Comments 173 | Suppose you want to double any numeric literal that has a comment `// double` attached: 174 | ```js 175 | import {applyIteratively} from 'flast'; 176 | 177 | function doubleLiteralsWithComment(arb) { 178 | const literalNodes = arb.ast[0].typeMap.Literal || []; 179 | for (let i = 0; i < literalNodes.length; i++) { 180 | const n = literalNodes[i]; 181 | if ( 182 | typeof n.value === 'number' && 183 | n.leadingComments && 184 | n.leadingComments.some(c => c.value.includes('double')) 185 | ) { 186 | arb.markNode(n, { type: 'Literal', value: n.value * 2, raw: String(n.value * 2) }); 187 | } 188 | } 189 | return arb; 190 | } 191 | 192 | const script = 'const x = /* double */ 21;'; 193 | const result = applyIteratively(script, [doubleLiteralsWithComment], 1); // Last argument is the maximum number of iterations allowed. 194 | console.log(result); // const x = /* double */ 42; 195 | ``` 196 | --- 197 | 198 | ### Example 4: Proxy Variable Replacement Using References 199 | Replace all references to a variable that is a proxy for another variable. 200 | ```js 201 | import {applyIteratively} from 'flast'; 202 | 203 | function replaceProxyVars(arb) { 204 | const declarators = arb.ast[0].typeMap.VariableDeclarator || []; 205 | for (let i = 0; i < declarators.length; i++) { 206 | const n = declarators[i]; 207 | if (n.init && n.init.type === 'Identifier' && n.id && n.id.name) { 208 | // Replace all references to this variable with the variable it proxies 209 | const refs = n.references || []; 210 | for (let j = 0; j < refs.length; j++) { 211 | const ref = refs[j]; 212 | arb.markNode(ref, { 213 | type: 'Identifier', 214 | name: n.init.name, 215 | }); 216 | } 217 | } 218 | } 219 | return arb; 220 | } 221 | 222 | const script = 'var a = b; var b = 42; console.log(a);'; 223 | const result = applyIteratively(script, [replaceProxyVars]); 224 | console.log(result); // var a = b; var b = 42; console.log(b); 225 | ``` 226 | --- 227 | 228 | ## Projects Using flAST 229 | - **[Obfuscation-Detector](https://github.com/PerimeterX/obfuscation-detector):** Uses flAST to analyze and detect unique obfuscation in JavaScript code. 230 | - **[REstringer](https://github.com/PerimeterX/restringer):** Uses flAST for advanced code transformation and analysis. 231 | 232 | --- 233 | 234 | ## Best Practices 235 | 236 | ### AST Mutability 237 | You can directly mutate nodes in the flat AST (e.g., changing properties, adding or removing nodes). However, for safety and script validity, it's best to use the Arborist for all structural changes. The Arborist verifies your changes and prevents breaking the code, ensuring that the resulting AST remains valid and that all node information is updated correctly. 238 | 239 | - **Direct mutation is possible**, but should be used with caution. 240 | - **Recommended:** Use the Arborist's `markNode` method for all node deletions and replacements. 241 | 242 | ## How to Contribute 243 | To contribute to this project see our [contribution guide](CONTRIBUTING.md) 244 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | import path from "node:path"; 3 | import { fileURLToPath } from "node:url"; 4 | import js from "@eslint/js"; 5 | import { FlatCompat } from "@eslint/eslintrc"; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | const compat = new FlatCompat({ 10 | baseDirectory: __dirname, 11 | recommendedConfig: js.configs.recommended, 12 | allConfig: js.configs.all 13 | }); 14 | 15 | export default [{ 16 | ignores: ["**/*tmp*/", "**/*tmp*.*", "eslint.config.js", "node_modules/"], 17 | }, ...compat.extends("eslint:recommended"), { 18 | languageOptions: { 19 | globals: { 20 | ...globals.browser, 21 | ...globals.node, 22 | ...globals.commonjs, 23 | }, 24 | 25 | ecmaVersion: "latest", 26 | sourceType: "module", 27 | }, 28 | 29 | rules: { 30 | indent: ["error", "tab", { 31 | SwitchCase: 1, 32 | }], 33 | 34 | "linebreak-style": ["error", "unix"], 35 | 36 | quotes: ["error", "single", { 37 | allowTemplateLiterals: true, 38 | }], 39 | 40 | semi: ["error", "always"], 41 | "no-empty": ["off"], 42 | }, 43 | }]; -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flast", 3 | "version": "2.2.5", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "flast", 9 | "version": "2.2.5", 10 | "license": "MIT", 11 | "dependencies": { 12 | "escodegen": "npm:@javascript-obfuscator/escodegen", 13 | "eslint-scope": "^8.3.0", 14 | "espree": "^10.3.0" 15 | }, 16 | "devDependencies": { 17 | "eslint": "^9.26.0", 18 | "husky": "^9.1.7" 19 | } 20 | }, 21 | "node_modules/@eslint-community/eslint-utils": { 22 | "version": "4.7.0", 23 | "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", 24 | "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", 25 | "dev": true, 26 | "license": "MIT", 27 | "dependencies": { 28 | "eslint-visitor-keys": "^3.4.3" 29 | }, 30 | "engines": { 31 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 32 | }, 33 | "funding": { 34 | "url": "https://opencollective.com/eslint" 35 | }, 36 | "peerDependencies": { 37 | "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" 38 | } 39 | }, 40 | "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { 41 | "version": "3.4.3", 42 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", 43 | "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", 44 | "dev": true, 45 | "license": "Apache-2.0", 46 | "engines": { 47 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" 48 | }, 49 | "funding": { 50 | "url": "https://opencollective.com/eslint" 51 | } 52 | }, 53 | "node_modules/@eslint-community/regexpp": { 54 | "version": "4.12.1", 55 | "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", 56 | "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", 57 | "dev": true, 58 | "license": "MIT", 59 | "engines": { 60 | "node": "^12.0.0 || ^14.0.0 || >=16.0.0" 61 | } 62 | }, 63 | "node_modules/@eslint/config-array": { 64 | "version": "0.20.0", 65 | "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.20.0.tgz", 66 | "integrity": "sha512-fxlS1kkIjx8+vy2SjuCB94q3htSNrufYTXubwiBFeaQHbH6Ipi43gFJq2zCMt6PHhImH3Xmr0NksKDvchWlpQQ==", 67 | "dev": true, 68 | "license": "Apache-2.0", 69 | "dependencies": { 70 | "@eslint/object-schema": "^2.1.6", 71 | "debug": "^4.3.1", 72 | "minimatch": "^3.1.2" 73 | }, 74 | "engines": { 75 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 76 | } 77 | }, 78 | "node_modules/@eslint/config-helpers": { 79 | "version": "0.2.2", 80 | "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.2.tgz", 81 | "integrity": "sha512-+GPzk8PlG0sPpzdU5ZvIRMPidzAnZDl/s9L+y13iodqvb8leL53bTannOrQ/Im7UkpsmFU5Ily5U60LWixnmLg==", 82 | "dev": true, 83 | "license": "Apache-2.0", 84 | "engines": { 85 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 86 | } 87 | }, 88 | "node_modules/@eslint/core": { 89 | "version": "0.14.0", 90 | "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz", 91 | "integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==", 92 | "dev": true, 93 | "license": "Apache-2.0", 94 | "dependencies": { 95 | "@types/json-schema": "^7.0.15" 96 | }, 97 | "engines": { 98 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 99 | } 100 | }, 101 | "node_modules/@eslint/eslintrc": { 102 | "version": "3.3.1", 103 | "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", 104 | "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", 105 | "dev": true, 106 | "license": "MIT", 107 | "dependencies": { 108 | "ajv": "^6.12.4", 109 | "debug": "^4.3.2", 110 | "espree": "^10.0.1", 111 | "globals": "^14.0.0", 112 | "ignore": "^5.2.0", 113 | "import-fresh": "^3.2.1", 114 | "js-yaml": "^4.1.0", 115 | "minimatch": "^3.1.2", 116 | "strip-json-comments": "^3.1.1" 117 | }, 118 | "engines": { 119 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 120 | }, 121 | "funding": { 122 | "url": "https://opencollective.com/eslint" 123 | } 124 | }, 125 | "node_modules/@eslint/js": { 126 | "version": "9.27.0", 127 | "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz", 128 | "integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==", 129 | "dev": true, 130 | "license": "MIT", 131 | "engines": { 132 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 133 | }, 134 | "funding": { 135 | "url": "https://eslint.org/donate" 136 | } 137 | }, 138 | "node_modules/@eslint/object-schema": { 139 | "version": "2.1.6", 140 | "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", 141 | "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", 142 | "dev": true, 143 | "license": "Apache-2.0", 144 | "engines": { 145 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 146 | } 147 | }, 148 | "node_modules/@eslint/plugin-kit": { 149 | "version": "0.3.1", 150 | "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz", 151 | "integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==", 152 | "dev": true, 153 | "license": "Apache-2.0", 154 | "dependencies": { 155 | "@eslint/core": "^0.14.0", 156 | "levn": "^0.4.1" 157 | }, 158 | "engines": { 159 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 160 | } 161 | }, 162 | "node_modules/@humanfs/core": { 163 | "version": "0.19.1", 164 | "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", 165 | "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", 166 | "dev": true, 167 | "license": "Apache-2.0", 168 | "engines": { 169 | "node": ">=18.18.0" 170 | } 171 | }, 172 | "node_modules/@humanfs/node": { 173 | "version": "0.16.6", 174 | "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", 175 | "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", 176 | "dev": true, 177 | "license": "Apache-2.0", 178 | "dependencies": { 179 | "@humanfs/core": "^0.19.1", 180 | "@humanwhocodes/retry": "^0.3.0" 181 | }, 182 | "engines": { 183 | "node": ">=18.18.0" 184 | } 185 | }, 186 | "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { 187 | "version": "0.3.1", 188 | "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", 189 | "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", 190 | "dev": true, 191 | "license": "Apache-2.0", 192 | "engines": { 193 | "node": ">=18.18" 194 | }, 195 | "funding": { 196 | "type": "github", 197 | "url": "https://github.com/sponsors/nzakas" 198 | } 199 | }, 200 | "node_modules/@humanwhocodes/module-importer": { 201 | "version": "1.0.1", 202 | "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", 203 | "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", 204 | "dev": true, 205 | "license": "Apache-2.0", 206 | "engines": { 207 | "node": ">=12.22" 208 | }, 209 | "funding": { 210 | "type": "github", 211 | "url": "https://github.com/sponsors/nzakas" 212 | } 213 | }, 214 | "node_modules/@humanwhocodes/retry": { 215 | "version": "0.4.3", 216 | "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", 217 | "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", 218 | "dev": true, 219 | "license": "Apache-2.0", 220 | "engines": { 221 | "node": ">=18.18" 222 | }, 223 | "funding": { 224 | "type": "github", 225 | "url": "https://github.com/sponsors/nzakas" 226 | } 227 | }, 228 | "node_modules/@javascript-obfuscator/estraverse": { 229 | "version": "5.4.0", 230 | "resolved": "https://registry.npmjs.org/@javascript-obfuscator/estraverse/-/estraverse-5.4.0.tgz", 231 | "integrity": "sha512-CZFX7UZVN9VopGbjTx4UXaXsi9ewoM1buL0kY7j1ftYdSs7p2spv9opxFjHlQ/QGTgh4UqufYqJJ0WKLml7b6w==", 232 | "license": "BSD-2-Clause", 233 | "engines": { 234 | "node": ">=4.0" 235 | } 236 | }, 237 | "node_modules/@types/estree": { 238 | "version": "1.0.7", 239 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", 240 | "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", 241 | "dev": true, 242 | "license": "MIT" 243 | }, 244 | "node_modules/@types/json-schema": { 245 | "version": "7.0.15", 246 | "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", 247 | "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", 248 | "dev": true, 249 | "license": "MIT" 250 | }, 251 | "node_modules/acorn": { 252 | "version": "8.14.1", 253 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", 254 | "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", 255 | "license": "MIT", 256 | "bin": { 257 | "acorn": "bin/acorn" 258 | }, 259 | "engines": { 260 | "node": ">=0.4.0" 261 | } 262 | }, 263 | "node_modules/acorn-jsx": { 264 | "version": "5.3.2", 265 | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", 266 | "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", 267 | "license": "MIT", 268 | "peerDependencies": { 269 | "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" 270 | } 271 | }, 272 | "node_modules/ajv": { 273 | "version": "6.12.6", 274 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 275 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 276 | "dev": true, 277 | "license": "MIT", 278 | "dependencies": { 279 | "fast-deep-equal": "^3.1.1", 280 | "fast-json-stable-stringify": "^2.0.0", 281 | "json-schema-traverse": "^0.4.1", 282 | "uri-js": "^4.2.2" 283 | }, 284 | "funding": { 285 | "type": "github", 286 | "url": "https://github.com/sponsors/epoberezkin" 287 | } 288 | }, 289 | "node_modules/ansi-styles": { 290 | "version": "4.3.0", 291 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 292 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 293 | "dev": true, 294 | "license": "MIT", 295 | "dependencies": { 296 | "color-convert": "^2.0.1" 297 | }, 298 | "engines": { 299 | "node": ">=8" 300 | }, 301 | "funding": { 302 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 303 | } 304 | }, 305 | "node_modules/argparse": { 306 | "version": "2.0.1", 307 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 308 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 309 | "dev": true, 310 | "license": "Python-2.0" 311 | }, 312 | "node_modules/balanced-match": { 313 | "version": "1.0.2", 314 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 315 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 316 | "dev": true, 317 | "license": "MIT" 318 | }, 319 | "node_modules/brace-expansion": { 320 | "version": "1.1.11", 321 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 322 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 323 | "dev": true, 324 | "license": "MIT", 325 | "dependencies": { 326 | "balanced-match": "^1.0.0", 327 | "concat-map": "0.0.1" 328 | } 329 | }, 330 | "node_modules/callsites": { 331 | "version": "3.1.0", 332 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", 333 | "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", 334 | "dev": true, 335 | "license": "MIT", 336 | "engines": { 337 | "node": ">=6" 338 | } 339 | }, 340 | "node_modules/chalk": { 341 | "version": "4.1.2", 342 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 343 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 344 | "dev": true, 345 | "license": "MIT", 346 | "dependencies": { 347 | "ansi-styles": "^4.1.0", 348 | "supports-color": "^7.1.0" 349 | }, 350 | "engines": { 351 | "node": ">=10" 352 | }, 353 | "funding": { 354 | "url": "https://github.com/chalk/chalk?sponsor=1" 355 | } 356 | }, 357 | "node_modules/color-convert": { 358 | "version": "2.0.1", 359 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 360 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 361 | "dev": true, 362 | "license": "MIT", 363 | "dependencies": { 364 | "color-name": "~1.1.4" 365 | }, 366 | "engines": { 367 | "node": ">=7.0.0" 368 | } 369 | }, 370 | "node_modules/color-name": { 371 | "version": "1.1.4", 372 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 373 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 374 | "dev": true, 375 | "license": "MIT" 376 | }, 377 | "node_modules/concat-map": { 378 | "version": "0.0.1", 379 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 380 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", 381 | "dev": true, 382 | "license": "MIT" 383 | }, 384 | "node_modules/cross-spawn": { 385 | "version": "7.0.6", 386 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 387 | "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 388 | "dev": true, 389 | "license": "MIT", 390 | "dependencies": { 391 | "path-key": "^3.1.0", 392 | "shebang-command": "^2.0.0", 393 | "which": "^2.0.1" 394 | }, 395 | "engines": { 396 | "node": ">= 8" 397 | } 398 | }, 399 | "node_modules/debug": { 400 | "version": "4.4.1", 401 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", 402 | "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", 403 | "dev": true, 404 | "license": "MIT", 405 | "dependencies": { 406 | "ms": "^2.1.3" 407 | }, 408 | "engines": { 409 | "node": ">=6.0" 410 | }, 411 | "peerDependenciesMeta": { 412 | "supports-color": { 413 | "optional": true 414 | } 415 | } 416 | }, 417 | "node_modules/deep-is": { 418 | "version": "0.1.4", 419 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", 420 | "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", 421 | "license": "MIT" 422 | }, 423 | "node_modules/escape-string-regexp": { 424 | "version": "4.0.0", 425 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 426 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 427 | "dev": true, 428 | "license": "MIT", 429 | "engines": { 430 | "node": ">=10" 431 | }, 432 | "funding": { 433 | "url": "https://github.com/sponsors/sindresorhus" 434 | } 435 | }, 436 | "node_modules/escodegen": { 437 | "name": "@javascript-obfuscator/escodegen", 438 | "version": "2.3.0", 439 | "resolved": "https://registry.npmjs.org/@javascript-obfuscator/escodegen/-/escodegen-2.3.0.tgz", 440 | "integrity": "sha512-QVXwMIKqYMl3KwtTirYIA6gOCiJ0ZDtptXqAv/8KWLG9uQU2fZqTVy7a/A5RvcoZhbDoFfveTxuGxJ5ibzQtkw==", 441 | "license": "BSD-2-Clause", 442 | "dependencies": { 443 | "@javascript-obfuscator/estraverse": "^5.3.0", 444 | "esprima": "^4.0.1", 445 | "esutils": "^2.0.2", 446 | "optionator": "^0.8.1" 447 | }, 448 | "engines": { 449 | "node": ">=6.0" 450 | }, 451 | "optionalDependencies": { 452 | "source-map": "~0.6.1" 453 | } 454 | }, 455 | "node_modules/eslint": { 456 | "version": "9.27.0", 457 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz", 458 | "integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==", 459 | "dev": true, 460 | "license": "MIT", 461 | "dependencies": { 462 | "@eslint-community/eslint-utils": "^4.2.0", 463 | "@eslint-community/regexpp": "^4.12.1", 464 | "@eslint/config-array": "^0.20.0", 465 | "@eslint/config-helpers": "^0.2.1", 466 | "@eslint/core": "^0.14.0", 467 | "@eslint/eslintrc": "^3.3.1", 468 | "@eslint/js": "9.27.0", 469 | "@eslint/plugin-kit": "^0.3.1", 470 | "@humanfs/node": "^0.16.6", 471 | "@humanwhocodes/module-importer": "^1.0.1", 472 | "@humanwhocodes/retry": "^0.4.2", 473 | "@types/estree": "^1.0.6", 474 | "@types/json-schema": "^7.0.15", 475 | "ajv": "^6.12.4", 476 | "chalk": "^4.0.0", 477 | "cross-spawn": "^7.0.6", 478 | "debug": "^4.3.2", 479 | "escape-string-regexp": "^4.0.0", 480 | "eslint-scope": "^8.3.0", 481 | "eslint-visitor-keys": "^4.2.0", 482 | "espree": "^10.3.0", 483 | "esquery": "^1.5.0", 484 | "esutils": "^2.0.2", 485 | "fast-deep-equal": "^3.1.3", 486 | "file-entry-cache": "^8.0.0", 487 | "find-up": "^5.0.0", 488 | "glob-parent": "^6.0.2", 489 | "ignore": "^5.2.0", 490 | "imurmurhash": "^0.1.4", 491 | "is-glob": "^4.0.0", 492 | "json-stable-stringify-without-jsonify": "^1.0.1", 493 | "lodash.merge": "^4.6.2", 494 | "minimatch": "^3.1.2", 495 | "natural-compare": "^1.4.0", 496 | "optionator": "^0.9.3" 497 | }, 498 | "bin": { 499 | "eslint": "bin/eslint.js" 500 | }, 501 | "engines": { 502 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 503 | }, 504 | "funding": { 505 | "url": "https://eslint.org/donate" 506 | }, 507 | "peerDependencies": { 508 | "jiti": "*" 509 | }, 510 | "peerDependenciesMeta": { 511 | "jiti": { 512 | "optional": true 513 | } 514 | } 515 | }, 516 | "node_modules/eslint-scope": { 517 | "version": "8.3.0", 518 | "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", 519 | "integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", 520 | "license": "BSD-2-Clause", 521 | "dependencies": { 522 | "esrecurse": "^4.3.0", 523 | "estraverse": "^5.2.0" 524 | }, 525 | "engines": { 526 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 527 | }, 528 | "funding": { 529 | "url": "https://opencollective.com/eslint" 530 | } 531 | }, 532 | "node_modules/eslint-visitor-keys": { 533 | "version": "4.2.0", 534 | "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", 535 | "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", 536 | "license": "Apache-2.0", 537 | "engines": { 538 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 539 | }, 540 | "funding": { 541 | "url": "https://opencollective.com/eslint" 542 | } 543 | }, 544 | "node_modules/eslint/node_modules/optionator": { 545 | "version": "0.9.4", 546 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", 547 | "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", 548 | "dev": true, 549 | "license": "MIT", 550 | "dependencies": { 551 | "deep-is": "^0.1.3", 552 | "fast-levenshtein": "^2.0.6", 553 | "levn": "^0.4.1", 554 | "prelude-ls": "^1.2.1", 555 | "type-check": "^0.4.0", 556 | "word-wrap": "^1.2.5" 557 | }, 558 | "engines": { 559 | "node": ">= 0.8.0" 560 | } 561 | }, 562 | "node_modules/espree": { 563 | "version": "10.3.0", 564 | "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", 565 | "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", 566 | "license": "BSD-2-Clause", 567 | "dependencies": { 568 | "acorn": "^8.14.0", 569 | "acorn-jsx": "^5.3.2", 570 | "eslint-visitor-keys": "^4.2.0" 571 | }, 572 | "engines": { 573 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 574 | }, 575 | "funding": { 576 | "url": "https://opencollective.com/eslint" 577 | } 578 | }, 579 | "node_modules/esprima": { 580 | "version": "4.0.1", 581 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", 582 | "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", 583 | "license": "BSD-2-Clause", 584 | "bin": { 585 | "esparse": "bin/esparse.js", 586 | "esvalidate": "bin/esvalidate.js" 587 | }, 588 | "engines": { 589 | "node": ">=4" 590 | } 591 | }, 592 | "node_modules/esquery": { 593 | "version": "1.6.0", 594 | "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", 595 | "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", 596 | "dev": true, 597 | "license": "BSD-3-Clause", 598 | "dependencies": { 599 | "estraverse": "^5.1.0" 600 | }, 601 | "engines": { 602 | "node": ">=0.10" 603 | } 604 | }, 605 | "node_modules/esrecurse": { 606 | "version": "4.3.0", 607 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", 608 | "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", 609 | "license": "BSD-2-Clause", 610 | "dependencies": { 611 | "estraverse": "^5.2.0" 612 | }, 613 | "engines": { 614 | "node": ">=4.0" 615 | } 616 | }, 617 | "node_modules/estraverse": { 618 | "version": "5.3.0", 619 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", 620 | "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", 621 | "license": "BSD-2-Clause", 622 | "engines": { 623 | "node": ">=4.0" 624 | } 625 | }, 626 | "node_modules/esutils": { 627 | "version": "2.0.3", 628 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", 629 | "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", 630 | "license": "BSD-2-Clause", 631 | "engines": { 632 | "node": ">=0.10.0" 633 | } 634 | }, 635 | "node_modules/fast-deep-equal": { 636 | "version": "3.1.3", 637 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 638 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", 639 | "dev": true, 640 | "license": "MIT" 641 | }, 642 | "node_modules/fast-json-stable-stringify": { 643 | "version": "2.1.0", 644 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 645 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", 646 | "dev": true, 647 | "license": "MIT" 648 | }, 649 | "node_modules/fast-levenshtein": { 650 | "version": "2.0.6", 651 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 652 | "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", 653 | "license": "MIT" 654 | }, 655 | "node_modules/file-entry-cache": { 656 | "version": "8.0.0", 657 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", 658 | "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", 659 | "dev": true, 660 | "license": "MIT", 661 | "dependencies": { 662 | "flat-cache": "^4.0.0" 663 | }, 664 | "engines": { 665 | "node": ">=16.0.0" 666 | } 667 | }, 668 | "node_modules/find-up": { 669 | "version": "5.0.0", 670 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 671 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 672 | "dev": true, 673 | "license": "MIT", 674 | "dependencies": { 675 | "locate-path": "^6.0.0", 676 | "path-exists": "^4.0.0" 677 | }, 678 | "engines": { 679 | "node": ">=10" 680 | }, 681 | "funding": { 682 | "url": "https://github.com/sponsors/sindresorhus" 683 | } 684 | }, 685 | "node_modules/flat-cache": { 686 | "version": "4.0.1", 687 | "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", 688 | "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", 689 | "dev": true, 690 | "license": "MIT", 691 | "dependencies": { 692 | "flatted": "^3.2.9", 693 | "keyv": "^4.5.4" 694 | }, 695 | "engines": { 696 | "node": ">=16" 697 | } 698 | }, 699 | "node_modules/flatted": { 700 | "version": "3.3.3", 701 | "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", 702 | "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", 703 | "dev": true, 704 | "license": "ISC" 705 | }, 706 | "node_modules/glob-parent": { 707 | "version": "6.0.2", 708 | "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", 709 | "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", 710 | "dev": true, 711 | "license": "ISC", 712 | "dependencies": { 713 | "is-glob": "^4.0.3" 714 | }, 715 | "engines": { 716 | "node": ">=10.13.0" 717 | } 718 | }, 719 | "node_modules/globals": { 720 | "version": "14.0.0", 721 | "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", 722 | "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", 723 | "dev": true, 724 | "license": "MIT", 725 | "engines": { 726 | "node": ">=18" 727 | }, 728 | "funding": { 729 | "url": "https://github.com/sponsors/sindresorhus" 730 | } 731 | }, 732 | "node_modules/has-flag": { 733 | "version": "4.0.0", 734 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 735 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 736 | "dev": true, 737 | "license": "MIT", 738 | "engines": { 739 | "node": ">=8" 740 | } 741 | }, 742 | "node_modules/husky": { 743 | "version": "9.1.7", 744 | "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", 745 | "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", 746 | "dev": true, 747 | "license": "MIT", 748 | "bin": { 749 | "husky": "bin.js" 750 | }, 751 | "engines": { 752 | "node": ">=18" 753 | }, 754 | "funding": { 755 | "url": "https://github.com/sponsors/typicode" 756 | } 757 | }, 758 | "node_modules/ignore": { 759 | "version": "5.3.2", 760 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", 761 | "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", 762 | "dev": true, 763 | "license": "MIT", 764 | "engines": { 765 | "node": ">= 4" 766 | } 767 | }, 768 | "node_modules/import-fresh": { 769 | "version": "3.3.1", 770 | "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", 771 | "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", 772 | "dev": true, 773 | "license": "MIT", 774 | "dependencies": { 775 | "parent-module": "^1.0.0", 776 | "resolve-from": "^4.0.0" 777 | }, 778 | "engines": { 779 | "node": ">=6" 780 | }, 781 | "funding": { 782 | "url": "https://github.com/sponsors/sindresorhus" 783 | } 784 | }, 785 | "node_modules/imurmurhash": { 786 | "version": "0.1.4", 787 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 788 | "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", 789 | "dev": true, 790 | "license": "MIT", 791 | "engines": { 792 | "node": ">=0.8.19" 793 | } 794 | }, 795 | "node_modules/is-extglob": { 796 | "version": "2.1.1", 797 | "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", 798 | "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", 799 | "dev": true, 800 | "license": "MIT", 801 | "engines": { 802 | "node": ">=0.10.0" 803 | } 804 | }, 805 | "node_modules/is-glob": { 806 | "version": "4.0.3", 807 | "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", 808 | "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", 809 | "dev": true, 810 | "license": "MIT", 811 | "dependencies": { 812 | "is-extglob": "^2.1.1" 813 | }, 814 | "engines": { 815 | "node": ">=0.10.0" 816 | } 817 | }, 818 | "node_modules/isexe": { 819 | "version": "2.0.0", 820 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 821 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 822 | "dev": true, 823 | "license": "ISC" 824 | }, 825 | "node_modules/js-yaml": { 826 | "version": "4.1.0", 827 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 828 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 829 | "dev": true, 830 | "license": "MIT", 831 | "dependencies": { 832 | "argparse": "^2.0.1" 833 | }, 834 | "bin": { 835 | "js-yaml": "bin/js-yaml.js" 836 | } 837 | }, 838 | "node_modules/json-buffer": { 839 | "version": "3.0.1", 840 | "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", 841 | "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", 842 | "dev": true, 843 | "license": "MIT" 844 | }, 845 | "node_modules/json-schema-traverse": { 846 | "version": "0.4.1", 847 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 848 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", 849 | "dev": true, 850 | "license": "MIT" 851 | }, 852 | "node_modules/json-stable-stringify-without-jsonify": { 853 | "version": "1.0.1", 854 | "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", 855 | "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", 856 | "dev": true, 857 | "license": "MIT" 858 | }, 859 | "node_modules/keyv": { 860 | "version": "4.5.4", 861 | "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", 862 | "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", 863 | "dev": true, 864 | "license": "MIT", 865 | "dependencies": { 866 | "json-buffer": "3.0.1" 867 | } 868 | }, 869 | "node_modules/levn": { 870 | "version": "0.4.1", 871 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", 872 | "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", 873 | "dev": true, 874 | "license": "MIT", 875 | "dependencies": { 876 | "prelude-ls": "^1.2.1", 877 | "type-check": "~0.4.0" 878 | }, 879 | "engines": { 880 | "node": ">= 0.8.0" 881 | } 882 | }, 883 | "node_modules/locate-path": { 884 | "version": "6.0.0", 885 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 886 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 887 | "dev": true, 888 | "license": "MIT", 889 | "dependencies": { 890 | "p-locate": "^5.0.0" 891 | }, 892 | "engines": { 893 | "node": ">=10" 894 | }, 895 | "funding": { 896 | "url": "https://github.com/sponsors/sindresorhus" 897 | } 898 | }, 899 | "node_modules/lodash.merge": { 900 | "version": "4.6.2", 901 | "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", 902 | "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", 903 | "dev": true, 904 | "license": "MIT" 905 | }, 906 | "node_modules/minimatch": { 907 | "version": "3.1.2", 908 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 909 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 910 | "dev": true, 911 | "license": "ISC", 912 | "dependencies": { 913 | "brace-expansion": "^1.1.7" 914 | }, 915 | "engines": { 916 | "node": "*" 917 | } 918 | }, 919 | "node_modules/ms": { 920 | "version": "2.1.3", 921 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 922 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 923 | "dev": true, 924 | "license": "MIT" 925 | }, 926 | "node_modules/natural-compare": { 927 | "version": "1.4.0", 928 | "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 929 | "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", 930 | "dev": true, 931 | "license": "MIT" 932 | }, 933 | "node_modules/optionator": { 934 | "version": "0.8.3", 935 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", 936 | "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", 937 | "license": "MIT", 938 | "dependencies": { 939 | "deep-is": "~0.1.3", 940 | "fast-levenshtein": "~2.0.6", 941 | "levn": "~0.3.0", 942 | "prelude-ls": "~1.1.2", 943 | "type-check": "~0.3.2", 944 | "word-wrap": "~1.2.3" 945 | }, 946 | "engines": { 947 | "node": ">= 0.8.0" 948 | } 949 | }, 950 | "node_modules/optionator/node_modules/levn": { 951 | "version": "0.3.0", 952 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", 953 | "integrity": "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA==", 954 | "license": "MIT", 955 | "dependencies": { 956 | "prelude-ls": "~1.1.2", 957 | "type-check": "~0.3.2" 958 | }, 959 | "engines": { 960 | "node": ">= 0.8.0" 961 | } 962 | }, 963 | "node_modules/optionator/node_modules/prelude-ls": { 964 | "version": "1.1.2", 965 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", 966 | "integrity": "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w==", 967 | "engines": { 968 | "node": ">= 0.8.0" 969 | } 970 | }, 971 | "node_modules/optionator/node_modules/type-check": { 972 | "version": "0.3.2", 973 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", 974 | "integrity": "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg==", 975 | "license": "MIT", 976 | "dependencies": { 977 | "prelude-ls": "~1.1.2" 978 | }, 979 | "engines": { 980 | "node": ">= 0.8.0" 981 | } 982 | }, 983 | "node_modules/p-limit": { 984 | "version": "3.1.0", 985 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 986 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 987 | "dev": true, 988 | "license": "MIT", 989 | "dependencies": { 990 | "yocto-queue": "^0.1.0" 991 | }, 992 | "engines": { 993 | "node": ">=10" 994 | }, 995 | "funding": { 996 | "url": "https://github.com/sponsors/sindresorhus" 997 | } 998 | }, 999 | "node_modules/p-locate": { 1000 | "version": "5.0.0", 1001 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 1002 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 1003 | "dev": true, 1004 | "license": "MIT", 1005 | "dependencies": { 1006 | "p-limit": "^3.0.2" 1007 | }, 1008 | "engines": { 1009 | "node": ">=10" 1010 | }, 1011 | "funding": { 1012 | "url": "https://github.com/sponsors/sindresorhus" 1013 | } 1014 | }, 1015 | "node_modules/parent-module": { 1016 | "version": "1.0.1", 1017 | "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", 1018 | "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", 1019 | "dev": true, 1020 | "license": "MIT", 1021 | "dependencies": { 1022 | "callsites": "^3.0.0" 1023 | }, 1024 | "engines": { 1025 | "node": ">=6" 1026 | } 1027 | }, 1028 | "node_modules/path-exists": { 1029 | "version": "4.0.0", 1030 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 1031 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 1032 | "dev": true, 1033 | "license": "MIT", 1034 | "engines": { 1035 | "node": ">=8" 1036 | } 1037 | }, 1038 | "node_modules/path-key": { 1039 | "version": "3.1.1", 1040 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 1041 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 1042 | "dev": true, 1043 | "license": "MIT", 1044 | "engines": { 1045 | "node": ">=8" 1046 | } 1047 | }, 1048 | "node_modules/prelude-ls": { 1049 | "version": "1.2.1", 1050 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", 1051 | "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", 1052 | "dev": true, 1053 | "license": "MIT", 1054 | "engines": { 1055 | "node": ">= 0.8.0" 1056 | } 1057 | }, 1058 | "node_modules/punycode": { 1059 | "version": "2.3.1", 1060 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 1061 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 1062 | "dev": true, 1063 | "license": "MIT", 1064 | "engines": { 1065 | "node": ">=6" 1066 | } 1067 | }, 1068 | "node_modules/resolve-from": { 1069 | "version": "4.0.0", 1070 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", 1071 | "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", 1072 | "dev": true, 1073 | "license": "MIT", 1074 | "engines": { 1075 | "node": ">=4" 1076 | } 1077 | }, 1078 | "node_modules/shebang-command": { 1079 | "version": "2.0.0", 1080 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 1081 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 1082 | "dev": true, 1083 | "license": "MIT", 1084 | "dependencies": { 1085 | "shebang-regex": "^3.0.0" 1086 | }, 1087 | "engines": { 1088 | "node": ">=8" 1089 | } 1090 | }, 1091 | "node_modules/shebang-regex": { 1092 | "version": "3.0.0", 1093 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 1094 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 1095 | "dev": true, 1096 | "license": "MIT", 1097 | "engines": { 1098 | "node": ">=8" 1099 | } 1100 | }, 1101 | "node_modules/source-map": { 1102 | "version": "0.6.1", 1103 | "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", 1104 | "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", 1105 | "license": "BSD-3-Clause", 1106 | "optional": true, 1107 | "engines": { 1108 | "node": ">=0.10.0" 1109 | } 1110 | }, 1111 | "node_modules/strip-json-comments": { 1112 | "version": "3.1.1", 1113 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 1114 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 1115 | "dev": true, 1116 | "license": "MIT", 1117 | "engines": { 1118 | "node": ">=8" 1119 | }, 1120 | "funding": { 1121 | "url": "https://github.com/sponsors/sindresorhus" 1122 | } 1123 | }, 1124 | "node_modules/supports-color": { 1125 | "version": "7.2.0", 1126 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1127 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1128 | "dev": true, 1129 | "license": "MIT", 1130 | "dependencies": { 1131 | "has-flag": "^4.0.0" 1132 | }, 1133 | "engines": { 1134 | "node": ">=8" 1135 | } 1136 | }, 1137 | "node_modules/type-check": { 1138 | "version": "0.4.0", 1139 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", 1140 | "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", 1141 | "dev": true, 1142 | "license": "MIT", 1143 | "dependencies": { 1144 | "prelude-ls": "^1.2.1" 1145 | }, 1146 | "engines": { 1147 | "node": ">= 0.8.0" 1148 | } 1149 | }, 1150 | "node_modules/uri-js": { 1151 | "version": "4.4.1", 1152 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 1153 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 1154 | "dev": true, 1155 | "license": "BSD-2-Clause", 1156 | "dependencies": { 1157 | "punycode": "^2.1.0" 1158 | } 1159 | }, 1160 | "node_modules/which": { 1161 | "version": "2.0.2", 1162 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1163 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1164 | "dev": true, 1165 | "license": "ISC", 1166 | "dependencies": { 1167 | "isexe": "^2.0.0" 1168 | }, 1169 | "bin": { 1170 | "node-which": "bin/node-which" 1171 | }, 1172 | "engines": { 1173 | "node": ">= 8" 1174 | } 1175 | }, 1176 | "node_modules/word-wrap": { 1177 | "version": "1.2.5", 1178 | "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", 1179 | "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", 1180 | "license": "MIT", 1181 | "engines": { 1182 | "node": ">=0.10.0" 1183 | } 1184 | }, 1185 | "node_modules/yocto-queue": { 1186 | "version": "0.1.0", 1187 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1188 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1189 | "dev": true, 1190 | "license": "MIT", 1191 | "engines": { 1192 | "node": ">=10" 1193 | }, 1194 | "funding": { 1195 | "url": "https://github.com/sponsors/sindresorhus" 1196 | } 1197 | } 1198 | } 1199 | } 1200 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flast", 3 | "version": "2.2.5", 4 | "description": "Flatten JS AST", 5 | "main": "src/index.js", 6 | "type": "module", 7 | "scripts": { 8 | "lint": "eslint .", 9 | "prepare": "husky", 10 | "test": "node --test", 11 | "test:coverage": "node --test --experimental-test-coverage" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/PerimeterX/flast.git" 16 | }, 17 | "keywords": [ 18 | "js", 19 | "javascript", 20 | "AST" 21 | ], 22 | "author": "Ben Baryo (ben.baryo@humansecurity.com)", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/PerimeterX/flast/issues" 26 | }, 27 | "homepage": "https://github.com/PerimeterX/flast#readme", 28 | "dependencies": { 29 | "escodegen": "npm:@javascript-obfuscator/escodegen", 30 | "eslint-scope": "^8.3.0", 31 | "espree": "^10.3.0" 32 | }, 33 | "devDependencies": { 34 | "eslint": "^9.26.0", 35 | "husky": "^9.1.7" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/arborist.js: -------------------------------------------------------------------------------- 1 | import {logger} from './utils/logger.js'; 2 | import {generateCode, generateFlatAST} from './flast.js'; 3 | 4 | /** 5 | * Arborist allows marking nodes for deletion or replacement, and then applying all changes in a single pass. 6 | * Note: Marking a node with markNode() only sets a flag; the AST is not officially changed until applyChanges() is called. 7 | */ 8 | class Arborist { 9 | /** 10 | * @param {string|ASTNode[]} scriptOrFlatAstArr - The target script or a flat AST array. 11 | */ 12 | constructor(scriptOrFlatAstArr) { 13 | this.script = ''; 14 | this.ast = []; 15 | this.markedForDeletion = []; // Array of node ids. 16 | this.appliedCounter = 0; // Track the number of times changes were applied. 17 | this.replacements = []; 18 | this.logger = logger; 19 | if (typeof scriptOrFlatAstArr === 'string') { 20 | this.script = scriptOrFlatAstArr; 21 | this.ast = generateFlatAST(scriptOrFlatAstArr); 22 | } else if (Array.isArray(scriptOrFlatAstArr)) { 23 | this.ast = scriptOrFlatAstArr; 24 | } else throw Error(`Undetermined argument`); 25 | } 26 | 27 | /** 28 | * When applicable, replace the provided node with its nearest parent node that can be removed without breaking the code. 29 | * @param {ASTNode} startNode 30 | * @return {ASTNode} 31 | */ 32 | _getCorrectTargetForDeletion(startNode) { 33 | const relevantTypes = ['ExpressionStatement', 'UnaryExpression', 'UpdateExpression']; 34 | const relevantClauses = ['consequent', 'alternate']; 35 | let currentNode = startNode; 36 | while (relevantTypes.includes(currentNode?.parentNode?.type) || 37 | (currentNode.parentNode.type === 'VariableDeclaration' && 38 | (currentNode.parentNode.declarations.length === 1 || 39 | !currentNode.parentNode.declarations.some(d => d !== currentNode && !d.isMarked)) 40 | )) currentNode = currentNode.parentNode; 41 | if (relevantClauses.includes(currentNode.parentKey)) currentNode.isEmpty = true; 42 | return currentNode; 43 | } 44 | 45 | /** 46 | * @returns {number} The number of changes to be applied. 47 | */ 48 | getNumberOfChanges() { 49 | return this.replacements.length + this.markedForDeletion.length; 50 | } 51 | 52 | /** 53 | * Mark a node for replacement or deletion. This only sets a flag; the AST is not changed until applyChanges() is called. 54 | * @param {ASTNode} targetNode The node to replace or remove. 55 | * @param {object|ASTNode} [replacementNode] If exists, replace the target node with this node. 56 | */ 57 | markNode(targetNode, replacementNode) { 58 | if (!targetNode.isMarked) { 59 | if (replacementNode) { // Mark for replacement 60 | this.replacements.push([targetNode, replacementNode]); 61 | targetNode.isMarked = true; 62 | } else { // Mark for deletion 63 | targetNode = this._getCorrectTargetForDeletion(targetNode); 64 | if (targetNode.isEmpty) this.markNode(targetNode, {type: 'EmptyStatement'}); 65 | else if (!targetNode.isMarked) { 66 | this.markedForDeletion.push(targetNode.nodeId); 67 | targetNode.isMarked = true; 68 | } 69 | } 70 | } 71 | } 72 | 73 | /** 74 | * Merge comments from a source node into a target node or array. 75 | * @param {ASTNode|Object} target - The node or array element to receive comments. 76 | * @param {ASTNode} source - The node whose comments should be merged. 77 | * @param {'leadingComments'|'trailingComments'} which 78 | */ 79 | static mergeComments(target, source, which) { 80 | if (!source[which] || !source[which].length) return; 81 | if (!target[which]) { 82 | target[which] = [...source[which]]; 83 | } else if (target[which] !== source[which]) { 84 | target[which] = target[which].concat(source[which]); 85 | } 86 | } 87 | 88 | /** 89 | * Iterate over the complete AST and replace / remove marked nodes, 90 | * then rebuild code and AST to validate changes. 91 | * 92 | * Note: If you delete a node that is the only child of its parent (e.g., the only statement in a block), 93 | * you may leave the parent in an invalid or empty state. Consider cleaning up empty parents if needed. 94 | * 95 | * @return {number} The number of modifications made. 96 | */ 97 | applyChanges() { 98 | let changesCounter = 0; 99 | let rootNode = this.ast[0]; 100 | try { 101 | if (this.getNumberOfChanges() > 0) { 102 | if (rootNode.isMarked) { 103 | const rootNodeReplacement = this.replacements.find(n => n[0].nodeId === 0); 104 | ++changesCounter; 105 | this.logger.debug(`[+] Applying changes to the root node...`); 106 | const leadingComments = rootNode.leadingComments || []; 107 | const trailingComments = rootNode.trailingComments || []; 108 | rootNode = rootNodeReplacement[1]; 109 | if (leadingComments.length && rootNode.leadingComments !== leadingComments) 110 | Arborist.mergeComments(rootNode, {leadingComments}, 'leadingComments'); 111 | if (trailingComments.length && rootNode.trailingComments !== trailingComments) 112 | Arborist.mergeComments(rootNode, {trailingComments}, 'trailingComments'); 113 | } else { 114 | for (const targetNodeId of this.markedForDeletion) { 115 | try { 116 | let targetNode = this.ast[targetNodeId]; 117 | targetNode = targetNode.nodeId === targetNodeId ? targetNode : this.ast.find(n => n.nodeId === targetNodeId); 118 | if (targetNode) { 119 | const parent = targetNode.parentNode; 120 | if (parent[targetNode.parentKey] === targetNode) { 121 | delete parent[targetNode.parentKey]; 122 | Arborist.mergeComments(parent, targetNode, 'trailingComments'); 123 | ++changesCounter; 124 | } else if (Array.isArray(parent[targetNode.parentKey])) { 125 | const idx = parent[targetNode.parentKey].indexOf(targetNode); 126 | if (idx !== -1) { 127 | parent[targetNode.parentKey].splice(idx, 1); 128 | const comments = (targetNode.leadingComments || []).concat(targetNode.trailingComments || []); 129 | let targetParent = null; 130 | if (parent[targetNode.parentKey].length > 0) { 131 | if (idx > 0) { 132 | targetParent = parent[targetNode.parentKey][idx - 1]; 133 | Arborist.mergeComments(targetParent, {trailingComments: comments}, 'trailingComments'); 134 | } else { 135 | targetParent = parent[targetNode.parentKey][0]; 136 | Arborist.mergeComments(targetParent, {leadingComments: comments}, 'leadingComments'); 137 | } 138 | } else { 139 | this.logger.debug(`[!] Deleted last element from array '${targetNode.parentKey}' in parent node type '${parent.type}'. Array is now empty.`); 140 | Arborist.mergeComments(parent, {trailingComments: comments}, 'trailingComments'); 141 | } 142 | ++changesCounter; 143 | } 144 | } 145 | } 146 | } catch (e) { 147 | this.logger.debug(`[-] Unable to delete node: ${e}`); 148 | } 149 | } 150 | for (const [targetNode, replacementNode] of this.replacements) { 151 | try { 152 | if (targetNode) { 153 | const parent = targetNode.parentNode; 154 | if (parent[targetNode.parentKey] === targetNode) { 155 | parent[targetNode.parentKey] = replacementNode; 156 | Arborist.mergeComments(replacementNode, targetNode, 'leadingComments'); 157 | Arborist.mergeComments(replacementNode, targetNode, 'trailingComments'); 158 | ++changesCounter; 159 | } else if (Array.isArray(parent[targetNode.parentKey])) { 160 | const idx = parent[targetNode.parentKey].indexOf(targetNode); 161 | parent[targetNode.parentKey][idx] = replacementNode; 162 | const comments = (targetNode.leadingComments || []).concat(targetNode.trailingComments || []); 163 | if (idx > 0) { 164 | Arborist.mergeComments(parent[targetNode.parentKey][idx - 1], {trailingComments: comments}, 'trailingComments'); 165 | } else if (parent[targetNode.parentKey].length > 1) { 166 | Arborist.mergeComments(parent[targetNode.parentKey][idx + 1], {leadingComments: comments}, 'leadingComments'); 167 | } else { 168 | Arborist.mergeComments(parent, {trailingComments: comments}, 'trailingComments'); 169 | } 170 | ++changesCounter; 171 | } 172 | } 173 | } catch (e) { 174 | this.logger.debug(`[-] Unable to replace node: ${e}`); 175 | } 176 | } 177 | } 178 | } 179 | if (changesCounter) { 180 | this.replacements.length = 0; 181 | this.markedForDeletion.length = 0; 182 | // If any of the changes made will break the script the next line will fail and the 183 | // script will remain the same. If it doesn't break, the changes are valid and the script can be marked as modified. 184 | const script = generateCode(rootNode); 185 | const ast = generateFlatAST(script); 186 | if (ast && ast.length) { 187 | this.ast = ast; 188 | this.script = script; 189 | } 190 | else { 191 | this.logger.log(`[-] Modified script is invalid. Reverting ${changesCounter} changes...`); 192 | changesCounter = 0; 193 | } 194 | } 195 | } catch (e) { 196 | this.logger.log(`[-] Unable to apply changes to AST: ${e}`); 197 | } 198 | ++this.appliedCounter; 199 | return changesCounter; 200 | } 201 | } 202 | 203 | export { 204 | Arborist, 205 | }; -------------------------------------------------------------------------------- /src/flast.js: -------------------------------------------------------------------------------- 1 | import {parse} from 'espree'; 2 | import {analyze} from 'eslint-scope'; 3 | import {logger} from './utils/logger.js'; 4 | import {generate, attachComments} from 'escodegen'; 5 | 6 | const ecmaVersion = 'latest'; 7 | const currentYear = (new Date()).getFullYear(); 8 | const sourceType = 'module'; 9 | 10 | /** 11 | * @param {string} inputCode 12 | * @param {object} opts Additional options for espree 13 | * @return {ASTNode} The root of the AST 14 | */ 15 | function parseCode(inputCode, opts = {}) { 16 | const rootNode = parse(inputCode, {ecmaVersion, comment: true, range: true, ...opts}); 17 | if (rootNode.tokens) attachComments(rootNode, rootNode.comments, rootNode.tokens); 18 | return rootNode; 19 | } 20 | 21 | const excludedParentKeys = [ 22 | 'type', 'start', 'end', 'range', 'sourceType', 'comments', 'srcClosure', 'nodeId', 'leadingComments', 'trailingComments', 23 | 'childNodes', 'parentNode', 'parentKey', 'scope', 'typeMap', 'lineage', 'allScopes', 'tokens', 24 | ]; 25 | 26 | const generateFlatASTDefaultOptions = { 27 | // If false, do not include any scope details 28 | detailed: true, 29 | // If false, do not include node src 30 | includeSrc: true, 31 | // Retry to parse the code with sourceType: 'script' if 'module' failed with 'strict' error message 32 | alternateSourceTypeOnFailure: true, 33 | // Options for the espree parser 34 | parseOpts: { 35 | sourceType, 36 | comment: true, 37 | tokens: true, 38 | }, 39 | }; 40 | 41 | /** 42 | * Return a function which retrieves a node's source on demand 43 | * @param {string} src 44 | * @returns {function(number, number): string} 45 | */ 46 | function createSrcClosure(src) { 47 | return function(start, end) {return src.slice(start, end);}; 48 | } 49 | 50 | /** 51 | * @param {string} inputCode 52 | * @param {object} opts Optional changes to behavior. See generateFlatASTDefaultOptions for available options. 53 | * @return {ASTNode[]} An array of flattened AST 54 | */ 55 | function generateFlatAST(inputCode, opts = {}) { 56 | opts = {...generateFlatASTDefaultOptions, ...opts}; 57 | let tree = []; 58 | const rootNode = generateRootNode(inputCode, opts); 59 | if (rootNode) { 60 | tree = extractNodesFromRoot(rootNode, opts); 61 | } 62 | return tree; 63 | } 64 | 65 | const generateCodeDefaultOptions = { 66 | format: { 67 | indent: { 68 | style: ' ', 69 | adjustMultilineComment: true, 70 | }, 71 | quotes: 'auto', 72 | escapeless: true, 73 | compact: false, 74 | }, 75 | comment: true, 76 | }; 77 | 78 | /** 79 | * @param {ASTNode} rootNode 80 | * @param {object} opts Optional changes to behavior. See generateCodeDefaultOptions for available options. 81 | * All escodegen options are supported, including sourceMap, sourceMapWithCode, etc. 82 | * @return {string} Code generated from AST 83 | */ 84 | function generateCode(rootNode, opts = {}) { 85 | return generate(rootNode, { ...generateCodeDefaultOptions, ...opts }); 86 | } 87 | 88 | /** 89 | * @param {string} inputCode 90 | * @param {object} [opts] 91 | * @return {ASTNode} 92 | */ 93 | function generateRootNode(inputCode, opts = {}) { 94 | opts = {...generateFlatASTDefaultOptions, ...opts}; 95 | const parseOpts = opts.parseOpts || {}; 96 | let rootNode; 97 | try { 98 | rootNode = parseCode(inputCode, parseOpts); 99 | if (opts.includeSrc) rootNode.srcClosure = createSrcClosure(inputCode); 100 | } catch (e) { 101 | // If any parse error occurs and alternateSourceTypeOnFailure is set, try 'script' mode 102 | if (opts.alternateSourceTypeOnFailure) { 103 | try { 104 | rootNode = parseCode(inputCode, {...parseOpts, sourceType: 'script'}); 105 | if (opts.includeSrc) rootNode.srcClosure = createSrcClosure(inputCode); 106 | } catch (e2) { 107 | logger.debug('Failed to parse as module and script:', e, e2); 108 | } 109 | } else { 110 | logger.debug(e); 111 | } 112 | } 113 | return rootNode; 114 | } 115 | 116 | /** 117 | * @param rootNode 118 | * @param opts 119 | * @return {ASTNode[]} 120 | */ 121 | function extractNodesFromRoot(rootNode, opts) { 122 | opts = {...generateFlatASTDefaultOptions, ...opts}; 123 | let nodeId = 0; 124 | const typeMap = {}; 125 | const allNodes = []; 126 | const scopes = opts.detailed ? getAllScopes(rootNode) : {}; 127 | 128 | const stack = [rootNode]; 129 | while (stack.length) { 130 | const node = stack.shift(); 131 | if (node.nodeId) continue; 132 | node.childNodes = node.childNodes || []; 133 | const childrenLoc = {}; // Store the location of child nodes to sort them by order 134 | node.parentKey = node.parentKey || ''; // Make sure parentKey exists 135 | // Iterate over all keys of the node to find child nodes 136 | const keys = Object.keys(node); 137 | for (let i = 0; i < keys.length; i++) { 138 | const key = keys[i]; 139 | if (excludedParentKeys.includes(key)) continue; 140 | const content = node[key]; 141 | if (content && typeof content === 'object') { 142 | // Sort each child node by its start position 143 | // and set the parentNode and parentKey attributes 144 | if (Array.isArray(content)) { 145 | for (let j = 0; j < content.length; j++) { 146 | const childNode = content[j]; 147 | if (!childNode) continue; 148 | childNode.parentNode = node; 149 | childNode.parentKey = key; 150 | childrenLoc[childNode.start] = childNode; 151 | } 152 | } else { 153 | content.parentNode = node; 154 | content.parentKey = key; 155 | childrenLoc[content.start] = content; 156 | } 157 | } 158 | } 159 | // Add the child nodes to top of the stack and populate the node's childNodes array 160 | stack.unshift(...Object.values(childrenLoc)); 161 | node.childNodes.push(...Object.values(childrenLoc)); 162 | 163 | allNodes.push(node); 164 | node.nodeId = nodeId++; 165 | typeMap[node.type] = typeMap[node.type] || []; 166 | typeMap[node.type].push(node); 167 | if (opts.detailed) { 168 | node.scope = scopes[node.scopeId] || node.parentNode?.scope; 169 | node.lineage = [...node.parentNode?.lineage || []]; 170 | if (!node.lineage.includes(node.scope.scopeId)) { 171 | node.lineage.push(node.scope.scopeId); 172 | } 173 | } 174 | // Add a getter for the node's source code 175 | if (opts.includeSrc && !node.src) Object.defineProperty(node, 'src', { 176 | get() {return rootNode.srcClosure(node.start, node.end);}, 177 | }); 178 | } 179 | if (opts.detailed) { 180 | const identifiers = typeMap.Identifier || []; 181 | const scopeVarMaps = buildScopeVarMaps(scopes); 182 | for (let i = 0; i < identifiers.length; i++) { 183 | mapIdentifierRelations(identifiers[i], scopeVarMaps); 184 | } 185 | } 186 | if (allNodes?.length) { 187 | allNodes[0].typeMap = new Proxy(typeMap, { 188 | get(target, prop, receiver) { 189 | if (prop in target) { 190 | return Reflect.get(target, prop, receiver); 191 | } 192 | return []; // Return an empty array for any undefined type 193 | }, 194 | }); 195 | } 196 | return allNodes; 197 | } 198 | 199 | /** 200 | * Precompute a map of variable names to declarations for each scope for fast lookup. 201 | * @param {object} scopes 202 | * @return {Map} Map of scopeId to { [name]: variable } 203 | */ 204 | function buildScopeVarMaps(scopes) { 205 | const scopeVarMaps = {}; 206 | for (const scopeId in scopes) { 207 | const scope = scopes[scopeId]; 208 | const varMap = {}; 209 | for (let i = 0; i < scope.variables.length; i++) { 210 | const v = scope.variables[i]; 211 | varMap[v.name] = v; 212 | } 213 | scopeVarMaps[scopeId] = varMap; 214 | } 215 | return scopeVarMaps; 216 | } 217 | 218 | /** 219 | * @param {ASTNode} node 220 | * @param {object} scopeVarMaps 221 | */ 222 | function mapIdentifierRelations(node, scopeVarMaps) { 223 | // Track references and declarations 224 | // Prevent assigning declNode to member expression properties or object keys 225 | if (node.type === 'Identifier' && !(!node.parentNode.computed && ['property', 'key'].includes(node.parentKey))) { 226 | const scope = node.scope; 227 | const varMap = scope && scopeVarMaps ? scopeVarMaps[scope.scopeId] : undefined; 228 | const variable = varMap ? varMap[node.name] : undefined; 229 | if (node.parentKey === 'id' || variable?.identifiers?.includes(node)) { 230 | node.references = node.references || []; 231 | } else { 232 | // Find declaration by finding the closest declaration of the same name. 233 | let decls = []; 234 | if (variable) { 235 | decls = variable.identifiers || []; 236 | } else if (scope && (scope.references.length || scope.variableScope?.references.length)) { 237 | const references = [...(scope.references || []), ...(scope.variableScope?.references || [])]; 238 | for (let i = 0; i < references.length; i++) { 239 | if (references[i].identifier.name === node.name) { 240 | decls = references[i].resolved?.identifiers || []; 241 | break; 242 | } 243 | } 244 | } 245 | let declNode = decls[0]; 246 | if (decls.length > 1) { 247 | let commonAncestors = maxSharedLength(declNode.lineage, node.lineage); 248 | for (let i = 1; i < decls.length; i++) { 249 | const ca = maxSharedLength(decls[i].lineage, node.lineage); 250 | if (ca > commonAncestors) { 251 | commonAncestors = ca; 252 | declNode = decls[i]; 253 | } 254 | } 255 | } 256 | if (declNode) { 257 | declNode.references = declNode.references || []; 258 | declNode.references.push(node); 259 | node.declNode = declNode; 260 | } 261 | } 262 | } 263 | } 264 | 265 | /** 266 | * @param {number[]} targetArr 267 | * @param {number[]} containedArr 268 | * @return {number} Return the maximum length of shared numbers 269 | */ 270 | function maxSharedLength(targetArr, containedArr) { 271 | let count = 0; 272 | for (let i = 0; i < containedArr.length; i++) { 273 | if (targetArr[i] !== containedArr[i]) break; 274 | ++count; 275 | } 276 | return count; 277 | } 278 | 279 | /** 280 | * @param {ASTNode} rootNode 281 | * @return {{number: ASTScope}} 282 | */ 283 | function getAllScopes(rootNode) { 284 | // noinspection JSCheckFunctionSignatures 285 | const globalScope = analyze(rootNode, { 286 | optimistic: true, 287 | ecmaVersion: currentYear, 288 | sourceType}).acquireAll(rootNode)[0]; 289 | let scopeId = 0; 290 | const allScopes = {}; 291 | const stack = [globalScope]; 292 | while (stack.length) { 293 | const scope = stack.shift(); 294 | if (scope.type !== 'module' && !scope.type.includes('-name')) { 295 | scope.scopeId = scopeId++; 296 | scope.block.scopeId = scope.scopeId; 297 | allScopes[scope.scopeId] = allScopes[scope.scopeId] || scope; 298 | 299 | for (let i = 0; i < scope.variables.length; i++) { 300 | const v = scope.variables[i]; 301 | for (let j = 0; j < v.identifiers.length; j++) { 302 | v.identifiers[j].scope = scope; 303 | v.identifiers[j].references = []; 304 | } 305 | } 306 | } else if (scope.upper === globalScope && scope.variables?.length) { 307 | // A single global scope is enough, so if there are variables in a module scope, add them to the global scope 308 | for (let i = 0; i < scope.variables.length; i++) { 309 | const v = scope.variables[i]; 310 | if (!globalScope.variables.includes(v)) globalScope.variables.push(v); 311 | } 312 | } 313 | stack.unshift(...scope.childScopes); 314 | } 315 | return rootNode.allScopes = allScopes; 316 | } 317 | 318 | export { 319 | extractNodesFromRoot, 320 | generateCode, 321 | generateFlatAST, 322 | generateRootNode, 323 | mapIdentifierRelations, 324 | parseCode, 325 | }; 326 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export * from './flast.js'; 2 | export * from './arborist.js'; 3 | export * from './types.js'; 4 | export * from './utils/index.js'; -------------------------------------------------------------------------------- /src/types.js: -------------------------------------------------------------------------------- 1 | import {Scope} from 'eslint-scope'; 2 | 3 | /** 4 | * @typedef {Object.} ASTTypeMap - Map of node type to array of nodes. 5 | */ 6 | 7 | /** 8 | * @typedef {Object.} ASTAllScopes - Map of scopeId to ASTScope. 9 | */ 10 | 11 | /** 12 | * @typedef ASTNode 13 | * @property {ASTNode[]} childNodes - Array of child nodes. 14 | * @property {number} nodeId - Unique id in the AST. 15 | * @property {string} parentKey - The property name this node occupies in its parent. 16 | * @property {ASTNode|null} parentNode - Parent node, or null for the root. 17 | * @property {ASTTypeMap} typeMap - Only present on the root node. Map of node type to array of nodes. 18 | * @property {string} type - Node type. 19 | * 20 | * @property {ASTAllScopes} [allScopes] - Only present on the root node. Map of scopeId to ASTScope. 21 | * @property {ASTNode} [alternate] - Alternate branch (e.g., in IfStatement). 22 | * @property {ASTNode} [argument] - Argument node. 23 | * @property {ASTNode[]} [arguments] - Array of argument nodes. 24 | * @property {boolean} [async] - True if the function is async. 25 | * @property {ASTNode|ASTNode[]} [body] - Function or block body. 26 | * @property {ASTNode} [callee] - Callee node in a CallExpression. 27 | * @property {ASTNode[]} [cases] - Switch cases. 28 | * @property {Object[]} [comments] - Comments attached to the node. 29 | * @property {boolean} [computed] - True if property is computed. 30 | * @property {ASTNode} [consequent] - Consequent branch (e.g., in IfStatement). 31 | * @property {string} [cooked] - Cooked value for template literals. 32 | * @property {ASTNode} [declaration] - Declaration node. 33 | * @property {ASTNode[]} [declarations] - Array of declaration nodes. 34 | * @property {ASTNode} [declNode] - Only present on identifier nodes that are references (detailed: true). 35 | * @property {boolean} [delegate] - True if yield*. 36 | * @property {ASTNode} [discriminant] - Switch discriminant. 37 | * @property {ASTNode[]} [elements] - Array elements. 38 | * @property {number} [end] - End position in source. 39 | * @property {ASTNode} [exported] - Exported node. 40 | * @property {ASTNode|boolean} [expression] - Expression node or boolean. 41 | * @property {ASTNode[]} [expressions] - Array of expressions. 42 | * @property {string} [flags] - Regex flags. 43 | * @property {boolean} [generator] - True if function is a generator. 44 | * @property {ASTNode} [id] - Identifier node. 45 | * @property {ASTNode} [imported] - Imported node. 46 | * @property {ASTNode} [init] - Initializer node. 47 | * @property {boolean} [isEmpty] - True when the node is set for deletion but should be replaced with an Empty Statement instead. 48 | * @property {boolean} [isMarked] - True when the node has already been marked for replacement or deletion. 49 | * @property {ASTNode} [key] - Key node in properties. 50 | * @property {string} [kind] - Kind of declaration (e.g., 'const'). 51 | * @property {ASTNode} [label] - Label node. 52 | * @property {Object[]} [leadingComments] - Leading comments. 53 | * @property {number[]} [lineage] - Only present if detailed: true. Array of scopeIds representing the ancestry of this node's scope. 54 | * @property {ASTNode} [left] - Left side of assignment or binary expression. 55 | * @property {ASTNode} [local] - Local node. 56 | * @property {boolean} [method] - True if method. 57 | * @property {string} [name] - Name of identifier. 58 | * @property {string} [operator] - Operator string. 59 | * @property {ASTNode} [object] - Object node. 60 | * @property {string} [pattern] - Pattern string. 61 | * @property {ASTNode[]} [params] - Function parameters. 62 | * @property {boolean} [prefix] - True if prefix operator. 63 | * @property {ASTNode} [property] - Property node. 64 | * @property {ASTNode[]} [properties] - Array of property nodes. 65 | * @property {ASTNode[]} [quasis] - Template literal quasis. 66 | * @property {number[]} [range] - [start, end] positions in source. 67 | * @property {string} [raw] - Raw source string. 68 | * @property {ASTNode} [regex] - Regex node. 69 | * @property {ASTNode[]} [references] - Only present on identifier and declaration nodes (detailed: true). 70 | * @property {ASTNode} [right] - Right side of assignment or binary expression. 71 | * @property {ASTScope} [scope] - Only present if detailed: true. The lexical scope for this node. 72 | * @property {number} [scopeId] - For nodes which are also a scope's block. 73 | * @property {string} [scriptHash] - Used for caching/iteration in some utilities. 74 | * @property {boolean} [shorthand] - True if shorthand property. 75 | * @property {ASTNode} [source] - Source node. 76 | * @property {string} [sourceType] - Source type (e.g., 'module'). 77 | * @property {ASTNode[]} [specifiers] - Import/export specifiers. 78 | * @property {boolean} [static] - True if static property. 79 | * @property {number} [start] - Start position in source. 80 | * @property {ASTNode} [superClass] - Superclass node. 81 | * @property {boolean} [tail] - True if tail in template literal. 82 | * @property {ASTNode} [test] - Test node (e.g., in IfStatement). 83 | * @property {Object[]} [tokens] - Tokens array. 84 | * @property {Object[]} [trailingComments] - Trailing comments. 85 | * @property {ASTNode} [update] - Update node. 86 | * @property {ASTNode|string|number|boolean} [value] - Value of the node. 87 | * @property {string|function} [src] - The source code for the node, or a getter function if includeSrc is true. 88 | */ 89 | class ASTNode {} 90 | 91 | /** 92 | * @typedef ASTScope 93 | * @extends Scope 94 | * @property {ASTNode} block - The AST node that is the block for this scope. 95 | * @property {ASTScope[]} childScopes - Array of child scopes. 96 | * @property {number} scopeId - Unique id for this scope. 97 | * @property {string} type - Scope type. 98 | */ 99 | class ASTScope extends Scope {} 100 | 101 | export { 102 | ASTNode, 103 | ASTScope, 104 | }; -------------------------------------------------------------------------------- /src/utils/applyIteratively.js: -------------------------------------------------------------------------------- 1 | import {Arborist} from '../arborist.js'; 2 | import {logger} from './logger.js'; 3 | import {createHash} from 'node:crypto'; 4 | 5 | const generateHash = str => createHash('sha256').update(str).digest('hex'); 6 | 7 | 8 | /** 9 | * Apply functions to modify the script repeatedly until they are no long effective or the max number of iterations is reached. 10 | * @param {string} script The target script to run the functions on. 11 | * @param {function[]} funcs 12 | * @param {number?} maxIterations (optional) Stop the loop after this many iterations at most. 13 | * @return {string} The possibly modified script. 14 | */ 15 | function applyIteratively(script, funcs, maxIterations = 500) { 16 | let scriptSnapshot = ''; 17 | let currentIteration = 0; 18 | let changesCounter = 0; 19 | let iterationsCounter = 0; 20 | try { 21 | let scriptHash = generateHash(script); 22 | let arborist = new Arborist(script); 23 | while (arborist.ast?.length && scriptSnapshot !== script && currentIteration < maxIterations) { 24 | const iterationStartTime = Date.now(); 25 | scriptSnapshot = script; 26 | 27 | // Mark the root node with the script hash to distinguish cache of different scripts. 28 | arborist.ast[0].scriptHash = scriptHash; 29 | for (let i = 0; i < funcs.length; i++) { 30 | const func = funcs[i]; 31 | const funcStartTime = Date.now(); 32 | try { 33 | logger.debug(`\t[!] Running ${func.name}...`); 34 | arborist = func(arborist); 35 | if (!arborist.ast?.length) break; 36 | // If the hash doesn't exist it means the Arborist was replaced 37 | const numberOfNewChanges = arborist.getNumberOfChanges() + +!arborist.ast[0].scriptHash; 38 | if (numberOfNewChanges) { 39 | changesCounter += numberOfNewChanges; 40 | logger.log(`\t[+] ${func.name} applying ${numberOfNewChanges} new changes!`); 41 | arborist.applyChanges(); 42 | script = arborist.script; 43 | scriptHash = generateHash(script); 44 | arborist.ast[0].scriptHash = scriptHash; 45 | } 46 | } catch (e) { 47 | logger.error(`[-] Error in ${func.name} (iteration #${iterationsCounter}): ${e}\n${e.stack}`); 48 | } finally { 49 | logger.debug(`\t\t[!] Running ${func.name} completed in ` + 50 | `${((Date.now() - funcStartTime) / 1000).toFixed(3)} seconds`); 51 | } 52 | } 53 | ++currentIteration; 54 | ++iterationsCounter; 55 | logger.log(`[+] ==> Iteartion #${iterationsCounter} completed in ${(Date.now() - iterationStartTime) / 1000} seconds` + 56 | ` with ${changesCounter ? changesCounter : 'no'} changes (${arborist.ast?.length || '???'} nodes)`); 57 | changesCounter = 0; 58 | } 59 | if (changesCounter) script = arborist.script; 60 | } catch (e) { 61 | logger.error(`[-] Error on iteration #${iterationsCounter}: ${e}\n${e.stack}`); 62 | } 63 | return script; 64 | } 65 | 66 | export {applyIteratively}; -------------------------------------------------------------------------------- /src/utils/index.js: -------------------------------------------------------------------------------- 1 | export * from './applyIteratively.js'; 2 | export * from './logger.js'; 3 | export * from './treeModifier.js'; -------------------------------------------------------------------------------- /src/utils/logger.js: -------------------------------------------------------------------------------- 1 | const logLevels = { 2 | DEBUG: 1, 3 | LOG: 2, 4 | ERROR: 3, 5 | NONE: 9e10, 6 | }; 7 | 8 | /** 9 | * @param {number} logLevel 10 | * @returns {function(*): void|undefined} 11 | */ 12 | function createLoggerForLevel(logLevel) { 13 | if (!Object.values(logLevels).includes(logLevel)) throw new Error(`Unknown log level ${logLevel}.`); 14 | return msg => logLevel >= logger.currentLogLevel ? logger.logFunc(msg) : undefined; 15 | } 16 | 17 | const logger = { 18 | logLevels, 19 | logFunc: console.log, 20 | debug: createLoggerForLevel(logLevels.DEBUG), 21 | log: createLoggerForLevel(logLevels.LOG), 22 | error: createLoggerForLevel(logLevels.ERROR), 23 | currentLogLevel: logLevels.NONE, 24 | 25 | /** 26 | * Set the current log level 27 | * @param {number} newLogLevel 28 | */ 29 | setLogLevel(newLogLevel) { 30 | if (!Object.values(this.logLevels).includes(newLogLevel)) throw new Error(`Unknown log level ${newLogLevel}.`); 31 | this.currentLogLevel = newLogLevel; 32 | }, 33 | 34 | setLogLevelNone() {this.setLogLevel(this.logLevels.NONE);}, 35 | setLogLevelDebug() {this.setLogLevel(this.logLevels.DEBUG);}, 36 | setLogLevelLog() {this.setLogLevel(this.logLevels.LOG);}, 37 | setLogLevelError() {this.setLogLevel(this.logLevels.ERROR);}, 38 | 39 | setLogFunc(newLogfunc) { 40 | this.logFunc = newLogfunc; 41 | }, 42 | }; 43 | 44 | export {logger}; -------------------------------------------------------------------------------- /src/utils/treeModifier.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Boilerplate for filter functions that identify the desired structure and a modifier function that modifies the tree. 3 | * An optional name for the function can be provided for better logging. 4 | * @param {Function} filterFunc 5 | * @param {Function} modFunc 6 | * @param {string} [funcName] 7 | * @returns {function(Arborist): Arborist} 8 | */ 9 | function treeModifier(filterFunc, modFunc, funcName) { 10 | const func = function(arb) { 11 | for (let i = 0; i < arb.ast.length; i++) { 12 | const n = arb.ast[i]; 13 | if (filterFunc(n, arb)) { 14 | modFunc(n, arb); 15 | } 16 | } 17 | return arb; 18 | }; 19 | if (funcName) Object.defineProperty(func, 'name', {value: funcName}); 20 | return func; 21 | } 22 | 23 | export {treeModifier}; -------------------------------------------------------------------------------- /tests/arborist.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import {describe, it} from 'node:test'; 3 | import {Arborist, generateFlatAST} from '../src/index.js'; 4 | 5 | describe('Arborist tests', () => { 6 | it('Verify node replacement works as expected', () => { 7 | const code = `console.log('Hello' + ' ' + 'there!');`; 8 | const expectedOutput = `console.log('General' + ' ' + 'Kenobi');`; 9 | const replacements = { 10 | 'Hello': 'General', 11 | 'there!': 'Kenobi', 12 | }; 13 | const arborist = new Arborist(code); 14 | arborist.ast.filter(n => n.type === 'Literal' && replacements[n.value]) 15 | .forEach(n => arborist.markNode(n, { 16 | type: 'Literal', 17 | value: replacements[n.value], 18 | raw: `'${replacements[n.value]}'`, 19 | })); 20 | const numberOfChangesMade = arborist.applyChanges(); 21 | const result = arborist.script; 22 | 23 | assert.equal(result, expectedOutput, `Result does not match expected output.`); 24 | assert.equal(numberOfChangesMade, Object.keys(replacements).length, `The number of actual replacements does not match expectations.`); 25 | }); 26 | it('Verify the root node replacement works as expected', () => { 27 | const code = `a;`; 28 | const expectedOutput = `b`; 29 | const arborist = new Arborist(code); 30 | arborist.markNode(arborist.ast[0], { 31 | type: 'Identifier', 32 | name: 'b', 33 | }); 34 | arborist.applyChanges(); 35 | const result = arborist.script; 36 | 37 | assert.equal(result, expectedOutput, `Result does not match expected output.`); 38 | }); 39 | it('Verify only the root node is replaced', () => { 40 | const code = `a;b;`; 41 | const expectedOutput = `c`; 42 | const arborist = new Arborist(code); 43 | arborist.markNode(arborist.ast[4], { 44 | type: 'Identifier', 45 | name: 'v', 46 | }); 47 | arborist.markNode(arborist.ast[0], { 48 | type: 'Identifier', 49 | name: 'c', 50 | }); 51 | arborist.applyChanges(); 52 | const result = arborist.script; 53 | 54 | assert.equal(result, expectedOutput, `Result does not match expected output.`); 55 | }); 56 | it('Verify node deletion works as expected', () => { 57 | const code = `const a = ['There', 'can', 'be', 'only', 'one'];`; 58 | const expectedOutput = `const a = ['one'];`; 59 | const literalToSave = 'one'; 60 | const arborist = new Arborist(code); 61 | arborist.ast.filter(n => n.type === 'Literal' && n.value !== literalToSave).forEach(n => arborist.markNode(n)); 62 | const numberOfChangesMade = arborist.applyChanges(); 63 | const expectedNumberOfChanges = 4; 64 | const result = arborist.script; 65 | 66 | assert.equal(result, expectedOutput, `Result does not match expected output.`); 67 | assert.equal(numberOfChangesMade, expectedNumberOfChanges, `The number of actual changes does not match expectations.`); 68 | }); 69 | it('Verify the correct node is targeted for deletion', () => { 70 | const code = `var a = 1;`; 71 | const expectedResult = ``; 72 | const arborist = new Arborist(code); 73 | arborist.markNode(arborist.ast.find(n => n.type === 'VariableDeclarator')); 74 | arborist.applyChanges(); 75 | assert.equal(arborist.script, expectedResult, 'An incorrect node was targeted for deletion.'); 76 | }); 77 | it('Verify a valid script can be used to initialize an arborist instance', () => { 78 | const code = `console.log('test');`; 79 | let error = ''; 80 | let arborist; 81 | const expectedArraySize = generateFlatAST(code).length; 82 | try { 83 | arborist = new Arborist(code); 84 | } catch (e) { 85 | error = e.message; 86 | } 87 | assert.ok(arborist?.script, `Arborist failed to instantiate. ${error ? 'Error: ' + error : ''}`); 88 | assert.ok(!error, `Arborist instantiated with an error: ${error}`); 89 | assert.equal(arborist.script, code, `Arborist script did not match initialization argument.`); 90 | assert.equal(arborist.ast.length, expectedArraySize, `Arborist did not generate a flat AST array.`); 91 | }); 92 | it('Verify a valid AST array can be used to initialize an arborist instance', () => { 93 | const code = `console.log('test');`; 94 | const ast = generateFlatAST(code); 95 | let error = ''; 96 | let arborist; 97 | try { 98 | arborist = new Arborist(ast); 99 | } catch (e) { 100 | error = e.message; 101 | } 102 | assert.ok(arborist?.ast?.length, `Arborist failed to instantiate. ${error ? 'Error: ' + error : ''}`); 103 | assert.equal(error, '', `Arborist instantiated with an error: ${error}`); 104 | assert.deepEqual(arborist.ast, ast, `Arborist ast array did not match initialization argument.`); 105 | }); 106 | it('Verify invalid changes are not applied', () => { 107 | const code = `console.log('test');`; 108 | const arborist = new Arborist(code); 109 | arborist.markNode(arborist.ast.find(n => n.type === 'Literal'), {type: 'EmptyStatement'}); 110 | arborist.markNode(arborist.ast.find(n => n.name === 'log'), {type: 'EmptyStatement'}); 111 | arborist.applyChanges(); 112 | assert.equal(arborist.script, code, 'Invalid changes were applied.'); 113 | }); 114 | it(`Verify comments aren't duplicated when replacing the root node`, () => { 115 | const code = `//comment1\nconst a = 1, b = 2;`; 116 | const expected = `//comment1\nconst a = 1;\nconst b = 2;`; 117 | const arb = new Arborist(code); 118 | const decls = []; 119 | arb.ast.forEach(n => { 120 | if (n.type === 'VariableDeclarator') { 121 | decls.push({ 122 | type: 'VariableDeclaration', 123 | kind: 'const', 124 | declarations: [n], 125 | }); 126 | } 127 | }); 128 | arb.markNode(arb.ast[0], { 129 | ...arb.ast[0], 130 | body: decls, 131 | }); 132 | arb.applyChanges(); 133 | assert.equal(arb.script, expected); 134 | }); 135 | it.skip(`FIX: Verify comments are kept when replacing a node`, () => { 136 | const code = ` 137 | // comment1 138 | const a = 1; 139 | 140 | // comment2 141 | let b = 2; 142 | 143 | // comment3 144 | const c = 3;`; 145 | const expected = `// comment1\nvar a = 1;\n// comment2\nlet b = 2;\n// comment3\nvar c = 3;`; 146 | const arb = new Arborist(code); 147 | arb.ast.forEach(n => { 148 | if (n.type === 'VariableDeclaration' 149 | && n.kind === 'const') { 150 | arb.markNode(n, { 151 | ...n, 152 | kind: 'var', 153 | }); 154 | } 155 | }); 156 | arb.applyChanges(); 157 | assert.equal(arb.script, expected); 158 | }); 159 | }); 160 | 161 | describe('Arborist edge case tests', () => { 162 | it('Preserves comments when replacing a non-root node', () => { 163 | const code = `const a = 1; // trailing\nconst b = 2;`; 164 | const expected = `const a = 1;\n// trailing\nconst b = 3;`; 165 | const arb = new Arborist(code); 166 | const bDecl = arb.ast.find(n => n.type === 'VariableDeclarator' && n.id.name === 'b'); 167 | arb.markNode(bDecl.init, {type: 'Literal', value: 3, raw: '3'}); 168 | arb.applyChanges(); 169 | assert.equal(arb.script, expected); 170 | }); 171 | 172 | it('Deleting the only element in an array leaves parent valid', () => { 173 | const code = `const a = [42];`; 174 | const expected = `const a = [];`; 175 | const arb = new Arborist(code); 176 | const literal = arb.ast.find(n => n.type === 'Literal'); 177 | arb.markNode(literal); 178 | arb.applyChanges(); 179 | assert.equal(arb.script, expected); 180 | }); 181 | 182 | it('Multiple changes in a single pass (replace and delete siblings)', () => { 183 | const code = `let a = 1, b = 2, c = 3;`; 184 | const expected = `let a = 10, c = 3;`; 185 | const arb = new Arborist(code); 186 | const bDecl = arb.ast.find(n => n.type === 'VariableDeclarator' && n.id.name === 'b'); 187 | const aDecl = arb.ast.find(n => n.type === 'VariableDeclarator' && n.id.name === 'a'); 188 | arb.markNode(bDecl); // delete b 189 | arb.markNode(aDecl.init, {type: 'Literal', value: 10, raw: '10'}); // replace a's value 190 | arb.applyChanges(); 191 | assert.equal(arb.script, expected); 192 | }); 193 | 194 | it('Deeply nested node replacement', () => { 195 | const code = `if (a) { if (b) { c(); } }`; 196 | const expected = `if (a) { 197 | if (b) { 198 | d(); 199 | } 200 | }`; 201 | const arb = new Arborist(code); 202 | const cCall = arb.ast.find(n => n.type === 'Identifier' && n.name === 'c'); 203 | arb.markNode(cCall, {type: 'Identifier', name: 'd'}); 204 | arb.applyChanges(); 205 | assert.equal(arb.script, expected); 206 | }); 207 | 208 | it('Multiple comments on a node being deleted', () => { 209 | const code = `// lead1\n// lead2\nconst a = 1; // trail1\n// trail2\nconst b = 2;`; 210 | const expected = `// lead1\n// lead2\nconst a = 1; // trail1\n // trail2`; 211 | const arb = new Arborist(code); 212 | const bDecl = arb.ast.find(n => n.type === 'VariableDeclaration' && n.declarations[0].id.name === 'b'); 213 | arb.markNode(bDecl); 214 | arb.applyChanges(); 215 | assert.equal(arb.script.trim(), expected.trim()); 216 | }); 217 | 218 | it('Marking the same node for deletion and replacement only applies one change', () => { 219 | const code = `let x = 1;`; 220 | const expected = `let x = 2;`; 221 | const arb = new Arborist(code); 222 | const literal = arb.ast.find(n => n.type === 'Literal'); 223 | arb.markNode(literal, {type: 'Literal', value: 2, raw: '2'}); 224 | arb.markNode(literal); // Should not delete after replacement 225 | arb.applyChanges(); 226 | assert.equal(arb.script, expected); 227 | }); 228 | 229 | it('AST is still valid and mutable after applyChanges', () => { 230 | const code = `let y = 5;`; 231 | const arb = new Arborist(code); 232 | const literal = arb.ast.find(n => n.type === 'Literal'); 233 | arb.markNode(literal, {type: 'Literal', value: 10, raw: '10'}); 234 | arb.applyChanges(); 235 | assert.equal(arb.script, 'let y = 10;'); // Validate the change was applied 236 | // Now change again 237 | const newLiteral = arb.ast.find(n => n.type === 'Literal'); 238 | arb.markNode(newLiteral, {type: 'Literal', value: 20, raw: '20'}); 239 | arb.applyChanges(); 240 | assert.equal(arb.script, 'let y = 20;'); 241 | }); 242 | }); -------------------------------------------------------------------------------- /tests/functionality.test.js: -------------------------------------------------------------------------------- 1 | import path from 'node:path'; 2 | import assert from 'node:assert'; 3 | import {describe, it} from 'node:test'; 4 | import {fileURLToPath} from 'node:url'; 5 | import {generateFlatAST, generateCode} from '../src/index.js'; 6 | 7 | const __filename = fileURLToPath(import.meta.url); 8 | const __dirname = path.dirname(__filename); 9 | 10 | describe('Functionality tests', () => { 11 | it('Verify the code breakdown generates the expected nodes by checking the properties of the generated ASTNodes', () => { 12 | const code = `a=3`; 13 | const ast = generateFlatAST(code); 14 | const expectedBreakdown = [ 15 | {nodeId: 0, type: 'Program', start: 0, end: 3, src: 'a=3', parentNode: null, parentKey: ''}, 16 | {nodeId: 1, type: 'ExpressionStatement', start: 0, end: 3, src: 'a=3', parentKey: 'body'}, 17 | {nodeId: 2, type: 'AssignmentExpression', start: 0, end: 3, src: 'a=3', operator: '=', parentKey: 'expression'}, 18 | {nodeId: 3, type: 'Identifier', start: 0, end: 1, src: 'a', parentKey: 'left'}, 19 | {nodeId: 4, type: 'Literal', start: 2, end: 3, src: '3', value: 3, raw: '3', parentKey: 'right'}, 20 | ]; 21 | expectedBreakdown.forEach(node => { 22 | const parsedNode = ast[node.nodeId]; 23 | for (const [k, v] of Object.entries(node)) { 24 | assert.equal(v, parsedNode[k], `Node #${node.nodeId} parsed wrong on key '${k}'`); 25 | } 26 | }); 27 | }); 28 | it('Verify the expected functions and classes can be imported', async () => { 29 | const availableImports = [ 30 | 'Arborist', 31 | 'ASTNode', 32 | 'ASTScope', 33 | 'generateCode', 34 | 'generateFlatAST', 35 | 'parseCode', 36 | 'applyIteratively', 37 | 'logger', 38 | 'treeModifier', 39 | ]; 40 | const flast = await import(path.resolve(__dirname + '/../src/index.js')); 41 | for (const importName of availableImports) { 42 | assert.ok(importName in flast, `Failed to import "${importName}"`); 43 | } 44 | }); 45 | it('Verify the code breakdown generates the expected nodes by checking the number of nodes for each expected type', () => { 46 | const code = `console.log('hello' + ' ' + 'there');`; 47 | const ast = generateFlatAST(code); 48 | const expectedBreakdown = { 49 | Program: 1, 50 | ExpressionStatement: 1, 51 | CallExpression: 1, 52 | MemberExpression: 1, 53 | Identifier: 2, 54 | BinaryExpression: 2, 55 | Literal: 3, 56 | }; 57 | const expectedNumberOfNodes = 11; 58 | assert.equal(ast.length, expectedNumberOfNodes, `Unexpected number of nodes`); 59 | for (const nodeType of Object.keys(expectedBreakdown)) { 60 | const numberOfNodes = ast.filter(n => n.type === nodeType).length; 61 | assert.equal(numberOfNodes, expectedBreakdown[nodeType], `Wrong number of nodes for '${nodeType}' node type`); 62 | } 63 | }); 64 | it('Verify the AST can be parsed and regenerated into the same code', () => { 65 | const code = `console.log('hello' + ' ' + 'there');`; 66 | const ast = generateFlatAST(code); 67 | const regeneratedCode = generateCode(ast[0]); 68 | assert.equal(regeneratedCode, code, `Original code did not regenerate back to the same source.`); 69 | }); 70 | it(`Verify generateFlatAST's detailed option works as expected`, () => { 71 | const code = `var a = [1]; a[0];`; 72 | const noDetailsAst = generateFlatAST(code, {detailed: false}); 73 | const [noDetailsVarDec, noDetailsVarRef] = noDetailsAst.filter(n => n.type === 'Identifier'); 74 | assert.equal(noDetailsVarDec.references || noDetailsVarRef.declNode || noDetailsVarRef.scope, undefined, 75 | `Flat AST generated with details despite 'detailed' option set to false.`); 76 | 77 | const noSrcAst = generateFlatAST(code, {includeSrc: false}); 78 | assert.equal(noSrcAst.find(n => n.src !== undefined), null, `Flat AST generated with src despite 'includeSrc' option set to false.`); 79 | 80 | const detailedAst = generateFlatAST(code, {detailed: true}); 81 | const [detailedVarDec, detailedVarRef] = detailedAst.filter(n => n.type === 'Identifier'); 82 | assert.ok(detailedVarDec.parentNode && detailedVarDec.childNodes && detailedVarDec.references && 83 | detailedVarRef.declNode && detailedVarRef.nodeId && detailedVarRef.scope && detailedVarRef.src, 84 | `Flat AST missing details despite 'detailed' option set to true.`); 85 | 86 | const detailedNoSrcAst = generateFlatAST(code, {detailed: true, includeSrc: false}); 87 | assert.equal(detailedNoSrcAst[0].src, undefined, `Flat AST includes details despite 'detailed' option set to true and 'includeSrc' option set to false.`); 88 | }); 89 | it(`Verify a script is parsed in "sloppy mode" if strict mode is restricting parsing`, () => { 90 | const code = `let a; delete a;`; 91 | let ast = []; 92 | let error = ''; 93 | try { 94 | ast = generateFlatAST(code); 95 | } catch (e) { 96 | error = e.message; 97 | } 98 | assert.ok(ast.length, `Script was not parsed. Got the error "${error}"`); 99 | }); 100 | it(`Verify a script is only parsed in its selected sourceType`, () => { 101 | const code = `let a; delete a;`; 102 | let unparsedAst = []; 103 | let parsedAst = []; 104 | let unparsedError = ''; 105 | let parsedError = ''; 106 | try { 107 | unparsedAst = generateFlatAST(code, {alternateSourceTypeOnFailure: false}); 108 | } catch (e) { 109 | unparsedError = e.message; 110 | } 111 | try { 112 | parsedAst = generateFlatAST(code, {alternateSourceTypeOnFailure: true}); 113 | } catch (e) { 114 | parsedError = e.message; 115 | } 116 | assert.equal(unparsedAst.length, 0, `Script was not parsed.${unparsedError ? 'Error: ' + unparsedError : ''}`); 117 | assert.ok(parsedAst.length, `Script was not parsed.${parsedError ? 'Error: ' + parsedError : ''}`); 118 | }); 119 | it(`Verify generateFlatAST doesn't throw an exception for invalid code`, () => { 120 | const code = `return a;`; 121 | let result; 122 | const expectedResult = []; 123 | try { 124 | result = generateFlatAST(code, {alternateSourceTypeOnFailure: false}); 125 | } catch (e) { 126 | result = e.message; 127 | } 128 | assert.deepStrictEqual(result, expectedResult); 129 | }); 130 | }); -------------------------------------------------------------------------------- /tests/parsing.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import {describe, it} from 'node:test'; 3 | import {generateFlatAST, generateCode} from '../src/index.js'; 4 | 5 | describe('Parsing tests', () => { 6 | it('Verify the function-expression-name scope is always replaced with its child scope', () => { 7 | const code = ` 8 | (function test(p) { 9 | let i = 1; 10 | i; 11 | })();`; 12 | const ast = generateFlatAST(code); 13 | const testedScope = ast[0].allScopes[Object.keys(ast[0].allScopes).slice(-1)[0]]; 14 | const expectedParentScopeType = 'function-expression-name'; 15 | const expectedScopeType = 'function'; 16 | // ast.slice(-1)[0].type is the last identifier in the code and should have the expected scope type 17 | assert.equal(ast.slice(-1)[0].scope.type, expectedScopeType, `Unexpected scope`); 18 | assert.equal(testedScope.upper.type, expectedParentScopeType, `Tested scope is not the child of the correct scope`); 19 | }); 20 | it('Verify declNode references the local declaration correctly', () => { 21 | const innerScopeVal = 'inner'; 22 | const outerScopeVal = 'outer'; 23 | const code = `var a = '${outerScopeVal}'; 24 | if (true) { 25 | let a = '${innerScopeVal}'; 26 | console.log(a); 27 | } 28 | console.log(a);`; 29 | const ast = generateFlatAST(code); 30 | const [innerIdentifier, outerIdentifier] = ast.filter(n => n.type === 'Identifier' && n.parentNode.type === 'CallExpression'); 31 | const innerValResult = innerIdentifier.declNode.parentNode.init.value; 32 | const outerValResult = outerIdentifier.declNode.parentNode.init.value; 33 | assert.equal(innerValResult, innerScopeVal, `Declaration node (inner scope) is incorrectly referenced.`); 34 | assert.equal(outerValResult, outerScopeVal, `Declaration node (outer scope) is incorrectly referenced.`); 35 | }); 36 | it(`Verify a function's identifier isn't treated as a reference`, () => { 37 | const code = `function a() { 38 | var a; 39 | }`; 40 | const ast = generateFlatAST(code); 41 | const funcId = ast.find(n => n.name ==='a' && n.parentNode.type === 'FunctionDeclaration'); 42 | const varId = ast.find(n =>n.name ==='a' && n.parentNode.type === 'VariableDeclarator'); 43 | const functionReferencesFound = !!funcId.references?.length; 44 | const variableReferencesFound = !!varId.references?.length; 45 | assert.ok(!functionReferencesFound, `References to a function were incorrectly found`); 46 | assert.ok(!variableReferencesFound, `References to a variable were incorrectly found`); 47 | }); 48 | it(`Verify proper handling of class properties`, () => { 49 | const code = `class a { 50 | static b = 1; 51 | #c = 2; 52 | }`; 53 | const expected = code; 54 | const ast = generateFlatAST(code); 55 | const result = generateCode(ast[0]); 56 | assert.strictEqual(result, expected); 57 | }); 58 | it(`Verify the type map is generated accurately`, () => { 59 | const code = `class a { 60 | static b = 1; 61 | #c = 2; 62 | }`; 63 | const ast = generateFlatAST(code); 64 | const expected = { 65 | Program: [ast[0]], 66 | ClassDeclaration: [ast[1]], 67 | Identifier: [ast[2], ast[5]], 68 | ClassBody: [ast[3]], 69 | PropertyDefinition: [ast[4], ast[7]], 70 | Literal: [ast[6], ast[9]], 71 | PrivateIdentifier: [ast[8]], 72 | }; 73 | const result = ast[0].typeMap; 74 | assert.deepEqual(result, expected); 75 | }); 76 | it(`Verify node relations are parsed correctly`, () => { 77 | const code = `for (var i = 0; i < 10; i++);\nfor (var i = 0; i < 10; i++);`; 78 | try { 79 | generateFlatAST(code); 80 | } catch (e) { 81 | assert.fail(`Parsing failed: ${e.message}`); 82 | } 83 | }); 84 | it(`Verify the module scope is ignored`, () => { 85 | const code = `function a() {return [1];}\nconst b = a();`; 86 | const ast = generateFlatAST(code); 87 | ast.forEach(n => assert.ok(n.scope.type !== 'module', `Module scope was not ignored`)); 88 | }); 89 | it(`Verify the lineage is correct`, () => { 90 | const code = `(function() {var a; function b() {var c;}})();`; 91 | const ast = generateFlatAST(code); 92 | function extractLineage(node) { 93 | const lineage = []; 94 | let currentNode = node; 95 | while (currentNode) { 96 | lineage.push(currentNode.scope.scopeId); 97 | if (!currentNode.scope.scopeId) break; 98 | currentNode = currentNode.parentNode; 99 | } 100 | return [...new Set(lineage)].reverse(); 101 | } 102 | ast[0].typeMap.Identifier.forEach(n => { 103 | const extractedLineage = extractLineage(n); 104 | assert.deepEqual(n.lineage, extractedLineage); 105 | }); 106 | }); 107 | it(`Verify null childNodes are correctly parsed`, () => { 108 | const code = `[,,,].join('-');`; 109 | const ast = generateFlatAST(code); 110 | assert.notEqual(ast, [1]); 111 | }); 112 | it(`Verify all identifiers are referenced correctly`, () => { 113 | const code = `let a = 1; switch(a) {case 1: a;}`; 114 | const ast = generateFlatAST(code); 115 | ast.filter(n => n.type === 'Identifier').forEach(n => { 116 | assert.ok(n.references?.length || n.declNode, `Identifier '${n.name}' (#${n.nodeId}) is not referenced`); 117 | }); 118 | }); 119 | }); -------------------------------------------------------------------------------- /tests/utils.test.js: -------------------------------------------------------------------------------- 1 | import assert from 'node:assert'; 2 | import {describe, it} from 'node:test'; 3 | import {treeModifier, applyIteratively, logger} from '../src/index.js'; 4 | 5 | describe('Utils tests: treeModifier', () => { 6 | it(`Verify treeModifier sets a generic function name`, () => { 7 | const expectedFuncName = 'func'; 8 | const generatedFunc = treeModifier(() => {}, () => {}); 9 | assert.equal(generatedFunc.name, expectedFuncName, `The default name of the generated function does not match`); 10 | }); 11 | it(`Verify treeModifier sets the function's name properly`, () => { 12 | const expectedFuncName = 'expectedFuncName'; 13 | const generatedFunc = treeModifier(() => {}, () => {}, expectedFuncName); 14 | assert.equal(generatedFunc.name, expectedFuncName, `The name of the generated function does not match`); 15 | }); 16 | }); 17 | describe('Utils tests: applyIteratively', () => { 18 | it('Verify applyIteratively cannot remove the root node without replacing it', () => { 19 | const code = `a`; 20 | const expectedOutput = code; 21 | const f = n => n.type === 'Program'; 22 | const m = (n, arb) => arb.markNode(n); 23 | const generatedFunc = treeModifier(f, m); 24 | const result = applyIteratively(code, [generatedFunc]); 25 | 26 | assert.equal(result, expectedOutput, `Result does not match expected output`); 27 | }); 28 | it('Verify applyIteratively catches a critical exception', () => { 29 | const code = `a`; 30 | // noinspection JSCheckFunctionSignatures 31 | const result = applyIteratively(code, {length: 4}); 32 | assert.equal(result, code, `Result does not match expected output`); 33 | }); 34 | it('Verify applyIteratively works as expected', () => { 35 | const code = `console.log('Hello' + ' ' + 'there');`; 36 | const expectedOutput = `console.log('General' + ' ' + 'Kenobi');`; 37 | const replacements = { 38 | Hello: 'General', 39 | there: 'Kenobi', 40 | }; 41 | let result = code; 42 | const f = n => n.type === 'Literal' && replacements[n.value]; 43 | const m = (n, arb) => arb.markNode(n, { 44 | type: 'Literal', 45 | value: replacements[n.value], 46 | }); 47 | const generatedFunc = treeModifier(f, m); 48 | result = applyIteratively(result, [generatedFunc]); 49 | 50 | assert.equal(result, expectedOutput, `Result does not match expected output`); 51 | }); 52 | }); 53 | describe('Utils tests: logger', () => { 54 | it(`Verify logger sets the log level to DEBUG properly`, () => { 55 | const expectedLogLevel = logger.logLevels.DEBUG; 56 | logger.setLogLevelDebug(); 57 | assert.equal(logger.currentLogLevel, expectedLogLevel, `The log level DEBUG was not set properly`); 58 | }); 59 | it(`Verify logger sets the log level to NONE properly`, () => { 60 | const expectedLogLevel = logger.logLevels.NONE; 61 | logger.setLogLevelNone(); 62 | assert.equal(logger.currentLogLevel, expectedLogLevel, `The log level NONE was not set properly`); 63 | }); 64 | it(`Verify logger sets the log level to LOG properly`, () => { 65 | const expectedLogLevel = logger.logLevels.LOG; 66 | logger.setLogLevelLog(); 67 | assert.equal(logger.currentLogLevel, expectedLogLevel, `The log level LOG was not set properly`); 68 | }); 69 | it(`Verify logger sets the log level to ERROR properly`, () => { 70 | const expectedLogLevel = logger.logLevels.ERROR; 71 | logger.setLogLevelError(); 72 | assert.equal(logger.currentLogLevel, expectedLogLevel, `The log level ERROR was not set properly`); 73 | }); 74 | it(`Verify logger sets the log function properly`, () => { 75 | const expectedLogFunc = () => 'test'; 76 | logger.setLogFunc(expectedLogFunc); 77 | assert.equal(logger.logFunc, expectedLogFunc, `The log function was not set properly`); 78 | }); 79 | it(`Verify logger throws an error when setting an unknown log level`, () => { 80 | assert.throws(() => logger.setLogLevel(0), Error, `An error was not thrown when setting an unknown log level`); 81 | }); 82 | }); --------------------------------------------------------------------------------