├── .editor
└── .gitignore
├── .editorconfig
├── .github
├── CODEOWNERS
├── release.yml
└── workflows
│ ├── lint.yml
│ ├── release.yml
│ └── web.yaml
├── .gitignore
├── LICENSE
├── README.md
├── bun.lock
├── dist
└── .gitignore
├── eslint.config.mjs
├── logo.png
├── logo.svg
├── package.json
├── rollup.config.js
├── src
├── cli.ts
├── index.ts
└── lib
│ ├── configparser
│ └── index.ts
│ ├── filewalker
│ └── index.ts
│ └── parser
│ ├── BrunoDocumentParser.ts
│ ├── BrunoToJson.ts
│ ├── Diff.ts
│ ├── DocumentBuilder.ts
│ ├── DocumentParser.ts
│ ├── OpenAPIDocumentParser.ts
│ ├── PostmanDocumentParser.ts
│ └── index.ts
├── tsconfig.json
└── web
├── .envrc
├── .gitignore
├── .npmrc
├── .nvmrc
├── .prettierignore
├── .prettierrc
├── .yamllint.yaml
├── README.md
├── bun.lock
├── eslint.config.js
├── package.json
├── src
├── app.css
├── app.d.ts
├── app.html
├── lib
│ ├── HeadComponent.svelte
│ └── index.ts
└── routes
│ ├── +layout.svelte
│ ├── +layout.ts
│ └── +page.svelte
├── static
├── favicon.png
├── logo.png
├── logo.svg
└── schema.json
├── svelte.config.js
├── tsconfig.json
└── vite.config.ts
/.editor/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 |
10 | [*.go]
11 | indent_style = tab
12 | insert_final_newline = false
13 |
14 | [Makefile]
15 | indent_style = tab
16 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @gorillamoe
2 |
--------------------------------------------------------------------------------
/.github/release.yml:
--------------------------------------------------------------------------------
1 | changelog:
2 | exclude:
3 | labels:
4 | - ignore-for-release
5 | categories:
6 | - title: Breaking Changes 💥
7 | labels:
8 | - Semver-Major
9 | - breaking-change
10 | - title: Exciting New Features ✨
11 | labels:
12 | - Semver-Minor
13 | - enhancement
14 | - title: Bug Fixes 🐛
15 | labels:
16 | - Semver-Patch
17 | - bug
18 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: Lint
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | pull_request:
8 |
9 | permissions:
10 | contents: read
11 | pull-requests: read
12 |
13 | jobs:
14 | lint:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - name: Checkout
18 | uses: actions/checkout@v4
19 | with:
20 | fetch-depth: 0
21 | - name: Set up env
22 | run: |
23 | VERSION=${GITHUB_REF_NAME#v}
24 | echo "VERSION=$VERSION" >> $GITHUB_ENV
25 | - name: Set up Bun
26 | uses: oven-sh/setup-bun@v2
27 | - name: Cache Bun
28 | uses: actions/cache@v4
29 | with:
30 | path: ~/.bun/install/cache
31 | key: linux-bun-${{ hashFiles('**/bun.lock') }}
32 | restore-keys: |
33 | linux-bun-${{ hashFiles('**/bun.lock') }}
34 | - name: Install dependencies
35 | run: bun install --frozen-lockfile
36 | - name: Lint
37 | run: bun run lint
38 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Release
3 |
4 | on:
5 | push:
6 | tags:
7 | - 'v[0-9]+.[0-9]+.[0-9]+'
8 | jobs:
9 | release:
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v4
14 | with:
15 | fetch-depth: 0
16 | - name: Set up env
17 | run: |
18 | VERSION=${GITHUB_REF_NAME#v}
19 | echo "VERSION=$VERSION" >> $GITHUB_ENV
20 | - name: Set up Bun
21 | uses: oven-sh/setup-bun@v2
22 | - name: Cache Bun
23 | uses: actions/cache@v4
24 | with:
25 | path: ~/.bun/install/cache
26 | key: linux-bun-${{ hashFiles('**/bun.lock') }}
27 | restore-keys: |
28 | linux-bun-${{ hashFiles('**/bun.lock') }}
29 | - name: Install dependencies
30 | run: bun install --frozen-lockfile
31 | - name: Build
32 | run: bun run build
33 | - name: Publish
34 | run: bun publish
35 | env:
36 | NPM_CONFIG_TOKEN: ${{ secrets.NPM_CONFIG_TOKEN }}
37 | - name: Make release
38 | run: |
39 | gh release create v$VERSION -t "v$VERSION" --generate-notes
40 | env:
41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
42 |
--------------------------------------------------------------------------------
/.github/workflows/web.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | name: Deploy website
3 |
4 | on:
5 | push:
6 | branches:
7 | - main
8 | paths:
9 | - 'web/**'
10 |
11 | # Allows you to run this workflow manually from the Actions tab
12 | workflow_dispatch:
13 |
14 | # Allow only one concurrent deployment,
15 | # skipping runs queued between the run in-progress and latest queued.
16 | # However, do NOT cancel in-progress runs as we want
17 | # to allow these production deployments to complete.
18 | concurrency:
19 | group: "web"
20 | cancel-in-progress: false
21 |
22 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
23 | permissions:
24 | contents: write
25 | pages: write
26 | id-token: write
27 |
28 | jobs:
29 | build-linux:
30 | name: Deploy website
31 | runs-on: ubuntu-latest
32 | steps:
33 | - name: Checkout
34 | uses: actions/checkout@v4
35 | with:
36 | fetch-depth: 0
37 | - name: Set up Bun
38 | uses: oven-sh/setup-bun@v2
39 | - name: Cache Bun
40 | uses: actions/cache@v4
41 | with:
42 | path: ~/.bun/install/cache
43 | key: linux-bun-web-${{ hashFiles('**/bun.lock') }}
44 | restore-keys: |
45 | linux-bun-web-${{ hashFiles('**/bun.lock') }}
46 | - name: Install web dependencies
47 | run: cd web && bun install --frozen-lockfile
48 | - name: Create Website
49 | run: cd web && bun run build
50 | - name: Setup Pages
51 | uses: actions/configure-pages@v5
52 | - name: Upload artifact
53 | uses: actions/upload-pages-artifact@v3
54 | with:
55 | path: "web/build"
56 | - name: Deploy to GitHub Pages
57 | id: deployment
58 | uses: actions/deploy-pages@v4
59 | env:
60 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
61 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Binaries for programs and plugins
2 | *.exe
3 | *.exe~
4 | *.dll
5 | *.so
6 | *.dylib
7 |
8 | # Test binary, built with `go test -c`
9 | *.test
10 |
11 | # Output of the go coverage tool, specifically when used with LiteIDE
12 | *.out
13 |
14 | # Go workspace file
15 | go.work
16 |
17 | .env
18 | http-client.env.json
19 | http-client.private.env.json
20 | *.rest
21 | *.http
22 |
23 | node_modules
24 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2025+ mistweaverco
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 |
2 |
3 | 
4 |
5 | # kulala-fmt
6 |
7 | [](https://www.npmjs.com/package/@mistweaverco/kulala-fmt)
8 | [](https://www.typescriptlang.org/)
9 | [](https://rollupjs.org/)
10 | [](https://github.com/mistweaverco/kulala-fmt/releases/latest)
11 | [](https://discord.gg/QyVQmfY4Rt)
12 |
13 | [Install](#install) • [Usage](#usage)
14 |
15 |
16 |
17 | An opinionated 🦄 .http and .rest 🐼 files linter 💄 and formatter ⚡.
18 |
19 |
20 |
21 |
22 |
23 | ## Install
24 |
25 | Via npm:
26 |
27 | ```sh
28 | npm install -g @mistweaverco/kulala-fmt
29 | ```
30 |
31 | ## Usage
32 |
33 | kulala-fmt can `format` and `check` `.http` and `.rest` files.
34 |
35 | It can also `convert` OpenAPI `.yaml`, `.yml` or `.json` files to `.http` files.
36 |
37 | ### Format
38 |
39 | Format all `.http` and `.rest` files in the current directory and its subdirectories:
40 |
41 | ```sh
42 | kulala-fmt format
43 | ```
44 |
45 | Format specific `.http` and `.rest` files.
46 |
47 | ```sh
48 | kulala-fmt format file1.http file2.rest http/*.http
49 | ```
50 |
51 | Format stdin input:
52 |
53 | ```sh
54 | cat SOMEFILE.http | kulala-fmt format --stdin
55 | ```
56 |
57 | ### Check
58 |
59 | Check if all `.http` and `.rest` files in the current directory and its subdirectories are formatted:
60 |
61 | ```sh
62 | kulala-fmt check
63 | ```
64 |
65 | Check if specific `.http` and `.rest` files are formatted:
66 |
67 | ```sh
68 | kulala-fmt check file1.http file2.rest http/*.http
69 | ```
70 |
71 | Check if all `.http` and `.rest` files in the current directory and
72 | its subdirectories are formatted and
73 | prints the desired output to the console:
74 |
75 | ```sh
76 | kulala-fmt check --verbose
77 | ```
78 |
79 | Check if specific `.http` and `.rest` files are formatted and
80 | prints the desired output to the console:
81 |
82 | ```sh
83 | kulala-fmt check --verbose file1.http file2.rest http/*.http
84 | ```
85 |
86 | Check stdin input:
87 |
88 | ```sh
89 | cat SOMEFILE.http | kulala-fmt format --stdin
90 | ```
91 |
92 | ### Convert
93 |
94 | #### OpenAPI to `.http`
95 |
96 | Convert OpenAPI `.yaml`, `.yml` or `.json` files to `.http` files:
97 |
98 | ```sh
99 | kulala-fmt convert --from openapi openapi.yaml
100 | ```
101 |
102 | #### Postman collection to `.http`
103 |
104 | Convert Postman collection `.json` files to `.http` files:
105 |
106 | ```sh
107 | kulala-fmt convert --from postman postman.json
108 | ```
109 |
110 | #### Bruno to `.http`
111 |
112 | Convert Bruno collections to `.http` files:
113 |
114 | ```sh
115 | kulala-fmt convert --from bruno path/to/bruno/collection
116 | ```
117 |
118 | ## What does it do?
119 |
120 | - Checks if the file is formatted and valid
121 | - Removes extraneous newlines
122 | - Makes sure document variables are at the top of the file
123 | - Lowercases all headers (when HTTP/2 or HTTP/3) else it will uppercase the first letter
124 | - Puts all metadata right before the request line
125 | - Ensures all comments are at the top of the request
126 |
127 | So a perfect request would look like this:
128 |
129 | ```http
130 | @variables1 = value1
131 |
132 |
133 | ### REQUEST_NAME_ONE
134 |
135 | # This is a comment
136 | # This is another comment
137 | # @someother metatag
138 | GET http://localhost:8080/api/v1/health HTTP/1.1
139 | Content-Type: application/json
140 |
141 | {
142 | "key": "value"
143 | }
144 | ```
145 |
146 | or this:
147 |
148 | ```http
149 | @variables1 = value1
150 |
151 |
152 | ### REQUEST_NAME_ONE
153 |
154 | # This is a comment
155 | # This is another comment
156 | # @someother metatag
157 | GET http://localhost:8080/api/v1/health HTTP/2
158 | content-type: application/json
159 |
160 | {
161 | "key": "value"
162 | }
163 | ```
164 |
165 | ## Use it with conform.nvim
166 |
167 | ```lua
168 | return {
169 | "stevearc/conform.nvim",
170 | config = function()
171 | require("conform").setup({
172 | formatters = {
173 | kulala = {
174 | command = "kulala-fmt",
175 | args = { "format", "$FILENAME" },
176 | stdin = false,
177 | },
178 | },
179 | formatters_by_ft = {
180 | http = { "kulala" },
181 | },
182 | format_on_save = true,
183 | })
184 | end,
185 | }
186 | ```
187 |
--------------------------------------------------------------------------------
/bun.lock:
--------------------------------------------------------------------------------
1 | {
2 | "lockfileVersion": 1,
3 | "workspaces": {
4 | "": {
5 | "name": "@mistweaverco/kulala-fmt",
6 | "dependencies": {
7 | "@mistweaverco/tree-sitter-kulala": "1.9.0",
8 | "prettier": "^3.5.2",
9 | "tree-sitter": "0.22.4",
10 | },
11 | "devDependencies": {
12 | "@rollup/plugin-commonjs": "^28.0.2",
13 | "@rollup/plugin-json": "^6.1.0",
14 | "@rollup/plugin-node-resolve": "^16.0.0",
15 | "@rollup/plugin-typescript": "^12.1.2",
16 | "@types/bun": "latest",
17 | "@types/diff": "^7.0.1",
18 | "@types/js-yaml": "^4.0.9",
19 | "@typescript-eslint/eslint-plugin": "^8.24.1",
20 | "@typescript-eslint/parser": "^8.24.1",
21 | "colors": "^1.4.0",
22 | "commander": "^13.1.0",
23 | "diff": "^7.0.0",
24 | "eslint": "^9.20.1",
25 | "eslint-config-prettier": "^10.0.1",
26 | "eslint-plugin-prettier": "^5.2.3",
27 | "js-yaml": "^4.1.0",
28 | "lodash": "^4.17.21",
29 | "ohm-js": "^17.1.0",
30 | "rollup": "^4.34.8",
31 | "tslib": "^2.8.1",
32 | "typescript-eslint": "^8.24.1",
33 | },
34 | "peerDependencies": {
35 | "typescript": "^5.7.3",
36 | },
37 | },
38 | },
39 | "trustedDependencies": [
40 | "@mistweaverco/tree-sitter-kulala",
41 | ],
42 | "packages": {
43 | "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.4.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA=="],
44 |
45 | "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.1", "", {}, "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ=="],
46 |
47 | "@eslint/config-array": ["@eslint/config-array@0.19.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.6", "debug": "^4.3.1", "minimatch": "^3.1.2" } }, "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w=="],
48 |
49 | "@eslint/core": ["@eslint/core@0.11.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA=="],
50 |
51 | "@eslint/eslintrc": ["@eslint/eslintrc@3.2.0", "", { "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" } }, "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w=="],
52 |
53 | "@eslint/js": ["@eslint/js@9.20.0", "", {}, "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ=="],
54 |
55 | "@eslint/object-schema": ["@eslint/object-schema@2.1.6", "", {}, "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA=="],
56 |
57 | "@eslint/plugin-kit": ["@eslint/plugin-kit@0.2.7", "", { "dependencies": { "@eslint/core": "^0.12.0", "levn": "^0.4.1" } }, "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g=="],
58 |
59 | "@humanfs/core": ["@humanfs/core@0.19.1", "", {}, "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA=="],
60 |
61 | "@humanfs/node": ["@humanfs/node@0.16.6", "", { "dependencies": { "@humanfs/core": "^0.19.1", "@humanwhocodes/retry": "^0.3.0" } }, "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw=="],
62 |
63 | "@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA=="],
64 |
65 | "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.2", "", {}, "sha512-xeO57FpIu4p1Ri3Jq/EXq4ClRm86dVF2z/+kvFnyqVYRavTZmaFaUBbWCOuuTh0o/g7DSsk6kc2vrS4Vl5oPOQ=="],
66 |
67 | "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="],
68 |
69 | "@mistweaverco/tree-sitter-kulala": ["@mistweaverco/tree-sitter-kulala@1.9.0", "", { "dependencies": { "node-addon-api": "8.1.0", "node-gyp-build": "4.8.2" } }, "sha512-4F+kCZFEvurSGrWM52lnowXBOqQ5+nfzxp55ufCqTdDSzSI92kySHmlhF7d2g/ZW4Jjrbromh1WH9XK/mHPbzQ=="],
70 |
71 | "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="],
72 |
73 | "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="],
74 |
75 | "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="],
76 |
77 | "@pkgr/core": ["@pkgr/core@0.1.1", "", {}, "sha512-cq8o4cWH0ibXh9VGi5P20Tu9XF/0fFXl9EUinr9QfTM7a7p0oTA4iJRCQWppXR1Pg8dSM0UCItCkPwsk9qWWYA=="],
78 |
79 | "@rollup/plugin-commonjs": ["@rollup/plugin-commonjs@28.0.2", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "commondir": "^1.0.1", "estree-walker": "^2.0.2", "fdir": "^6.2.0", "is-reference": "1.2.1", "magic-string": "^0.30.3", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^2.68.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-BEFI2EDqzl+vA1rl97IDRZ61AIwGH093d9nz8+dThxJNH8oSoB7MjWvPCX3dkaK1/RCJ/1v/R1XB15FuSs0fQw=="],
80 |
81 | "@rollup/plugin-json": ["@rollup/plugin-json@6.1.0", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA=="],
82 |
83 | "@rollup/plugin-node-resolve": ["@rollup/plugin-node-resolve@16.0.0", "", { "dependencies": { "@rollup/pluginutils": "^5.0.1", "@types/resolve": "1.20.2", "deepmerge": "^4.2.2", "is-module": "^1.0.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.78.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-0FPvAeVUT/zdWoO0jnb/V5BlBsUSNfkIOtFHzMO4H9MOklrmQFY6FduVHKucNb/aTFxvnGhj4MNj/T1oNdDfNg=="],
84 |
85 | "@rollup/plugin-typescript": ["@rollup/plugin-typescript@12.1.2", "", { "dependencies": { "@rollup/pluginutils": "^5.1.0", "resolve": "^1.22.1" }, "peerDependencies": { "rollup": "^2.14.0||^3.0.0||^4.0.0", "tslib": "*", "typescript": ">=3.7.0" }, "optionalPeers": ["rollup", "tslib"] }, "sha512-cdtSp154H5sv637uMr1a8OTWB0L1SWDSm1rDGiyfcGcvQ6cuTs4MDk2BVEBGysUWago4OJN4EQZqOTl/QY3Jgg=="],
86 |
87 | "@rollup/pluginutils": ["@rollup/pluginutils@5.1.4", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-walker": "^2.0.2", "picomatch": "^4.0.2" }, "peerDependencies": { "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" }, "optionalPeers": ["rollup"] }, "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ=="],
88 |
89 | "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.34.8", "", { "os": "android", "cpu": "arm" }, "sha512-q217OSE8DTp8AFHuNHXo0Y86e1wtlfVrXiAlwkIvGRQv9zbc6mE3sjIVfwI8sYUyNxwOg0j/Vm1RKM04JcWLJw=="],
90 |
91 | "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.34.8", "", { "os": "android", "cpu": "arm64" }, "sha512-Gigjz7mNWaOL9wCggvoK3jEIUUbGul656opstjaUSGC3eT0BM7PofdAJaBfPFWWkXNVAXbaQtC99OCg4sJv70Q=="],
92 |
93 | "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.34.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-02rVdZ5tgdUNRxIUrFdcMBZQoaPMrxtwSb+/hOfBdqkatYHR3lZ2A2EGyHq2sGOd0Owk80oV3snlDASC24He3Q=="],
94 |
95 | "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.34.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-qIP/elwR/tq/dYRx3lgwK31jkZvMiD6qUtOycLhTzCvrjbZ3LjQnEM9rNhSGpbLXVJYQ3rq39A6Re0h9tU2ynw=="],
96 |
97 | "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.34.8", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-IQNVXL9iY6NniYbTaOKdrlVP3XIqazBgJOVkddzJlqnCpRi/yAeSOa8PLcECFSQochzqApIOE1GHNu3pCz+BDA=="],
98 |
99 | "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.34.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-TYXcHghgnCqYFiE3FT5QwXtOZqDj5GmaFNTNt3jNC+vh22dc/ukG2cG+pi75QO4kACohZzidsq7yKTKwq/Jq7Q=="],
100 |
101 | "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.34.8", "", { "os": "linux", "cpu": "arm" }, "sha512-A4iphFGNkWRd+5m3VIGuqHnG3MVnqKe7Al57u9mwgbyZ2/xF9Jio72MaY7xxh+Y87VAHmGQr73qoKL9HPbXj1g=="],
102 |
103 | "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.34.8", "", { "os": "linux", "cpu": "arm" }, "sha512-S0lqKLfTm5u+QTxlFiAnb2J/2dgQqRy/XvziPtDd1rKZFXHTyYLoVL58M/XFwDI01AQCDIevGLbQrMAtdyanpA=="],
104 |
105 | "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.34.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-jpz9YOuPiSkL4G4pqKrus0pn9aYwpImGkosRKwNi+sJSkz+WU3anZe6hi73StLOQdfXYXC7hUfsQlTnjMd3s1A=="],
106 |
107 | "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.34.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-KdSfaROOUJXgTVxJNAZ3KwkRc5nggDk+06P6lgi1HLv1hskgvxHUKZ4xtwHkVYJ1Rep4GNo+uEfycCRRxht7+Q=="],
108 |
109 | "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.34.8", "", { "os": "linux", "cpu": "none" }, "sha512-NyF4gcxwkMFRjgXBM6g2lkT58OWztZvw5KkV2K0qqSnUEqCVcqdh2jN4gQrTn/YUpAcNKyFHfoOZEer9nwo6uQ=="],
110 |
111 | "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.34.8", "", { "os": "linux", "cpu": "ppc64" }, "sha512-LMJc999GkhGvktHU85zNTDImZVUCJ1z/MbAJTnviiWmmjyckP5aQsHtcujMjpNdMZPT2rQEDBlJfubhs3jsMfw=="],
112 |
113 | "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.34.8", "", { "os": "linux", "cpu": "none" }, "sha512-xAQCAHPj8nJq1PI3z8CIZzXuXCstquz7cIOL73HHdXiRcKk8Ywwqtx2wrIy23EcTn4aZ2fLJNBB8d0tQENPCmw=="],
114 |
115 | "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.34.8", "", { "os": "linux", "cpu": "s390x" }, "sha512-DdePVk1NDEuc3fOe3dPPTb+rjMtuFw89gw6gVWxQFAuEqqSdDKnrwzZHrUYdac7A7dXl9Q2Vflxpme15gUWQFA=="],
116 |
117 | "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.34.8", "", { "os": "linux", "cpu": "x64" }, "sha512-8y7ED8gjxITUltTUEJLQdgpbPh1sUQ0kMTmufRF/Ns5tI9TNMNlhWtmPKKHCU0SilX+3MJkZ0zERYYGIVBYHIA=="],
118 |
119 | "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.34.8", "", { "os": "linux", "cpu": "x64" }, "sha512-SCXcP0ZpGFIe7Ge+McxY5zKxiEI5ra+GT3QRxL0pMMtxPfpyLAKleZODi1zdRHkz5/BhueUrYtYVgubqe9JBNQ=="],
120 |
121 | "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.34.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-YHYsgzZgFJzTRbth4h7Or0m5O74Yda+hLin0irAIobkLQFRQd1qWmnoVfwmKm9TXIZVAD0nZ+GEb2ICicLyCnQ=="],
122 |
123 | "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.34.8", "", { "os": "win32", "cpu": "ia32" }, "sha512-r3NRQrXkHr4uWy5TOjTpTYojR9XmF0j/RYgKCef+Ag46FWUTltm5ziticv8LdNsDMehjJ543x/+TJAek/xBA2w=="],
124 |
125 | "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.34.8", "", { "os": "win32", "cpu": "x64" }, "sha512-U0FaE5O1BCpZSeE6gBl3c5ObhePQSfk9vDRToMmTkbhCOgW4jqvtS5LGyQ76L1fH8sM0keRp4uDTsbjiUyjk0g=="],
126 |
127 | "@types/bun": ["@types/bun@1.2.2", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="],
128 |
129 | "@types/diff": ["@types/diff@7.0.1", "", {}, "sha512-R/BHQFripuhW6XPXy05hIvXJQdQ4540KnTvEFHSLjXfHYM41liOLKgIJEyYYiQe796xpaMHfe4Uj/p7Uvng2vA=="],
130 |
131 | "@types/estree": ["@types/estree@1.0.6", "", {}, "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw=="],
132 |
133 | "@types/js-yaml": ["@types/js-yaml@4.0.9", "", {}, "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg=="],
134 |
135 | "@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
136 |
137 | "@types/node": ["@types/node@22.13.4", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg=="],
138 |
139 | "@types/resolve": ["@types/resolve@1.20.2", "", {}, "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q=="],
140 |
141 | "@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
142 |
143 | "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.24.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.10.0", "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/type-utils": "8.24.1", "@typescript-eslint/utils": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-ll1StnKtBigWIGqvYDVuDmXJHVH4zLVot1yQ4fJtLpL7qacwkxJc1T0bptqw+miBQ/QfUbhl1TcQ4accW5KUyA=="],
144 |
145 | "@typescript-eslint/parser": ["@typescript-eslint/parser@8.24.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/types": "8.24.1", "@typescript-eslint/typescript-estree": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-Tqoa05bu+t5s8CTZFaGpCH2ub3QeT9YDkXbPd3uQ4SfsLoh1/vv2GEYAioPoxCWJJNsenXlC88tRjwoHNts1oQ=="],
146 |
147 | "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1" } }, "sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q=="],
148 |
149 | "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.24.1", "", { "dependencies": { "@typescript-eslint/typescript-estree": "8.24.1", "@typescript-eslint/utils": "8.24.1", "debug": "^4.3.4", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-/Do9fmNgCsQ+K4rCz0STI7lYB4phTtEXqqCAs3gZW0pnK7lWNkvWd5iW545GSmApm4AzmQXmSqXPO565B4WVrw=="],
150 |
151 | "@typescript-eslint/types": ["@typescript-eslint/types@8.24.1", "", {}, "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A=="],
152 |
153 | "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.8.0" } }, "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg=="],
154 |
155 | "@typescript-eslint/utils": ["@typescript-eslint/utils@8.24.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/types": "8.24.1", "@typescript-eslint/typescript-estree": "8.24.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ=="],
156 |
157 | "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "eslint-visitor-keys": "^4.2.0" } }, "sha512-EwVHlp5l+2vp8CoqJm9KikPZgi3gbdZAtabKT9KPShGeOcJhsv4Zdo3oc8T8I0uKEmYoU4ItyxbptjF08enaxg=="],
158 |
159 | "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
160 |
161 | "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
162 |
163 | "ajv": ["ajv@6.12.6", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="],
164 |
165 | "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
166 |
167 | "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
168 |
169 | "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
170 |
171 | "brace-expansion": ["brace-expansion@1.1.11", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA=="],
172 |
173 | "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
174 |
175 | "bun-types": ["bun-types@1.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg=="],
176 |
177 | "callsites": ["callsites@3.1.0", "", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="],
178 |
179 | "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
180 |
181 | "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
182 |
183 | "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
184 |
185 | "colors": ["colors@1.4.0", "", {}, "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA=="],
186 |
187 | "commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="],
188 |
189 | "commondir": ["commondir@1.0.1", "", {}, "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="],
190 |
191 | "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
192 |
193 | "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
194 |
195 | "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="],
196 |
197 | "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
198 |
199 | "deepmerge": ["deepmerge@4.3.1", "", {}, "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A=="],
200 |
201 | "diff": ["diff@7.0.0", "", {}, "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw=="],
202 |
203 | "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
204 |
205 | "eslint": ["eslint@9.20.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.19.0", "@eslint/core": "^0.11.0", "@eslint/eslintrc": "^3.2.0", "@eslint/js": "9.20.0", "@eslint/plugin-kit": "^0.2.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.1", "@types/estree": "^1.0.6", "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.2.0", "eslint-visitor-keys": "^4.2.0", "espree": "^10.3.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g=="],
206 |
207 | "eslint-config-prettier": ["eslint-config-prettier@10.0.1", "", { "peerDependencies": { "eslint": ">=7.0.0" }, "bin": { "eslint-config-prettier": "build/bin/cli.js" } }, "sha512-lZBts941cyJyeaooiKxAtzoPHTN+GbQTJFAIdQbRhA4/8whaAraEh47Whw/ZFfrjNSnlAxqfm9i0XVAEkULjCw=="],
208 |
209 | "eslint-plugin-prettier": ["eslint-plugin-prettier@5.2.3", "", { "dependencies": { "prettier-linter-helpers": "^1.0.0", "synckit": "^0.9.1" }, "peerDependencies": { "@types/eslint": ">=8.0.0", "eslint": ">=8.0.0", "eslint-config-prettier": "*", "prettier": ">=3.0.0" }, "optionalPeers": ["@types/eslint", "eslint-config-prettier"] }, "sha512-qJ+y0FfCp/mQYQ/vWQ3s7eUlFEL4PyKfAJxsnYTJ4YT73nsJBWqmEpFryxV9OeUiqmsTsYJ5Y+KDNaeP31wrRw=="],
210 |
211 | "eslint-scope": ["eslint-scope@8.2.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A=="],
212 |
213 | "eslint-visitor-keys": ["eslint-visitor-keys@4.2.0", "", {}, "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw=="],
214 |
215 | "espree": ["espree@10.3.0", "", { "dependencies": { "acorn": "^8.14.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.0" } }, "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg=="],
216 |
217 | "esquery": ["esquery@1.6.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg=="],
218 |
219 | "esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
220 |
221 | "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="],
222 |
223 | "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="],
224 |
225 | "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="],
226 |
227 | "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
228 |
229 | "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="],
230 |
231 | "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="],
232 |
233 | "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="],
234 |
235 | "fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
236 |
237 | "fastq": ["fastq@1.19.0", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-7SFSRCNjBQIZH/xZR3iy5iQYR8aGBE0h3VG6/cwlbrpdciNYBMotQav8c1XI3HjHH+NikUpP53nPdlZSdWmFzA=="],
238 |
239 | "fdir": ["fdir@6.4.3", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-PMXmW2y1hDDfTSRc9gaXIuCCRpuoz3Kaz8cUelp3smouvfT632ozg2vrT6lJsHKKOF59YLbOGfAWGUcKEfRMQw=="],
240 |
241 | "file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
242 |
243 | "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
244 |
245 | "find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng=="],
246 |
247 | "flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw=="],
248 |
249 | "flatted": ["flatted@3.3.3", "", {}, "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg=="],
250 |
251 | "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
252 |
253 | "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
254 |
255 | "glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="],
256 |
257 | "globals": ["globals@14.0.0", "", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
258 |
259 | "graphemer": ["graphemer@1.4.0", "", {}, "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="],
260 |
261 | "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="],
262 |
263 | "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
264 |
265 | "ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="],
266 |
267 | "import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ=="],
268 |
269 | "imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
270 |
271 | "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
272 |
273 | "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
274 |
275 | "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
276 |
277 | "is-module": ["is-module@1.0.0", "", {}, "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g=="],
278 |
279 | "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
280 |
281 | "is-reference": ["is-reference@1.2.1", "", { "dependencies": { "@types/estree": "*" } }, "sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ=="],
282 |
283 | "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
284 |
285 | "js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
286 |
287 | "json-buffer": ["json-buffer@3.0.1", "", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
288 |
289 | "json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
290 |
291 | "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
292 |
293 | "keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="],
294 |
295 | "levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
296 |
297 | "locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
298 |
299 | "lodash": ["lodash@4.17.21", "", {}, "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="],
300 |
301 | "lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
302 |
303 | "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="],
304 |
305 | "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="],
306 |
307 | "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="],
308 |
309 | "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
310 |
311 | "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
312 |
313 | "natural-compare": ["natural-compare@1.4.0", "", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
314 |
315 | "node-addon-api": ["node-addon-api@8.1.0", "", {}, "sha512-yBY+qqWSv3dWKGODD6OGE6GnTX7Q2r+4+DfpqxHSHh8x0B4EKP9+wVGLS6U/AM1vxSNNmUEuIV5EGhYwPpfOwQ=="],
316 |
317 | "node-gyp-build": ["node-gyp-build@4.8.2", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-test": "build-test.js", "node-gyp-build-optional": "optional.js" } }, "sha512-IRUxE4BVsHWXkV/SFOut4qTlagw2aM8T5/vnTsmrHJvVoKueJHRc/JaFND7QDDc61kLYUJ6qlZM3sqTSyx2dTw=="],
318 |
319 | "ohm-js": ["ohm-js@17.1.0", "", {}, "sha512-xc3B5dgAjTBQGHaH7B58M2Pmv6WvzrJ/3/7LeUzXNg0/sY3jQPdSd/S2SstppaleO77rifR1tyhdfFGNIwxf2Q=="],
320 |
321 | "optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
322 |
323 | "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
324 |
325 | "p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="],
326 |
327 | "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="],
328 |
329 | "path-exists": ["path-exists@4.0.0", "", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="],
330 |
331 | "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
332 |
333 | "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
334 |
335 | "picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
336 |
337 | "prelude-ls": ["prelude-ls@1.2.1", "", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
338 |
339 | "prettier": ["prettier@3.5.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg=="],
340 |
341 | "prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="],
342 |
343 | "punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
344 |
345 | "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="],
346 |
347 | "resolve": ["resolve@1.22.10", "", { "dependencies": { "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w=="],
348 |
349 | "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
350 |
351 | "reusify": ["reusify@1.0.4", "", {}, "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="],
352 |
353 | "rollup": ["rollup@4.34.8", "", { "dependencies": { "@types/estree": "1.0.6" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.34.8", "@rollup/rollup-android-arm64": "4.34.8", "@rollup/rollup-darwin-arm64": "4.34.8", "@rollup/rollup-darwin-x64": "4.34.8", "@rollup/rollup-freebsd-arm64": "4.34.8", "@rollup/rollup-freebsd-x64": "4.34.8", "@rollup/rollup-linux-arm-gnueabihf": "4.34.8", "@rollup/rollup-linux-arm-musleabihf": "4.34.8", "@rollup/rollup-linux-arm64-gnu": "4.34.8", "@rollup/rollup-linux-arm64-musl": "4.34.8", "@rollup/rollup-linux-loongarch64-gnu": "4.34.8", "@rollup/rollup-linux-powerpc64le-gnu": "4.34.8", "@rollup/rollup-linux-riscv64-gnu": "4.34.8", "@rollup/rollup-linux-s390x-gnu": "4.34.8", "@rollup/rollup-linux-x64-gnu": "4.34.8", "@rollup/rollup-linux-x64-musl": "4.34.8", "@rollup/rollup-win32-arm64-msvc": "4.34.8", "@rollup/rollup-win32-ia32-msvc": "4.34.8", "@rollup/rollup-win32-x64-msvc": "4.34.8", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ=="],
354 |
355 | "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="],
356 |
357 | "semver": ["semver@7.7.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA=="],
358 |
359 | "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
360 |
361 | "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
362 |
363 | "strip-json-comments": ["strip-json-comments@3.1.1", "", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="],
364 |
365 | "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
366 |
367 | "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
368 |
369 | "synckit": ["synckit@0.9.2", "", { "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" } }, "sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw=="],
370 |
371 | "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
372 |
373 | "tree-sitter": ["tree-sitter@0.22.4", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg=="],
374 |
375 | "ts-api-utils": ["ts-api-utils@2.0.1", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-dnlgjFSVetynI8nzgJ+qF62efpglpWRk8isUEWZGWlJYySCTD6aKvbUDu+zbPeDakk3bg5H4XpitHukgfL1m9w=="],
376 |
377 | "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
378 |
379 | "type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
380 |
381 | "typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
382 |
383 | "typescript-eslint": ["typescript-eslint@8.24.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.24.1", "@typescript-eslint/parser": "8.24.1", "@typescript-eslint/utils": "8.24.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-cw3rEdzDqBs70TIcb0Gdzbt6h11BSs2pS0yaq7hDWDBtCCSei1pPSUXE9qUdQ/Wm9NgFg8mKtMt1b8fTHIl1jA=="],
384 |
385 | "undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
386 |
387 | "uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="],
388 |
389 | "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
390 |
391 | "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="],
392 |
393 | "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="],
394 |
395 | "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
396 |
397 | "@eslint/plugin-kit/@eslint/core": ["@eslint/core@0.12.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg=="],
398 |
399 | "@humanfs/node/@humanwhocodes/retry": ["@humanwhocodes/retry@0.3.1", "", {}, "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA=="],
400 |
401 | "@typescript-eslint/typescript-estree/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="],
402 |
403 | "fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
404 |
405 | "micromatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
406 |
407 | "tree-sitter/node-addon-api": ["node-addon-api@8.3.1", "", {}, "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA=="],
408 |
409 | "tree-sitter/node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
410 |
411 | "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@2.0.1", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA=="],
412 | }
413 | }
414 |
--------------------------------------------------------------------------------
/dist/.gitignore:
--------------------------------------------------------------------------------
1 | *
2 | !.gitignore
3 |
--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------
1 | import globals from "globals";
2 | import pluginJs from "@eslint/js";
3 | import tseslint from "typescript-eslint";
4 | import * as eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
5 |
6 | export default [
7 | { files: ["**/*.{js,mjs,cjs,ts}"] },
8 | { files: ["**/*.js"], languageOptions: { sourceType: "commonjs" } },
9 | { languageOptions: { globals: globals.browser } },
10 | pluginJs.configs.recommended,
11 | ...tseslint.configs.recommended,
12 | eslintPluginPrettierRecommended.default,
13 | {
14 | ignores: ["dist/**/*", "web/**/*", "node_modules/**/*"],
15 | },
16 | ];
17 |
--------------------------------------------------------------------------------
/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mistweaverco/kulala-fmt/304929661c3aa218f1903376c6db5153b4ee443f/logo.png
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@mistweaverco/kulala-fmt",
3 | "version": "2.10.0",
4 | "type": "module",
5 | "scripts": {
6 | "build": "rollup -c",
7 | "lint": "eslint ."
8 | },
9 | "bin": {
10 | "kulala-fmt": "dist/cli.cjs"
11 | },
12 | "files": [
13 | "dist/cli.cjs"
14 | ],
15 | "publishConfig": {
16 | "access": "public"
17 | },
18 | "dependencies": {
19 | "@mistweaverco/tree-sitter-kulala": "1.9.0",
20 | "tree-sitter": "0.22.4",
21 | "prettier": "^3.5.2"
22 | },
23 | "devDependencies": {
24 | "@rollup/plugin-commonjs": "^28.0.2",
25 | "@rollup/plugin-json": "^6.1.0",
26 | "@rollup/plugin-node-resolve": "^16.0.0",
27 | "@rollup/plugin-typescript": "^12.1.2",
28 | "@types/bun": "latest",
29 | "@types/diff": "^7.0.1",
30 | "@types/js-yaml": "^4.0.9",
31 | "@typescript-eslint/eslint-plugin": "^8.24.1",
32 | "@typescript-eslint/parser": "^8.24.1",
33 | "colors": "^1.4.0",
34 | "commander": "^13.1.0",
35 | "diff": "^7.0.0",
36 | "eslint": "^9.20.1",
37 | "eslint-config-prettier": "^10.0.1",
38 | "eslint-plugin-prettier": "^5.2.3",
39 | "js-yaml": "^4.1.0",
40 | "lodash": "^4.17.21",
41 | "ohm-js": "^17.1.0",
42 | "rollup": "^4.34.8",
43 | "tslib": "^2.8.1",
44 | "typescript-eslint": "^8.24.1"
45 | },
46 | "peerDependencies": {
47 | "typescript": "^5.7.3"
48 | },
49 | "trustedDependencies": [
50 | "@mistweaverco/tree-sitter-kulala"
51 | ]
52 | }
53 |
--------------------------------------------------------------------------------
/rollup.config.js:
--------------------------------------------------------------------------------
1 | import json from "@rollup/plugin-json";
2 | import typescript from "@rollup/plugin-typescript";
3 | import resolve from "@rollup/plugin-node-resolve";
4 | import commonjs from "@rollup/plugin-commonjs";
5 | const config = [
6 | {
7 | input: "src/index.ts",
8 | output: {
9 | file: "dist/cli.cjs",
10 | format: "cjs",
11 | sourcemap: false,
12 | },
13 | external: ["tree-sitter", "@mistweaverco/tree-sitter-kulala", "prettier"],
14 | plugins: [
15 | json(),
16 | commonjs(),
17 | typescript({
18 | tsconfig: "./tsconfig.json",
19 | noForceEmit: true,
20 | }),
21 | resolve({
22 | preferBuiltins: true,
23 | }),
24 | ],
25 | },
26 | ];
27 | export default config;
28 |
--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------
1 | import pkg from "./../package.json";
2 | import { Command } from "commander";
3 | import { check, format, convert } from "./lib/parser";
4 | import { configparser } from "./lib/configparser";
5 | const program = new Command();
6 |
7 | program
8 | .name("kulala-fmt")
9 | .description(
10 | "An opinionated 🦄 .http and .rest 🐼 files linter 💄 and formatter ⚡.",
11 | )
12 | .version(pkg.version);
13 |
14 | program
15 | .command("format")
16 | .description("Format files")
17 | .argument("[files]", "files to include", null)
18 | .option("--body", "also format the body", true)
19 | .option("--stdin", "read input from stdin, print output to stdout", false)
20 | .action(async (files, options) => {
21 | await format(files, options);
22 | });
23 |
24 | program
25 | .command("check")
26 | .description("Check if files are well formatted")
27 | .argument("[files]", "files to include", null)
28 | .option("-v, --verbose", "enable verbose mode", false)
29 | .option("--body", "also format the body", true)
30 | .option("--stdin", "read input from stdin", false)
31 | .action(async (files, options) => {
32 | await check(files, options);
33 | });
34 |
35 | program
36 | .command("convert")
37 | .description("Convert files to .http format")
38 | .argument("", "files to include")
39 | .option("--from ", "source format", "openapi")
40 | .option("--to ", "destination format", "http")
41 | .action(async (files, options) => {
42 | await convert(options, files);
43 | });
44 |
45 | program
46 | .command("init")
47 | .description("initialize a new kulala-fmt.yaml file")
48 | .action(() => {
49 | configparser.init();
50 | });
51 |
52 | program.parse();
53 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import "./cli";
4 |
--------------------------------------------------------------------------------
/src/lib/configparser/index.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import path from "path";
3 | import yaml from "js-yaml";
4 | import chalk from "chalk";
5 | import readline from "readline";
6 |
7 | const CONFIG_FILENAME = "kulala-fmt.yaml";
8 |
9 | export interface Config {
10 | defaults: {
11 | http_method: string;
12 | http_version: string;
13 | };
14 | }
15 |
16 | const DEFAULT_CONFIG: Config = {
17 | defaults: {
18 | http_method: "GET",
19 | http_version: "HTTP/1.1",
20 | },
21 | };
22 |
23 | const init = (): void => {
24 | const file = path.join(process.cwd(), CONFIG_FILENAME);
25 | const configHeader = `# yaml-language-server: $schema=https://fmt.getkulala.net/schema.json\n---\n`;
26 | if (fs.existsSync(file)) {
27 | console.log(chalk.red(`🦄 Config file already exists: ${file}`));
28 | const rl = readline.createInterface({
29 | input: process.stdin,
30 | output: process.stdout,
31 | });
32 | rl.question(
33 | `Do you want to overwrite the file? (y/N) `,
34 | (answer: string) => {
35 | if (answer.toLowerCase() === "y") {
36 | fs.writeFileSync(file, configHeader + yaml.dump(DEFAULT_CONFIG));
37 | console.log(chalk.green(`🦄 Config file written: ${file}`));
38 | } else if (answer.toLowerCase() === "n") {
39 | console.log(chalk.yellow("🦄 Exiting..."));
40 | } else {
41 | console.log(chalk.red("🦄 Invalid input"));
42 | }
43 | rl.close();
44 | },
45 | );
46 | } else {
47 | fs.writeFileSync(file, configHeader + yaml.dump(DEFAULT_CONFIG));
48 | console.log(chalk.green(`🦄 Config file written: ${file}`));
49 | }
50 | };
51 |
52 | const parse = (): Config => {
53 | const file = path.join(process.cwd(), CONFIG_FILENAME);
54 | if (!fs.existsSync(file)) {
55 | return DEFAULT_CONFIG;
56 | }
57 | const content = fs.readFileSync(file, "utf8");
58 | const json = yaml.load(content) as Config;
59 | return { ...DEFAULT_CONFIG, ...json };
60 | };
61 |
62 | export const configparser = {
63 | init,
64 | parse,
65 | };
66 |
--------------------------------------------------------------------------------
/src/lib/filewalker/index.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import * as path from "path";
3 | import chalk from "chalk";
4 |
5 | /**
6 | * Walks a directory recursively (or processes a single file) and returns an array of file paths
7 | * matching the given extensions.
8 | *
9 | * @param {string} inputPath The starting directory path or a file path.
10 | * @param {string[]} extensions An array of file extensions to filter by (e.g., ['.http', '.rest']).
11 | * @returns {string[]} An array of file paths matching the extensions.
12 | */
13 | export function fileWalker(inputPath: string, extensions: string[]): string[] {
14 | const filePaths: string[] = [];
15 | const absolutePath = path.resolve(inputPath);
16 | let stats: fs.Stats;
17 | try {
18 | stats = fs.lstatSync(absolutePath);
19 | } catch (err: unknown) {
20 | const error = err as Error;
21 | console.error(chalk.red(`Error: ${error.message}`));
22 | return filePaths;
23 | }
24 |
25 | // Define a root directory for computing relative paths.
26 | const rootDir: string = process.cwd();
27 | if (stats.isFile()) {
28 | const ext = path.extname(absolutePath).toLowerCase();
29 | if (extensions.includes(ext)) {
30 | // Return the file path relative to its parent directory.
31 | filePaths.push(path.relative(rootDir, absolutePath));
32 | }
33 | return filePaths;
34 | } else if (stats.isDirectory()) {
35 | // nothing to do here
36 | } else {
37 | // If it's neither a file nor a directory (e.g., a special file), return empty.
38 | return filePaths;
39 | }
40 |
41 | // Recursive directory walker.
42 | function walk(currentPath: string) {
43 | const files = fs.readdirSync(currentPath);
44 | for (const file of files) {
45 | const filePath = path.join(currentPath, file);
46 | const stats = fs.lstatSync(filePath);
47 |
48 | // Skip symbolic links
49 | if (stats.isSymbolicLink()) {
50 | continue;
51 | }
52 |
53 | if (stats.isDirectory()) {
54 | walk(filePath); // Recursive call for subdirectories
55 | } else if (stats.isFile()) {
56 | const ext = path.extname(filePath).toLowerCase();
57 | if (extensions.includes(ext)) {
58 | // Compute the relative path with respect to the root directory.
59 | filePaths.push(path.relative(rootDir, filePath));
60 | }
61 | }
62 | }
63 | }
64 |
65 | walk(rootDir);
66 | return filePaths;
67 | }
68 |
--------------------------------------------------------------------------------
/src/lib/parser/BrunoDocumentParser.ts:
--------------------------------------------------------------------------------
1 | import { BrunoToJSONParser } from "./BrunoToJson";
2 | import { readFileSync, readdirSync, statSync } from "fs";
3 | import { join, relative } from "path";
4 | import type { Document, Block, Header, Variable } from "./DocumentParser";
5 |
6 | interface BrunoCollection {
7 | version: string;
8 | name: string;
9 | type: "collection";
10 | ignore?: string[];
11 | }
12 |
13 | interface BrunoEnvironmentVars {
14 | vars?: Record;
15 | }
16 |
17 | interface EnvironmentInfo {
18 | name: string;
19 | vars: Record;
20 | }
21 |
22 | interface BrunoRequest {
23 | meta?: {
24 | name?: string;
25 | url?: string;
26 | };
27 | http?: {
28 | method?: string;
29 | url?: string;
30 | };
31 | headers?: BrunoHeader[];
32 | body?: {
33 | json?: string;
34 | graphql?: {
35 | query?: string;
36 | variables?: string;
37 | };
38 | formUrlEncoded?: Record;
39 | multipartForm?: Array<{
40 | name: string;
41 | value: string;
42 | type?: string;
43 | }>;
44 | };
45 | "script:pre-request"?: string;
46 | tests?: string;
47 | }
48 |
49 | interface ParseResult {
50 | documents: Document[];
51 | environmentNames: string[];
52 | collectionName: string;
53 | }
54 |
55 | interface BrunoHeader {
56 | name: string;
57 | value: string;
58 | enabled?: boolean;
59 | }
60 |
61 | let collectionInfo: BrunoCollection | undefined;
62 |
63 | export class BrunoDocumentParser {
64 | private parseEnvironmentBruFile(content: string): BrunoEnvironmentVars {
65 | const lines = content.split("\n");
66 | const environment: BrunoEnvironmentVars = { vars: {} };
67 | let isInVarsBlock = false;
68 |
69 | for (const line of lines) {
70 | const trimmedLine = line.trim();
71 |
72 | if (!trimmedLine) continue;
73 |
74 | if (trimmedLine === "vars {") {
75 | isInVarsBlock = true;
76 | continue;
77 | } else if (trimmedLine === "}" && isInVarsBlock) {
78 | isInVarsBlock = false;
79 | continue;
80 | }
81 |
82 | if (isInVarsBlock && environment.vars) {
83 | const [key, ...valueParts] = trimmedLine
84 | .split(":")
85 | .map((part) => part.trim());
86 | const value = valueParts.join(":").replace(/^"(.*)"$/, "$1"); // Remove quotes if present
87 | if (key && value) {
88 | environment.vars[key] = value;
89 | }
90 | }
91 | }
92 |
93 | return environment;
94 | }
95 |
96 | private buildRequestBlock(
97 | bruRequest: BrunoRequest,
98 | folderPath: string,
99 | ): Block {
100 | const request = {
101 | method: (bruRequest.http?.method || "POST").toUpperCase(),
102 | url: bruRequest.http?.url || bruRequest.meta?.url || "",
103 | httpVersion: "HTTP/1.1",
104 | headers: [] as Header[],
105 | body: null as string | null,
106 | };
107 |
108 | const block: Block = {
109 | requestSeparator: {
110 | text: null,
111 | },
112 | metadata: [],
113 | comments: [],
114 | request,
115 | preRequestScripts: [],
116 | postRequestScripts: [],
117 | responseRedirect: null,
118 | };
119 |
120 | // Add folder path and name as comments
121 | if (folderPath) {
122 | block.comments.push(`# Folder: ${folderPath}\n`);
123 | }
124 | if (bruRequest.meta?.name) {
125 | block.comments.push(`# ${bruRequest.meta.name}\n`);
126 | block.metadata.push({
127 | key: "name",
128 | value: bruRequest.meta.name.replace(/\s+/g, "_").toUpperCase(),
129 | });
130 | }
131 |
132 | // Handle headers - they come as array of { name, value, enabled }
133 | if (Array.isArray(bruRequest.headers)) {
134 | bruRequest.headers
135 | .filter((header) => header.enabled !== false)
136 | .forEach((header) => {
137 | request.headers.push({
138 | key: header.name,
139 | value: header.value,
140 | });
141 | });
142 | }
143 |
144 | // Handle GraphQL specific headers and body
145 | if (bruRequest.body?.graphql) {
146 | request.headers.push(
147 | { key: "Accept", value: "application/json" },
148 | { key: "Content-Type", value: "application/json" },
149 | { key: "X-REQUEST-TYPE", value: "GraphQL" },
150 | );
151 |
152 | const query = bruRequest.body.graphql.query;
153 | const variables = bruRequest.body.graphql.variables;
154 |
155 | if (query) {
156 | request.body = query;
157 | if (variables) {
158 | request.body += "\n\n" + variables;
159 | }
160 | }
161 | } else if (bruRequest.body?.json) {
162 | request.body = bruRequest.body.json;
163 |
164 | if (
165 | !request.headers.some((h) => h.key.toLowerCase() === "content-type")
166 | ) {
167 | request.headers.push({
168 | key: "Content-Type",
169 | value: "application/json",
170 | });
171 | }
172 | } else if (bruRequest.body?.formUrlEncoded) {
173 | const formEntries = bruRequest.body.formUrlEncoded;
174 | const formParts: string[] = [];
175 |
176 | // Log the form data for debugging
177 | console.log("Form data:", formEntries);
178 |
179 | if (Array.isArray(formEntries)) {
180 | formEntries
181 | .filter((entry) => entry.enabled !== false)
182 | .forEach((entry) => {
183 | if (
184 | entry.name &&
185 | entry.value !== undefined &&
186 | entry.value !== null
187 | ) {
188 | const value = String(entry.value);
189 | // Don't encode {{variables}}
190 | const encodedValue = value.match(/^{{.*}}$/)
191 | ? value
192 | : encodeURIComponent(value);
193 |
194 | formParts.push(
195 | `${encodeURIComponent(entry.name)}=${encodedValue}`,
196 | );
197 | }
198 | });
199 | }
200 |
201 | request.body = formParts.join("&");
202 |
203 | request.headers.push({
204 | key: "Content-Type",
205 | value: "application/x-www-form-urlencoded",
206 | });
207 | } else if (bruRequest.body?.multipartForm) {
208 | const boundary =
209 | "----WebKitFormBoundary" + Math.random().toString(36).slice(2);
210 | const parts: string[] = [];
211 |
212 | bruRequest.body.multipartForm.forEach((field) => {
213 | parts.push(
214 | `--${boundary}\r\n` +
215 | `Content-Disposition: form-data; name="${field.name}"\r\n` +
216 | (field.type ? `Content-Type: ${field.type}\r\n` : "") +
217 | `\r\n${field.value}\r\n`,
218 | );
219 | });
220 | parts.push(`--${boundary}--\r\n`);
221 |
222 | request.body = parts.join("");
223 | request.headers.push({
224 | key: "Content-Type",
225 | value: `multipart/form-data; boundary=${boundary}`,
226 | });
227 | }
228 |
229 | // Handle scripts
230 | if (bruRequest["script:pre-request"]) {
231 | block.preRequestScripts.push({
232 | script: bruRequest["script:pre-request"].trim(),
233 | inline: true,
234 | });
235 | }
236 |
237 | if (bruRequest.tests) {
238 | block.postRequestScripts.push({
239 | script: bruRequest.tests.trim(),
240 | inline: true,
241 | });
242 | }
243 |
244 | return block;
245 | }
246 |
247 | private processDirectory(dirPath: string): Document {
248 | const document: Document = {
249 | variables: [],
250 | blocks: [],
251 | };
252 |
253 | const items = readdirSync(dirPath);
254 |
255 | for (const item of items) {
256 | const fullPath = join(dirPath, item);
257 | const stat = statSync(fullPath);
258 |
259 | // Skip the environments directory completely
260 | if (item === "environments") {
261 | continue;
262 | }
263 |
264 | if (stat.isDirectory()) {
265 | const subDocument = this.processDirectory(fullPath);
266 | document.blocks.push(...subDocument.blocks);
267 | document.variables.push(...subDocument.variables);
268 | } else if (item.endsWith(".bru")) {
269 | const content = readFileSync(fullPath, "utf-8");
270 | const bruRequest = BrunoToJSONParser.parse(content) as BrunoRequest;
271 | const relativePath = relative(dirPath, fullPath).replace(".bru", "");
272 |
273 | const block = this.buildRequestBlock(bruRequest, relativePath);
274 | document.blocks.push(block);
275 | }
276 | }
277 |
278 | return document;
279 | }
280 |
281 | private getEnvironments(dirPath: string): EnvironmentInfo[] {
282 | const environments: EnvironmentInfo[] = [];
283 | const environmentsDir = join(dirPath, "environments");
284 |
285 | if (statSync(environmentsDir, { throwIfNoEntry: false })?.isDirectory()) {
286 | const envFiles = readdirSync(environmentsDir);
287 |
288 | for (const file of envFiles) {
289 | if (file.endsWith(".bru")) {
290 | const envContent = readFileSync(join(environmentsDir, file), "utf-8");
291 | const environment = this.parseEnvironmentBruFile(envContent);
292 |
293 | environments.push({
294 | name: file.replace(".bru", ""),
295 | vars: environment.vars || {},
296 | });
297 | }
298 | }
299 | }
300 |
301 | return environments;
302 | }
303 |
304 | private readCollectionInfo(dirPath: string): BrunoCollection | undefined {
305 | try {
306 | const brunoJsonPath = join(dirPath, "bruno.json");
307 | const content = readFileSync(brunoJsonPath, "utf-8");
308 | return JSON.parse(content) as BrunoCollection;
309 | } catch {
310 | return undefined;
311 | }
312 | }
313 |
314 | parse(collectionPath: string): ParseResult {
315 | // Read collection info first
316 | collectionInfo = this.readCollectionInfo(collectionPath);
317 | const environments = this.getEnvironments(collectionPath);
318 |
319 | // If no environments found, create a default document
320 | if (environments.length === 0) {
321 | return {
322 | documents: [this.processDirectory(collectionPath)],
323 | environmentNames: ["default"],
324 | collectionName: collectionInfo?.name || "bruno-collection",
325 | };
326 | }
327 |
328 | // Create a document for each environment
329 | const documents = environments.map((env) => {
330 | const doc = this.processDirectory(collectionPath);
331 | // Add environment variables to the document
332 | doc.variables = Object.entries(env.vars).map(
333 | ([key, value]): Variable => ({
334 | key,
335 | value,
336 | }),
337 | );
338 | return doc;
339 | });
340 |
341 | return {
342 | documents,
343 | environmentNames: environments.map((env) => env.name),
344 | collectionName: collectionInfo?.name || "bruno-collection",
345 | };
346 | }
347 | }
348 |
--------------------------------------------------------------------------------
/src/lib/parser/BrunoToJson.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | // @ts-nocheck
3 | import * as ohm from "ohm-js";
4 | import * as _ from "lodash";
5 | const outdentString = (str: string) => {
6 | const match = str.match(/^[ \t]*(?=\S)/gm);
7 | if (!match) {
8 | return str;
9 | }
10 | const indent = Math.min(...match.map((el) => el.length));
11 | const regexp = new RegExp(`^[ \\t]{${indent}}`, "gm");
12 | return indent > 0 ? str.replace(regexp, "") : str;
13 | };
14 |
15 | /**
16 | * A Bru file is made up of blocks.
17 | * There are two types of blocks
18 | *
19 | * 1. Dictionary Blocks - These are blocks that have key value pairs
20 | * ex:
21 | * headers {
22 | * content-type: application/json
23 | * }
24 | *
25 | * 2. Text Blocks - These are blocks that have text
26 | * ex:
27 | * body:json {
28 | * {
29 | * "username": "John Nash",
30 | * "password": "governingdynamics
31 | * }
32 | *
33 | */
34 | const grammar = ohm.grammar(`Bru {
35 | BruFile = (meta | http | query | params | headers | auths | bodies | varsandassert | script | tests | docs)*
36 | auths = authawsv4 | authbasic | authbearer | authdigest | authNTLM | authOAuth2 | authwsse | authapikey
37 | bodies = bodyjson | bodytext | bodyxml | bodysparql | bodygraphql | bodygraphqlvars | bodyforms | body
38 | bodyforms = bodyformurlencoded | bodymultipart | bodyfile
39 | params = paramspath | paramsquery
40 |
41 | nl = "\\r"? "\\n"
42 | st = " " | "\\t"
43 | stnl = st | nl
44 | tagend = nl "}"
45 | optionalnl = ~tagend nl
46 | keychar = ~(tagend | st | nl | ":") any
47 | valuechar = ~(nl | tagend) any
48 |
49 | // Multiline text block surrounded by '''
50 | multilinetextblockdelimiter = "'''"
51 | multilinetextblock = multilinetextblockdelimiter (~multilinetextblockdelimiter any)* multilinetextblockdelimiter
52 |
53 | // Dictionary Blocks
54 | dictionary = st* "{" pairlist? tagend
55 | pairlist = optionalnl* pair (~tagend stnl* pair)* (~tagend space)*
56 | pair = st* key st* ":" st* value st*
57 | key = keychar*
58 | value = multilinetextblock | valuechar*
59 |
60 | // Dictionary for Assert Block
61 | assertdictionary = st* "{" assertpairlist? tagend
62 | assertpairlist = optionalnl* assertpair (~tagend stnl* assertpair)* (~tagend space)*
63 | assertpair = st* assertkey st* ":" st* value st*
64 | assertkey = ~tagend assertkeychar*
65 | assertkeychar = ~(tagend | nl | ":") any
66 |
67 | // Text Blocks
68 | textblock = textline (~tagend nl textline)*
69 | textline = textchar*
70 | textchar = ~nl any
71 |
72 | meta = "meta" dictionary
73 |
74 | http = get | post | put | delete | patch | options | head | connect | trace
75 | get = "get" dictionary
76 | post = "post" dictionary
77 | put = "put" dictionary
78 | delete = "delete" dictionary
79 | patch = "patch" dictionary
80 | options = "options" dictionary
81 | head = "head" dictionary
82 | connect = "connect" dictionary
83 | trace = "trace" dictionary
84 |
85 | headers = "headers" dictionary
86 |
87 | query = "query" dictionary
88 | paramspath = "params:path" dictionary
89 | paramsquery = "params:query" dictionary
90 |
91 | varsandassert = varsreq | varsres | assert
92 | varsreq = "vars:pre-request" dictionary
93 | varsres = "vars:post-response" dictionary
94 | assert = "assert" assertdictionary
95 |
96 | authawsv4 = "auth:awsv4" dictionary
97 | authbasic = "auth:basic" dictionary
98 | authbearer = "auth:bearer" dictionary
99 | authdigest = "auth:digest" dictionary
100 | authNTLM = "auth:ntlm" dictionary
101 | authOAuth2 = "auth:oauth2" dictionary
102 | authwsse = "auth:wsse" dictionary
103 | authapikey = "auth:apikey" dictionary
104 |
105 | body = "body" st* "{" nl* textblock tagend
106 | bodyjson = "body:json" st* "{" nl* textblock tagend
107 | bodytext = "body:text" st* "{" nl* textblock tagend
108 | bodyxml = "body:xml" st* "{" nl* textblock tagend
109 | bodysparql = "body:sparql" st* "{" nl* textblock tagend
110 | bodygraphql = "body:graphql" st* "{" nl* textblock tagend
111 | bodygraphqlvars = "body:graphql:vars" st* "{" nl* textblock tagend
112 |
113 | bodyformurlencoded = "body:form-urlencoded" dictionary
114 | bodymultipart = "body:multipart-form" dictionary
115 | bodyfile = "body:file" dictionary
116 |
117 | script = scriptreq | scriptres
118 | scriptreq = "script:pre-request" st* "{" nl* textblock tagend
119 | scriptres = "script:post-response" st* "{" nl* textblock tagend
120 | tests = "tests" st* "{" nl* textblock tagend
121 | docs = "docs" st* "{" nl* textblock tagend
122 | }`);
123 |
124 | const mapPairListToKeyValPairs = (pairList = [], parseEnabled = true) => {
125 | if (!pairList.length) {
126 | return [];
127 | }
128 | return _.map(pairList[0], (pair) => {
129 | let name = _.keys(pair)[0];
130 | let value = pair[name];
131 |
132 | if (!parseEnabled) {
133 | return {
134 | name,
135 | value,
136 | };
137 | }
138 |
139 | let enabled = true;
140 | if (name && name.length && name.charAt(0) === "~") {
141 | name = name.slice(1);
142 | enabled = false;
143 | }
144 |
145 | return {
146 | name,
147 | value,
148 | enabled,
149 | };
150 | });
151 | };
152 |
153 | const mapRequestParams = (pairList = [], type) => {
154 | if (!pairList.length) {
155 | return [];
156 | }
157 | return _.map(pairList[0], (pair) => {
158 | let name = _.keys(pair)[0];
159 | let value = pair[name];
160 | let enabled = true;
161 | if (name && name.length && name.charAt(0) === "~") {
162 | name = name.slice(1);
163 | enabled = false;
164 | }
165 |
166 | return {
167 | name,
168 | value,
169 | enabled,
170 | type,
171 | };
172 | });
173 | };
174 |
175 | const multipartExtractContentType = (pair) => {
176 | if (_.isString(pair.value)) {
177 | const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/);
178 | if (match != null && match.length > 2) {
179 | pair.value = match[1];
180 | pair.contentType = match[2];
181 | } else {
182 | pair.contentType = "";
183 | }
184 | }
185 | };
186 |
187 | const fileExtractContentType = (pair) => {
188 | if (_.isString(pair.value)) {
189 | const match = pair.value.match(/^(.*?)\s*@contentType\((.*?)\)\s*$/);
190 | if (match && match.length > 2) {
191 | pair.value = match[1].trim();
192 | pair.contentType = match[2].trim();
193 | } else {
194 | pair.contentType = "";
195 | }
196 | }
197 | };
198 |
199 | const mapPairListToKeyValPairsMultipart = (
200 | pairList = [],
201 | parseEnabled = true,
202 | ) => {
203 | const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);
204 |
205 | return pairs.map((pair) => {
206 | pair.type = "text";
207 | multipartExtractContentType(pair);
208 |
209 | if (pair.value.startsWith("@file(") && pair.value.endsWith(")")) {
210 | let filestr = pair.value.replace(/^@file\(/, "").replace(/\)$/, "");
211 | pair.type = "file";
212 | pair.value = filestr.split("|");
213 | }
214 |
215 | return pair;
216 | });
217 | };
218 |
219 | const mapPairListToKeyValPairsFile = (pairList = [], parseEnabled = true) => {
220 | const pairs = mapPairListToKeyValPairs(pairList, parseEnabled);
221 | return pairs.map((pair) => {
222 | fileExtractContentType(pair);
223 |
224 | if (pair.value.startsWith("@file(") && pair.value.endsWith(")")) {
225 | let filePath = pair.value.replace(/^@file\(/, "").replace(/\)$/, "");
226 | pair.filePath = filePath;
227 | pair.selected = pair.enabled;
228 |
229 | // Remove pair.value as it only contains the file path reference
230 | delete pair.value;
231 | // Remove pair.name as it is auto-generated (e.g., file1, file2, file3, etc.)
232 | delete pair.name;
233 | delete pair.enabled;
234 | }
235 |
236 | return pair;
237 | });
238 | };
239 |
240 | const concatArrays = (objValue, srcValue) => {
241 | if (_.isArray(objValue) && _.isArray(srcValue)) {
242 | return objValue.concat(srcValue);
243 | }
244 | };
245 |
246 | const mapPairListToKeyValPair = (pairList = []) => {
247 | if (!pairList || !pairList.length) {
248 | return {};
249 | }
250 |
251 | return _.merge({}, ...pairList[0]);
252 | };
253 |
254 | const sem = grammar.createSemantics().addAttribute("ast", {
255 | BruFile(tags) {
256 | if (!tags || !tags.ast || !tags.ast.length) {
257 | return {};
258 | }
259 |
260 | return _.reduce(
261 | tags.ast,
262 | (result, item) => {
263 | return _.mergeWith(result, item, concatArrays);
264 | },
265 | {},
266 | );
267 | },
268 | dictionary(_1, _2, pairlist, _3) {
269 | return pairlist.ast;
270 | },
271 | pairlist(_1, pair, _2, rest, _3) {
272 | return [pair.ast, ...rest.ast];
273 | },
274 | pair(_1, key, _2, _3, _4, value, _5) {
275 | let res = {};
276 | res[key.ast] = value.ast ? value.ast.trim() : "";
277 | return res;
278 | },
279 | key(chars) {
280 | return chars.sourceString ? chars.sourceString.trim() : "";
281 | },
282 | value(chars) {
283 | try {
284 | let isMultiline =
285 | chars.sourceString?.startsWith(`'''`) &&
286 | chars.sourceString?.endsWith(`'''`);
287 | if (isMultiline) {
288 | const multilineString = chars.sourceString?.replace(/^'''|'''$/g, "");
289 | return multilineString
290 | .split("\n")
291 | .map((line) => line.slice(4))
292 | .join("\n");
293 | }
294 | return chars.sourceString ? chars.sourceString.trim() : "";
295 | } catch (err) {
296 | console.error(err);
297 | }
298 | return chars.sourceString ? chars.sourceString.trim() : "";
299 | },
300 | assertdictionary(_1, _2, pairlist, _3) {
301 | return pairlist.ast;
302 | },
303 | assertpairlist(_1, pair, _2, rest, _3) {
304 | return [pair.ast, ...rest.ast];
305 | },
306 | assertpair(_1, key, _2, _3, _4, value, _5) {
307 | let res = {};
308 | res[key.ast] = value.ast ? value.ast.trim() : "";
309 | return res;
310 | },
311 | assertkey(chars) {
312 | return chars.sourceString ? chars.sourceString.trim() : "";
313 | },
314 | textblock(line, _1, rest) {
315 | return [line.ast, ...rest.ast].join("\n");
316 | },
317 | textline(chars) {
318 | return chars.sourceString;
319 | },
320 | textchar(char) {
321 | return char.sourceString;
322 | },
323 | nl(_1, _2) {
324 | return "";
325 | },
326 | st(_) {
327 | return "";
328 | },
329 | tagend(_1, _2) {
330 | return "";
331 | },
332 | _iter(...elements) {
333 | return elements.map((e) => e.ast);
334 | },
335 | meta(_1, dictionary) {
336 | let meta = mapPairListToKeyValPair(dictionary.ast);
337 |
338 | if (!meta.seq) {
339 | meta.seq = 1;
340 | }
341 |
342 | if (!meta.type) {
343 | meta.type = "http";
344 | }
345 |
346 | return {
347 | meta,
348 | };
349 | },
350 | get(_1, dictionary) {
351 | return {
352 | http: {
353 | method: "get",
354 | ...mapPairListToKeyValPair(dictionary.ast),
355 | },
356 | };
357 | },
358 | post(_1, dictionary) {
359 | return {
360 | http: {
361 | method: "post",
362 | ...mapPairListToKeyValPair(dictionary.ast),
363 | },
364 | };
365 | },
366 | put(_1, dictionary) {
367 | return {
368 | http: {
369 | method: "put",
370 | ...mapPairListToKeyValPair(dictionary.ast),
371 | },
372 | };
373 | },
374 | delete(_1, dictionary) {
375 | return {
376 | http: {
377 | method: "delete",
378 | ...mapPairListToKeyValPair(dictionary.ast),
379 | },
380 | };
381 | },
382 | patch(_1, dictionary) {
383 | return {
384 | http: {
385 | method: "patch",
386 | ...mapPairListToKeyValPair(dictionary.ast),
387 | },
388 | };
389 | },
390 | options(_1, dictionary) {
391 | return {
392 | http: {
393 | method: "options",
394 | ...mapPairListToKeyValPair(dictionary.ast),
395 | },
396 | };
397 | },
398 | head(_1, dictionary) {
399 | return {
400 | http: {
401 | method: "head",
402 | ...mapPairListToKeyValPair(dictionary.ast),
403 | },
404 | };
405 | },
406 | connect(_1, dictionary) {
407 | return {
408 | http: {
409 | method: "connect",
410 | ...mapPairListToKeyValPair(dictionary.ast),
411 | },
412 | };
413 | },
414 | query(_1, dictionary) {
415 | return {
416 | params: mapRequestParams(dictionary.ast, "query"),
417 | };
418 | },
419 | paramspath(_1, dictionary) {
420 | return {
421 | params: mapRequestParams(dictionary.ast, "path"),
422 | };
423 | },
424 | paramsquery(_1, dictionary) {
425 | return {
426 | params: mapRequestParams(dictionary.ast, "query"),
427 | };
428 | },
429 | headers(_1, dictionary) {
430 | return {
431 | headers: mapPairListToKeyValPairs(dictionary.ast),
432 | };
433 | },
434 | authawsv4(_1, dictionary) {
435 | const auth = mapPairListToKeyValPairs(dictionary.ast, false);
436 | const accessKeyIdKey = _.find(auth, { name: "accessKeyId" });
437 | const secretAccessKeyKey = _.find(auth, { name: "secretAccessKey" });
438 | const sessionTokenKey = _.find(auth, { name: "sessionToken" });
439 | const serviceKey = _.find(auth, { name: "service" });
440 | const regionKey = _.find(auth, { name: "region" });
441 | const profileNameKey = _.find(auth, { name: "profileName" });
442 | const accessKeyId = accessKeyIdKey ? accessKeyIdKey.value : "";
443 | const secretAccessKey = secretAccessKeyKey ? secretAccessKeyKey.value : "";
444 | const sessionToken = sessionTokenKey ? sessionTokenKey.value : "";
445 | const service = serviceKey ? serviceKey.value : "";
446 | const region = regionKey ? regionKey.value : "";
447 | const profileName = profileNameKey ? profileNameKey.value : "";
448 | return {
449 | auth: {
450 | awsv4: {
451 | accessKeyId,
452 | secretAccessKey,
453 | sessionToken,
454 | service,
455 | region,
456 | profileName,
457 | },
458 | },
459 | };
460 | },
461 | authbasic(_1, dictionary) {
462 | const auth = mapPairListToKeyValPairs(dictionary.ast, false);
463 | const usernameKey = _.find(auth, { name: "username" });
464 | const passwordKey = _.find(auth, { name: "password" });
465 | const username = usernameKey ? usernameKey.value : "";
466 | const password = passwordKey ? passwordKey.value : "";
467 | return {
468 | auth: {
469 | basic: {
470 | username,
471 | password,
472 | },
473 | },
474 | };
475 | },
476 | authbearer(_1, dictionary) {
477 | const auth = mapPairListToKeyValPairs(dictionary.ast, false);
478 | const tokenKey = _.find(auth, { name: "token" });
479 | const token = tokenKey ? tokenKey.value : "";
480 | return {
481 | auth: {
482 | bearer: {
483 | token,
484 | },
485 | },
486 | };
487 | },
488 | authdigest(_1, dictionary) {
489 | const auth = mapPairListToKeyValPairs(dictionary.ast, false);
490 | const usernameKey = _.find(auth, { name: "username" });
491 | const passwordKey = _.find(auth, { name: "password" });
492 | const username = usernameKey ? usernameKey.value : "";
493 | const password = passwordKey ? passwordKey.value : "";
494 | return {
495 | auth: {
496 | digest: {
497 | username,
498 | password,
499 | },
500 | },
501 | };
502 | },
503 | authNTLM(_1, dictionary) {
504 | const auth = mapPairListToKeyValPairs(dictionary.ast, false);
505 | const usernameKey = _.find(auth, { name: "username" });
506 | const passwordKey = _.find(auth, { name: "password" });
507 | const domainKey = _.find(auth, { name: "domain" });
508 |
509 | const username = usernameKey ? usernameKey.value : "";
510 | const password = passwordKey ? passwordKey.value : "";
511 | const domain = passwordKey ? domainKey.value : "";
512 |
513 | return {
514 | auth: {
515 | ntlm: {
516 | username,
517 | password,
518 | domain,
519 | },
520 | },
521 | };
522 | },
523 | authOAuth2(_1, dictionary) {
524 | const auth = mapPairListToKeyValPairs(dictionary.ast, false);
525 | const grantTypeKey = _.find(auth, { name: "grant_type" });
526 | const usernameKey = _.find(auth, { name: "username" });
527 | const passwordKey = _.find(auth, { name: "password" });
528 | const callbackUrlKey = _.find(auth, { name: "callback_url" });
529 | const authorizationUrlKey = _.find(auth, { name: "authorization_url" });
530 | const accessTokenUrlKey = _.find(auth, { name: "access_token_url" });
531 | const clientIdKey = _.find(auth, { name: "client_id" });
532 | const clientSecretKey = _.find(auth, { name: "client_secret" });
533 | const scopeKey = _.find(auth, { name: "scope" });
534 | const stateKey = _.find(auth, { name: "state" });
535 | const pkceKey = _.find(auth, { name: "pkce" });
536 | return {
537 | auth: {
538 | oauth2:
539 | grantTypeKey?.value && grantTypeKey?.value == "password"
540 | ? {
541 | grantType: grantTypeKey ? grantTypeKey.value : "",
542 | accessTokenUrl: accessTokenUrlKey
543 | ? accessTokenUrlKey.value
544 | : "",
545 | username: usernameKey ? usernameKey.value : "",
546 | password: passwordKey ? passwordKey.value : "",
547 | clientId: clientIdKey ? clientIdKey.value : "",
548 | clientSecret: clientSecretKey ? clientSecretKey.value : "",
549 | scope: scopeKey ? scopeKey.value : "",
550 | }
551 | : grantTypeKey?.value && grantTypeKey?.value == "authorization_code"
552 | ? {
553 | grantType: grantTypeKey ? grantTypeKey.value : "",
554 | callbackUrl: callbackUrlKey ? callbackUrlKey.value : "",
555 | authorizationUrl: authorizationUrlKey
556 | ? authorizationUrlKey.value
557 | : "",
558 | accessTokenUrl: accessTokenUrlKey
559 | ? accessTokenUrlKey.value
560 | : "",
561 | clientId: clientIdKey ? clientIdKey.value : "",
562 | clientSecret: clientSecretKey ? clientSecretKey.value : "",
563 | scope: scopeKey ? scopeKey.value : "",
564 | state: stateKey ? stateKey.value : "",
565 | pkce: pkceKey ? JSON.parse(pkceKey?.value || false) : false,
566 | }
567 | : grantTypeKey?.value &&
568 | grantTypeKey?.value == "client_credentials"
569 | ? {
570 | grantType: grantTypeKey ? grantTypeKey.value : "",
571 | accessTokenUrl: accessTokenUrlKey
572 | ? accessTokenUrlKey.value
573 | : "",
574 | clientId: clientIdKey ? clientIdKey.value : "",
575 | clientSecret: clientSecretKey ? clientSecretKey.value : "",
576 | scope: scopeKey ? scopeKey.value : "",
577 | }
578 | : {},
579 | },
580 | };
581 | },
582 | authwsse(_1, dictionary) {
583 | const auth = mapPairListToKeyValPairs(dictionary.ast, false);
584 |
585 | const userKey = _.find(auth, { name: "username" });
586 | const secretKey = _.find(auth, { name: "password" });
587 | const username = userKey ? userKey.value : "";
588 | const password = secretKey ? secretKey.value : "";
589 |
590 | return {
591 | auth: {
592 | wsse: {
593 | username,
594 | password,
595 | },
596 | },
597 | };
598 | },
599 | authapikey(_1, dictionary) {
600 | const auth = mapPairListToKeyValPairs(dictionary.ast, false);
601 |
602 | const findValueByName = (name) => {
603 | const item = _.find(auth, { name });
604 | return item ? item.value : "";
605 | };
606 |
607 | const key = findValueByName("key");
608 | const value = findValueByName("value");
609 | const placement = findValueByName("placement");
610 |
611 | return {
612 | auth: {
613 | apikey: {
614 | key,
615 | value,
616 | placement,
617 | },
618 | },
619 | };
620 | },
621 | bodyformurlencoded(_1, dictionary) {
622 | return {
623 | body: {
624 | formUrlEncoded: mapPairListToKeyValPairs(dictionary.ast),
625 | },
626 | };
627 | },
628 | bodymultipart(_1, dictionary) {
629 | return {
630 | body: {
631 | multipartForm: mapPairListToKeyValPairsMultipart(dictionary.ast),
632 | },
633 | };
634 | },
635 | bodyfile(_1, dictionary) {
636 | return {
637 | body: {
638 | file: mapPairListToKeyValPairsFile(dictionary.ast),
639 | },
640 | };
641 | },
642 | body(_1, _2, _3, _4, textblock, _5) {
643 | return {
644 | http: {
645 | body: "json",
646 | },
647 | body: {
648 | json: outdentString(textblock.sourceString),
649 | },
650 | };
651 | },
652 | bodyjson(_1, _2, _3, _4, textblock, _5) {
653 | return {
654 | body: {
655 | json: outdentString(textblock.sourceString),
656 | },
657 | };
658 | },
659 | bodytext(_1, _2, _3, _4, textblock, _5) {
660 | return {
661 | body: {
662 | text: outdentString(textblock.sourceString),
663 | },
664 | };
665 | },
666 | bodyxml(_1, _2, _3, _4, textblock, _5) {
667 | return {
668 | body: {
669 | xml: outdentString(textblock.sourceString),
670 | },
671 | };
672 | },
673 | bodysparql(_1, _2, _3, _4, textblock, _5) {
674 | return {
675 | body: {
676 | sparql: outdentString(textblock.sourceString),
677 | },
678 | };
679 | },
680 | bodygraphql(_1, _2, _3, _4, textblock, _5) {
681 | return {
682 | body: {
683 | graphql: {
684 | query: outdentString(textblock.sourceString),
685 | },
686 | },
687 | };
688 | },
689 | bodygraphqlvars(_1, _2, _3, _4, textblock, _5) {
690 | return {
691 | body: {
692 | graphql: {
693 | variables: outdentString(textblock.sourceString),
694 | },
695 | },
696 | };
697 | },
698 | varsreq(_1, dictionary) {
699 | const vars = mapPairListToKeyValPairs(dictionary.ast);
700 | _.each(vars, (v) => {
701 | let name = v.name;
702 | if (name && name.length && name.charAt(0) === "@") {
703 | v.name = name.slice(1);
704 | v.local = true;
705 | } else {
706 | v.local = false;
707 | }
708 | });
709 |
710 | return {
711 | vars: {
712 | req: vars,
713 | },
714 | };
715 | },
716 | varsres(_1, dictionary) {
717 | const vars = mapPairListToKeyValPairs(dictionary.ast);
718 | _.each(vars, (v) => {
719 | let name = v.name;
720 | if (name && name.length && name.charAt(0) === "@") {
721 | v.name = name.slice(1);
722 | v.local = true;
723 | } else {
724 | v.local = false;
725 | }
726 | });
727 |
728 | return {
729 | vars: {
730 | res: vars,
731 | },
732 | };
733 | },
734 | assert(_1, dictionary) {
735 | return {
736 | assertions: mapPairListToKeyValPairs(dictionary.ast),
737 | };
738 | },
739 | scriptreq(_1, _2, _3, _4, textblock, _5) {
740 | return {
741 | script: {
742 | req: outdentString(textblock.sourceString),
743 | },
744 | };
745 | },
746 | scriptres(_1, _2, _3, _4, textblock, _5) {
747 | return {
748 | script: {
749 | res: outdentString(textblock.sourceString),
750 | },
751 | };
752 | },
753 | tests(_1, _2, _3, _4, textblock, _5) {
754 | return {
755 | tests: outdentString(textblock.sourceString),
756 | };
757 | },
758 | docs(_1, _2, _3, _4, textblock, _5) {
759 | return {
760 | docs: outdentString(textblock.sourceString),
761 | };
762 | },
763 | });
764 |
765 | const parser = (input) => {
766 | const match = grammar.match(input);
767 |
768 | if (match.succeeded()) {
769 | return sem(match).ast;
770 | } else {
771 | throw new Error(match.message);
772 | }
773 | };
774 |
775 | export const BrunoToJSONParser = {
776 | parse: (input) => {
777 | return parser(input);
778 | },
779 | };
780 |
--------------------------------------------------------------------------------
/src/lib/parser/Diff.ts:
--------------------------------------------------------------------------------
1 | import "colors";
2 | import * as diff from "diff";
3 |
4 | export const Diff = (build: string, content: string) => {
5 | const d = diff.diffChars(content, build);
6 |
7 | d.forEach((part) => {
8 | // green for additions, red for deletions
9 | // with a black color
10 | const text = part.added
11 | ? part.value.bgGreen
12 | : part.removed
13 | ? part.value.bgRed
14 | : part.value;
15 | process.stderr.write(text);
16 | });
17 |
18 | console.log();
19 | };
20 |
--------------------------------------------------------------------------------
/src/lib/parser/DocumentBuilder.ts:
--------------------------------------------------------------------------------
1 | import * as prettier from "prettier";
2 | import type { Document, Block, Header } from "./DocumentParser";
3 |
4 | function headerToPascalCase(str: string) {
5 | return str
6 | .split("-")
7 | .map((word) => word.charAt(0).toUpperCase() + word.slice(1))
8 | .join("-");
9 | }
10 |
11 | const formatFormBody = (body: string): string => {
12 | body = body.replace(/\n/g, "").replace(/\s+/g, "");
13 | const parts = body.split("&");
14 | if (parts.length === 1) {
15 | return parts[0];
16 | }
17 | return parts.join("&\n").replace(/\n\s*\n/g, "\n");
18 | };
19 |
20 | // Get header value based on case insensitive key
21 | function getHeader(headers: Header[], key: string) {
22 | const header = headers.find(
23 | (header) => header.key.toLowerCase() === key.toLowerCase(),
24 | );
25 | return header?.value.toLowerCase();
26 | }
27 |
28 | // Helper function to split GraphQL body and variables
29 | function splitGraphQLBody(body: string): {
30 | query: string;
31 | variables: string | null;
32 | } {
33 | // Split on first occurrence of two newlines followed by a JSON-like structure
34 | const parts = body.split(/\n\s*\n(\s*{)/);
35 |
36 | if (parts.length >= 2) {
37 | // Rejoin the JSON part if it was split
38 | const variables = parts.slice(1).join("");
39 | return {
40 | query: parts[0].trim(),
41 | variables: variables.trim(),
42 | };
43 | }
44 |
45 | return {
46 | query: body.trim(),
47 | variables: null,
48 | };
49 | }
50 |
51 | const getFormatParser = (block: Block): null | "graphql" | "json" => {
52 | const headers = block.request?.headers || [];
53 | if (getHeader(headers, "x-request-type") === "graphql") {
54 | return "graphql";
55 | }
56 | if (getHeader(headers, "content-type") === "application/json") {
57 | return "json";
58 | }
59 | return null;
60 | };
61 |
62 | function replaceCommentPrefix(comment: string): string {
63 | return comment.replace(/^(\/\/)/, "#");
64 | }
65 |
66 | function preservePlaceholders(body: string) {
67 | const placeholderRegex = /(?();
69 | let replacedBody = body;
70 |
71 | replacedBody = replacedBody.replace(placeholderRegex, (match) => {
72 | const key = `__KULALA_FMT_PLACEHOLDER_${placeholders.size}__`;
73 | placeholders.set(key, match);
74 | return `"${key}"`;
75 | });
76 | replacedBody = replacedBody.replace(
77 | /__""__KULALA_FMT_PLACEHOLDER_/g,
78 | "____KULALA_FMT_PLACEHOLDER_",
79 | );
80 |
81 | return { replacedBody, placeholders };
82 | }
83 |
84 | function restorePlaceholders(
85 | formattedBody: string,
86 | placeholders: Map,
87 | ) {
88 | let restoredBody = formattedBody;
89 | const placeholdersLength = placeholders.size;
90 | let idx = 0;
91 | placeholders.forEach((original, key) => {
92 | let firstQuote = "";
93 | let lastQuote = "";
94 | if (idx === 0) {
95 | firstQuote = '"';
96 | }
97 | if (idx === placeholdersLength - 1) {
98 | lastQuote = '"';
99 | }
100 | restoredBody = restoredBody.replace(
101 | `${firstQuote}${key}${lastQuote}`,
102 | original,
103 | );
104 | idx++;
105 | });
106 | return restoredBody;
107 | }
108 |
109 | const build = async (
110 | document: Document,
111 | formatBody: boolean = true,
112 | ): Promise => {
113 | let output = "";
114 |
115 | for (const variable of document.variables) {
116 | output += `@${variable.key} = ${variable.value}\n`;
117 | }
118 | output += "\n";
119 |
120 | for (const block of document.blocks) {
121 | const requestSeparatorText = block.requestSeparator.text
122 | ? ` ${block.requestSeparator.text}`
123 | : "";
124 | output += `\n###${requestSeparatorText}\n\n`;
125 |
126 | if (block.comments.length > 0) {
127 | for (const comment of block.comments) {
128 | output += `${replaceCommentPrefix(comment)}`;
129 | }
130 | }
131 | if (block.preRequestScripts.length > 0) {
132 | for (const script of block.preRequestScripts) {
133 | output += `< ${script.script}\n`;
134 | }
135 | }
136 | if (block.metadata.length > 0) {
137 | for (const metadata of block.metadata) {
138 | output += `# @${metadata.key} ${metadata.value}\n`;
139 | }
140 | }
141 | if (block.request) {
142 | const formatParser = getFormatParser(block);
143 | output += `${block.request.method} ${block.request.url} ${block.request.httpVersion}\n`;
144 | for (const header of block.request.headers) {
145 | let headerKey = header.key;
146 | switch (block.request.httpVersion) {
147 | case "HTTP/1.0":
148 | case "HTTP/1.1":
149 | headerKey = headerToPascalCase(header.key);
150 | break;
151 | case "HTTP/2":
152 | case "HTTP/3":
153 | headerKey = header.key.toLowerCase();
154 | break;
155 | }
156 | output += `${headerKey}: ${header.value}\n`;
157 | }
158 | if (block.request.body) {
159 | let body = block.request.body.trim();
160 | if (formatBody) {
161 | if (formatParser === "graphql") {
162 | try {
163 | const { replacedBody, placeholders } = preservePlaceholders(body);
164 | const { query, variables } = splitGraphQLBody(replacedBody);
165 |
166 | const formattedQuery = await prettier.format(query, {
167 | parser: formatParser,
168 | });
169 |
170 | // Format the variables if they exist
171 | let formattedVariables = "";
172 | if (variables) {
173 | formattedVariables = await prettier.format(variables, {
174 | parser: "json",
175 | });
176 | }
177 | body = formattedQuery.trim();
178 | if (formattedVariables) {
179 | body += `\n\n${formattedVariables.trim()}`;
180 | }
181 | body = restorePlaceholders(body, placeholders).trim();
182 | } catch (err) {
183 | const error = err as Error;
184 | console.log(error.message);
185 | process.exit(1);
186 | }
187 | } else if (formatParser === "json") {
188 | try {
189 | const { replacedBody, placeholders } = preservePlaceholders(body);
190 |
191 | body = await prettier.format(replacedBody, {
192 | parser: formatParser,
193 | });
194 |
195 | body = restorePlaceholders(body, placeholders).trim();
196 | } catch (err) {
197 | const error = err as Error;
198 | console.log(error.message);
199 | process.exit(1);
200 | }
201 | } else if (
202 | getHeader(block.request.headers, "content-type") ===
203 | "application/x-www-form-urlencoded"
204 | ) {
205 | body = formatFormBody(body);
206 | }
207 | }
208 | output += `\n${body}\n`;
209 | }
210 | }
211 | if (block.postRequestScripts.length > 0) {
212 | for (const script of block.postRequestScripts) {
213 | output += `\n> ${script.script}\n`;
214 | }
215 | }
216 | if (block.responseRedirect) {
217 | output += `\n${block.responseRedirect.trim()}\n`;
218 | }
219 | }
220 | output = output.trim() + "\n";
221 | return output;
222 | };
223 |
224 | export const DocumentBuilder = {
225 | build,
226 | };
227 |
--------------------------------------------------------------------------------
/src/lib/parser/DocumentParser.ts:
--------------------------------------------------------------------------------
1 | import Parser, { SyntaxNode, type Language } from "tree-sitter";
2 | import Kulala from "@mistweaverco/tree-sitter-kulala";
3 | import { configparser } from "./../configparser";
4 |
5 | const config = configparser.parse();
6 |
7 | export interface Header {
8 | key: string;
9 | value: string;
10 | }
11 |
12 | interface Request {
13 | method: string;
14 | url: string;
15 | httpVersion: string;
16 | headers: Header[];
17 | body: string | null;
18 | }
19 |
20 | interface Metadata {
21 | key: string;
22 | value: string;
23 | }
24 |
25 | interface PreRequestScript {
26 | script: string;
27 | inline: boolean;
28 | }
29 |
30 | interface PostRequestScript {
31 | script: string;
32 | inline: boolean;
33 | }
34 |
35 | export interface Variable {
36 | key: string;
37 | value: string;
38 | }
39 |
40 | export interface BlockRequestSeparator {
41 | text: string | null;
42 | }
43 |
44 | export interface Block {
45 | requestSeparator: BlockRequestSeparator;
46 | metadata: Metadata[];
47 | comments: string[];
48 | request: Request | null;
49 | preRequestScripts: PreRequestScript[];
50 | postRequestScripts: PostRequestScript[];
51 | responseRedirect: string | null;
52 | }
53 |
54 | export interface Document {
55 | variables: Variable[];
56 | blocks: Block[];
57 | }
58 |
59 | const traverseNodes = (
60 | node: SyntaxNode,
61 | callback: (node: SyntaxNode) => void,
62 | ): void => {
63 | callback(node);
64 | node.children.forEach((child) => traverseNodes(child, callback));
65 | };
66 |
67 | const documentHasErrors = (node: SyntaxNode): boolean => {
68 | const recursiveNodeWalk = (
69 | node: SyntaxNode,
70 | callback: (node: SyntaxNode) => boolean,
71 | ): boolean => {
72 | if (callback(node)) {
73 | return true; // Return true if the callback returns true
74 | }
75 | for (const child of node.children) {
76 | if (recursiveNodeWalk(child, callback)) {
77 | return true; // Return true if a child node has an error
78 | }
79 | }
80 | return false; // Return false if no error is found in this subtree
81 | };
82 | return recursiveNodeWalk(node, (n) => {
83 | return n.hasError; // Return true if the node has an error
84 | });
85 | };
86 |
87 | // formatUrl takes a URL string and retuns an formatted string
88 | // with the URL parts separated by new lines
89 | // For example, given the URL `https://httpbin.org/get?foo=bar&baz=qux`
90 | // the function should return:
91 | // ```
92 | // https://httpbin.org/get
93 | // ?foo=bar
94 | // &baz=qux
95 | // ```
96 | const formatUrl = (url: string): string => {
97 | const parts = url.split("?");
98 | if (parts.length === 1) {
99 | return parts[0];
100 | }
101 | const [base, query] = parts;
102 | return `${base}?${query.split("&").join("\n &")}`.replace(/\n\s*\n/g, "\n");
103 | };
104 |
105 | // Returns a Document object if the content is valid .http syntax,
106 | // otherwise null.
107 | const parse = (content: string): Document | null => {
108 | const parser = new Parser();
109 | const language = Kulala as Language;
110 | parser.setLanguage(language);
111 |
112 | const tree = parser.parse(content);
113 | const documentNode = tree.rootNode;
114 |
115 | if (documentHasErrors(documentNode)) {
116 | return null;
117 | }
118 |
119 | const variables: Variable[] = [];
120 | const blocks: Block[] = [];
121 |
122 | const blockNodes = documentNode.children.filter(
123 | (node) => node.type === "section",
124 | );
125 |
126 | blockNodes.forEach((bn) => {
127 | const block: Block = {
128 | requestSeparator: {
129 | text: null,
130 | },
131 | metadata: [],
132 | comments: [],
133 | request: null,
134 | preRequestScripts: [],
135 | postRequestScripts: [],
136 | responseRedirect: null,
137 | };
138 |
139 | const headers: Header[] = [];
140 | let method: string = "";
141 | let url: string = "";
142 | let httpVersion: string = "";
143 | let body: string | null = null;
144 |
145 | bn.children.forEach((node) => {
146 | if (node.type === "pre_request_script") {
147 | let preRequestScript: PreRequestScript | null = null;
148 | node.children.forEach((child) => {
149 | switch (child.type) {
150 | case "script":
151 | preRequestScript = {
152 | script: child.text,
153 | inline: true,
154 | };
155 | break;
156 | case "path":
157 | preRequestScript = {
158 | script: child.text,
159 | inline: false,
160 | };
161 | break;
162 | }
163 | });
164 | if (preRequestScript) {
165 | block.preRequestScripts.push(preRequestScript);
166 | }
167 | }
168 | if (node.type === "request_separator") {
169 | // without text
170 | if (node.children.length === 0) {
171 | block.requestSeparator.text = null;
172 | }
173 | node.children.forEach((child) => {
174 | switch (child.type) {
175 | case "value":
176 | block.requestSeparator.text = child.text;
177 | break;
178 | }
179 | });
180 | }
181 | if (node.type === "comment") {
182 | // normal comment
183 | if (node.children.length === 0) {
184 | block.comments.push(node.text);
185 | }
186 | let md: Metadata | null = null;
187 | // metadata comments like `# @key value`
188 | node.children.forEach((child) => {
189 | switch (child.type) {
190 | case "identifier":
191 | md = {
192 | key: child.text,
193 | value: "",
194 | };
195 | break;
196 | case "value":
197 | if (md) {
198 | md.value = child.text;
199 | }
200 | break;
201 | }
202 | });
203 | if (md) {
204 | block.metadata.push(md);
205 | }
206 | }
207 |
208 | if (node.type === "request") {
209 | node.children.forEach((child) => {
210 | let postRequestScript: PostRequestScript | null = null;
211 | let parts, key, value;
212 | switch (child.type) {
213 | case "method":
214 | method = child.text.toUpperCase();
215 | break;
216 | case "target_url":
217 | url = formatUrl(child.text);
218 | break;
219 | case "http_version":
220 | httpVersion = child.text;
221 | break;
222 | case "header":
223 | // a header string is in the format `key: value`
224 | // for example, `Content-Type: application/json`
225 | parts = child.text.split(":");
226 | key = parts[0].trim();
227 | // also make sure to take all parts after the first colon
228 | value = parts.slice(1).join(":").trim();
229 | headers.push({ key, value });
230 | break;
231 | case "res_redirect":
232 | block.responseRedirect = child.text;
233 | break;
234 | case "res_handler_script":
235 | child.children.forEach((c) => {
236 | switch (c.type) {
237 | case "script":
238 | postRequestScript = {
239 | script: c.text,
240 | inline: true,
241 | };
242 | break;
243 | case "path":
244 | postRequestScript = {
245 | script: c.text,
246 | inline: false,
247 | };
248 | break;
249 | }
250 | });
251 | if (postRequestScript) {
252 | block.postRequestScripts.push(postRequestScript);
253 | }
254 | break;
255 | }
256 | if (child.type.endsWith("_body")) {
257 | body = child.text;
258 | }
259 | });
260 |
261 | if (method === "") {
262 | method = config.defaults.http_method;
263 | }
264 |
265 | if (httpVersion === "") {
266 | httpVersion = config.defaults.http_version;
267 | }
268 | }
269 | block.request = {
270 | method,
271 | url,
272 | httpVersion,
273 | headers,
274 | body,
275 | };
276 | });
277 | if (
278 | block.request?.url.length ||
279 | block.metadata.length > 0 ||
280 | block.comments.length > 0
281 | ) {
282 | // sort headers by key
283 | block.request?.headers.sort((a, b) => {
284 | return a.key.localeCompare(b.key);
285 | });
286 | // sort metadata by key
287 | block.metadata.sort((a, b) => {
288 | return a.key.localeCompare(b.key);
289 | });
290 | blocks.push(block);
291 | }
292 | });
293 |
294 | traverseNodes(documentNode, (node) => {
295 | if (node.type === "variable_declaration") {
296 | let variable: Variable | null = null;
297 | // variables like `@key = value`
298 | node.children.forEach((child) => {
299 | switch (child.type) {
300 | case "identifier":
301 | variable = {
302 | key: child.text,
303 | value: "",
304 | };
305 | break;
306 | case "value":
307 | if (variable) {
308 | variable.value = child.text;
309 | }
310 | break;
311 | }
312 | });
313 | if (variable) {
314 | variables.push(variable);
315 | }
316 | }
317 | });
318 |
319 | // sort variables by key
320 | variables.sort((a, b) => {
321 | return a.key.localeCompare(b.key);
322 | });
323 |
324 | return { blocks, variables };
325 | };
326 |
327 | export const DocumentParser = {
328 | parse,
329 | };
330 |
--------------------------------------------------------------------------------
/src/lib/parser/OpenAPIDocumentParser.ts:
--------------------------------------------------------------------------------
1 | import type { Document, Block, Header } from "./DocumentParser";
2 |
3 | interface OpenAPIServer {
4 | url: string;
5 | description?: string;
6 | variables?: Record<
7 | string,
8 | {
9 | default: string;
10 | description?: string;
11 | enum?: string[];
12 | }
13 | >;
14 | }
15 |
16 | interface OpenAPIParameter {
17 | name: string;
18 | in: "query" | "header" | "path" | "cookie";
19 | description?: string;
20 | required?: boolean;
21 | deprecated?: boolean;
22 | example?: string | number | boolean;
23 | schema?: OpenAPISchema;
24 | }
25 |
26 | interface OpenAPISchema {
27 | type: "string" | "number" | "integer" | "boolean" | "array" | "object";
28 | format?: string;
29 | properties?: Record;
30 | items?: OpenAPISchema;
31 | required?: string[];
32 | description?: string;
33 | example?: OpenAPISchemaExample;
34 | }
35 |
36 | type OpenAPISchemaExample =
37 | | string
38 | | number
39 | | boolean
40 | | null
41 | | OpenAPISchemaExample[]
42 | | { [key: string]: OpenAPISchemaExample };
43 |
44 | interface OpenAPIRequestBody {
45 | description?: string;
46 | required?: boolean;
47 | content: Record<
48 | string,
49 | {
50 | schema?: OpenAPISchema;
51 | example?: OpenAPISchemaExample;
52 | }
53 | >;
54 | }
55 |
56 | interface OpenAPIOperation {
57 | summary?: string;
58 | description?: string;
59 | operationId?: string;
60 | parameters?: OpenAPIParameter[];
61 | requestBody?: OpenAPIRequestBody;
62 | responses?: Record<
63 | string,
64 | {
65 | description: string;
66 | content?: Record<
67 | string,
68 | {
69 | schema: OpenAPISchema;
70 | example?: OpenAPISchemaExample;
71 | }
72 | >;
73 | }
74 | >;
75 | }
76 |
77 | interface OpenAPIPathItem {
78 | get?: OpenAPIOperation;
79 | post?: OpenAPIOperation;
80 | put?: OpenAPIOperation;
81 | delete?: OpenAPIOperation;
82 | patch?: OpenAPIOperation;
83 | parameters?: OpenAPIParameter[];
84 | }
85 |
86 | export interface OpenAPISpec {
87 | openapi: string;
88 | info: {
89 | title: string;
90 | version: string;
91 | description?: string;
92 | };
93 | servers?: OpenAPIServer[];
94 | paths: Record;
95 | components?: {
96 | schemas?: Record;
97 | parameters?: Record;
98 | requestBodies?: Record;
99 | responses?: Record;
100 | };
101 | }
102 |
103 | interface OpenAPIParser {
104 | parse(openAPISpec: OpenAPISpec): ParseResult;
105 | }
106 |
107 | interface ParseResult {
108 | documents: Document[];
109 | serverUrls: string[];
110 | }
111 |
112 | export class OpenAPIDocumentParser implements OpenAPIParser {
113 | private buildRequestBlock(
114 | path: string,
115 | method: string,
116 | operation: OpenAPIOperation,
117 | ): Block {
118 | const block: Block = {
119 | requestSeparator: {
120 | text: null,
121 | },
122 | metadata: [],
123 | comments: [],
124 | request: {
125 | method: method.toUpperCase(),
126 | url: path,
127 | httpVersion: "HTTP/1.1",
128 | headers: [],
129 | body: null,
130 | },
131 | preRequestScripts: [],
132 | postRequestScripts: [],
133 | responseRedirect: null,
134 | };
135 |
136 | // Add operation summary/description as comments
137 | if (operation.summary) {
138 | block.comments.push(
139 | operation.summary
140 | .split("\n")
141 | .map((l) => `# ${l}`)
142 | .join("\n") + "\n",
143 | );
144 | }
145 | if (operation.description) {
146 | block.comments.push(
147 | operation.description
148 | .split("\n")
149 | .map((l) => `# ${l}`)
150 | .join("\n") + "\n",
151 | );
152 | }
153 |
154 | // Add operationId as metadata
155 | if (operation.operationId) {
156 | block.metadata.push({
157 | key: "name",
158 | value: operation.operationId,
159 | });
160 | }
161 |
162 | // Since we know block.request is initialized above, we can assert it's non-null
163 | if (operation.requestBody?.content) {
164 | const content = operation.requestBody.content;
165 | const contentType = Object.keys(content)[0];
166 |
167 | if (block.request) {
168 | // TypeScript guard
169 | block.request.headers.push({
170 | key: "Content-Type",
171 | value: contentType,
172 | });
173 |
174 | const mediaTypeObject = content[contentType];
175 | if (mediaTypeObject.example) {
176 | block.request.body = JSON.stringify(mediaTypeObject.example, null, 2);
177 | } else if (mediaTypeObject.schema) {
178 | block.request.body = this.generateExampleFromSchema(
179 | mediaTypeObject.schema,
180 | );
181 | }
182 | }
183 | }
184 |
185 | // Handle parameters
186 | if (operation.parameters && block.request) {
187 | // TypeScript guard
188 | for (const param of operation.parameters) {
189 | if (param.in === "header") {
190 | const header: Header = {
191 | key: param.name,
192 | value: param.example?.toString() || `{{${param.name}}}`,
193 | };
194 | block.request.headers.push(header);
195 | }
196 | }
197 | }
198 |
199 | return block;
200 | }
201 |
202 | private generateExampleFromSchema(schema: OpenAPISchema): string {
203 | const example: Record = {};
204 | if (schema.properties) {
205 | for (const [key, prop] of Object.entries(schema.properties)) {
206 | example[key] = prop.example || this.getDefaultValueForType(prop.type);
207 | }
208 | }
209 | return JSON.stringify(example, null, 2);
210 | }
211 |
212 | private getDefaultValueForType(type: OpenAPISchema["type"]): unknown {
213 | switch (type) {
214 | case "string":
215 | return "string";
216 | case "number":
217 | case "integer":
218 | return 0;
219 | case "boolean":
220 | return false;
221 | case "array":
222 | return [];
223 | case "object":
224 | return {};
225 | default:
226 | return null;
227 | }
228 | }
229 |
230 | private extractServerIdentifier(server: OpenAPIServer): string {
231 | // Remove protocol prefix (http:// or https://)
232 | let identifier = server.url.replace(/^https?:\/\//, "");
233 |
234 | // Replace variable patterns with their default values or placeholder
235 | if (server.variables) {
236 | Object.entries(server.variables).forEach(([key, variable]) => {
237 | identifier = identifier.replace(
238 | `{${key}}`,
239 | variable.default || `[${key}]`,
240 | );
241 | });
242 | }
243 |
244 | // Remove port and path for cleaner identifier
245 | identifier = identifier.split(":")[0].split("/")[0];
246 |
247 | return identifier;
248 | }
249 |
250 | parse(openAPISpec: OpenAPISpec): ParseResult {
251 | // If no servers are specified, create a single document with relative URLs
252 | if (!openAPISpec.servers || openAPISpec.servers.length === 0) {
253 | return {
254 | documents: [this.createDocument(openAPISpec)],
255 | serverUrls: ["default"],
256 | };
257 | }
258 |
259 | // Create a document for each server
260 | const documents = openAPISpec.servers.map((server, index) =>
261 | this.createDocument(openAPISpec, server, index),
262 | );
263 |
264 | // Extract server identifiers in matching order
265 | const serverUrls = openAPISpec.servers.map((server) =>
266 | this.extractServerIdentifier(server),
267 | );
268 |
269 | return {
270 | documents,
271 | serverUrls,
272 | };
273 | }
274 |
275 | private buildFullUrl(
276 | path: string,
277 | server?: OpenAPIServer,
278 | serverIndex?: number,
279 | ): string {
280 | if (!server) {
281 | return path;
282 | }
283 |
284 | // Remove leading slash from path
285 | const cleanPath = path.replace(/^\//, "");
286 |
287 | let url = server.url;
288 |
289 | // Replace server variables with double curly brace format
290 | if (server.variables) {
291 | Object.keys(server.variables).forEach((key) => {
292 | url = url.replace(`{${key}}`, `{{${key}}}`);
293 | });
294 | }
295 |
296 | // Replace the entire server URL with baseUrl variable
297 | url = `{{baseUrl${serverIndex || ""}}}/${cleanPath}`;
298 |
299 | return url;
300 | }
301 |
302 | private createDocument(
303 | openAPISpec: OpenAPISpec,
304 | server?: OpenAPIServer,
305 | serverIndex?: number,
306 | ): Document {
307 | const document: Document = {
308 | variables: [],
309 | blocks: [],
310 | };
311 |
312 | // Add server URL as a variable if it exists
313 | if (server) {
314 | let serverUrl = server.url.replace(/\/$/, ""); // Remove trailing slash
315 |
316 | // Replace server variables with double curly braces in the URL
317 | if (server.variables) {
318 | Object.keys(server.variables).forEach((key) => {
319 | serverUrl = serverUrl.replace(`{${key}}`, `{{${key}}}`);
320 | });
321 | }
322 |
323 | document.variables.push({
324 | key: `baseUrl${serverIndex || ""}`,
325 | value: serverUrl,
326 | });
327 |
328 | // Add server variables if they exist
329 | if (server.variables) {
330 | Object.entries(server.variables).forEach(([key, variable]) => {
331 | document.variables.push({
332 | key: key,
333 | value: variable.default,
334 | });
335 | });
336 | }
337 | }
338 |
339 | // Parse paths into blocks
340 | for (const [path, pathItem] of Object.entries(openAPISpec.paths)) {
341 | for (const [method, operation] of Object.entries(pathItem)) {
342 | if (["get", "post", "put", "delete", "patch"].includes(method)) {
343 | const block = this.buildRequestBlock(
344 | this.buildFullUrl(path, server, serverIndex),
345 | method,
346 | operation as OpenAPIOperation,
347 | );
348 |
349 | // Add server description as a comment if it exists
350 | if (server?.description) {
351 | block.comments.unshift(`# Server: ${server.description}\n`);
352 | }
353 |
354 | document.blocks.push(block);
355 | }
356 | }
357 | }
358 |
359 | return document;
360 | }
361 | }
362 |
--------------------------------------------------------------------------------
/src/lib/parser/PostmanDocumentParser.ts:
--------------------------------------------------------------------------------
1 | import type { Document, Block } from "./DocumentParser";
2 |
3 | interface PostmanVariable {
4 | key: string;
5 | value: string;
6 | enabled?: boolean;
7 | type?: string;
8 | }
9 |
10 | interface PostmanHeader {
11 | key: string;
12 | value: string;
13 | disabled?: boolean;
14 | }
15 |
16 | interface PostmanRequest {
17 | method: string;
18 | header: PostmanHeader[];
19 | url: {
20 | raw: string;
21 | protocol?: string;
22 | host?: string[];
23 | path?: string[];
24 | query?: Array<{
25 | key: string;
26 | value: string;
27 | disabled?: boolean;
28 | }>;
29 | };
30 | body?: {
31 | mode: "raw" | "formdata" | "urlencoded" | "file" | "graphql";
32 | raw?: string;
33 | options?: {
34 | raw?: {
35 | language: string;
36 | };
37 | };
38 | };
39 | description?: string;
40 | name?: string;
41 | }
42 |
43 | interface PostmanItem {
44 | name: string;
45 | request: PostmanRequest;
46 | response: unknown[];
47 | }
48 |
49 | interface PostmanItemGroup {
50 | name: string;
51 | item: (PostmanItem | PostmanItemGroup)[];
52 | description?: string;
53 | }
54 |
55 | interface PostmanCollection {
56 | info: {
57 | name: string;
58 | description?: string;
59 | version?: string;
60 | };
61 | item: (PostmanItem | PostmanItemGroup)[];
62 | variable?: PostmanVariable[];
63 | }
64 |
65 | interface ParseResult {
66 | document: Document;
67 | collectionName: string;
68 | }
69 |
70 | export class PostmanDocumentParser {
71 | private buildRequestBlock(item: PostmanItem): Block {
72 | const block: Block = {
73 | requestSeparator: {
74 | text: null,
75 | },
76 | metadata: [],
77 | comments: [],
78 | request: {
79 | method: item.request.method,
80 | url: item.request.url.raw,
81 | httpVersion: "HTTP/1.1",
82 | headers: [],
83 | body: null,
84 | },
85 | preRequestScripts: [],
86 | postRequestScripts: [],
87 | responseRedirect: null,
88 | };
89 |
90 | // Add name and description as comments
91 | if (item.name) {
92 | block.comments.push(`# ${item.name}\n`);
93 | }
94 | if (item.request.description) {
95 | block.comments.push(`# ${item.request.description}\n`);
96 | }
97 |
98 | // Add name as metadata
99 | if (item.name) {
100 | block.metadata.push({
101 | key: "name",
102 | value: item.name.replace(/\s+/g, "_").toUpperCase(),
103 | });
104 | }
105 |
106 | // Handle headers
107 | if (item.request.header) {
108 | item.request.header
109 | .filter((header) => !header.disabled)
110 | .forEach((header) => {
111 | if (block.request) {
112 | block.request.headers.push({
113 | key: header.key,
114 | value: header.value,
115 | });
116 | }
117 | });
118 | }
119 |
120 | // Handle request body
121 | if (item.request.body?.mode === "raw" && item.request.body.raw) {
122 | if (block.request) {
123 | block.request.body = item.request.body.raw;
124 |
125 | // Add Content-Type header if not present
126 | const contentType =
127 | item.request.body.options?.raw?.language || "text/plain";
128 | if (
129 | !block.request.headers.some(
130 | (h) => h.key.toLowerCase() === "content-type",
131 | )
132 | ) {
133 | block.request.headers.push({
134 | key: "Content-Type",
135 | value: this.getContentType(contentType),
136 | });
137 | }
138 | }
139 | }
140 |
141 | return block;
142 | }
143 |
144 | private getContentType(language: string): string {
145 | switch (language.toLowerCase()) {
146 | case "json":
147 | return "application/json";
148 | case "xml":
149 | return "application/xml";
150 | case "javascript":
151 | return "application/javascript";
152 | default:
153 | return "text/plain";
154 | }
155 | }
156 |
157 | private processItems(
158 | items: (PostmanItem | PostmanItemGroup)[],
159 | document: Document,
160 | parentPath: string = "",
161 | ): void {
162 | for (const item of items) {
163 | if (this.isItemGroup(item)) {
164 | // Process folder
165 | const newPath = parentPath ? `${parentPath}/${item.name}` : item.name;
166 | if (item.description) {
167 | document.blocks.push({
168 | requestSeparator: {
169 | text: null,
170 | },
171 | metadata: [],
172 | comments: [`# Folder: ${newPath}\n`, `# ${item.description}\n`],
173 | request: null,
174 | preRequestScripts: [],
175 | postRequestScripts: [],
176 | responseRedirect: null,
177 | });
178 | }
179 | this.processItems(item.item, document, newPath);
180 | } else {
181 | // Process request
182 | const block = this.buildRequestBlock(item);
183 | document.blocks.push(block);
184 | }
185 | }
186 | }
187 |
188 | private isItemGroup(
189 | item: PostmanItem | PostmanItemGroup,
190 | ): item is PostmanItemGroup {
191 | return "item" in item;
192 | }
193 |
194 | parse(collection: PostmanCollection): ParseResult {
195 | const document: Document = {
196 | variables: [],
197 | blocks: [],
198 | };
199 |
200 | // Add collection variables
201 | if (collection.variable) {
202 | collection.variable
203 | .filter((v) => v.enabled !== false)
204 | .forEach((v) => {
205 | document.variables.push({
206 | key: v.key,
207 | value: v.value,
208 | });
209 | });
210 | }
211 |
212 | // Process all requests in the collection
213 | this.processItems(collection.item, document);
214 |
215 | return {
216 | document: document,
217 | collectionName: collection.info.name,
218 | };
219 | }
220 | }
221 |
--------------------------------------------------------------------------------
/src/lib/parser/index.ts:
--------------------------------------------------------------------------------
1 | import * as fs from "fs";
2 | import * as process from "process";
3 | import chalk from "chalk";
4 | import yaml from "js-yaml";
5 | import { fileWalker } from "./../filewalker";
6 | import { DocumentParser } from "./DocumentParser";
7 | import { DocumentBuilder } from "./DocumentBuilder";
8 | import { Diff } from "./Diff";
9 | import { OpenAPIDocumentParser, OpenAPISpec } from "./OpenAPIDocumentParser";
10 | import { PostmanDocumentParser } from "./PostmanDocumentParser";
11 | import { BrunoDocumentParser } from "./BrunoDocumentParser";
12 |
13 | const OpenAPIParser = new OpenAPIDocumentParser();
14 | const PostmanParser = new PostmanDocumentParser();
15 | const BrunoParser = new BrunoDocumentParser();
16 |
17 | const getOpenAPISpecAsJSON = (filepath: string): OpenAPISpec => {
18 | if (filepath.endsWith(".yaml") || filepath.endsWith(".yml")) {
19 | return yaml.load(fs.readFileSync(filepath, "utf-8")) as OpenAPISpec;
20 | }
21 | return JSON.parse(fs.readFileSync(filepath, "utf-8")) as OpenAPISpec;
22 | };
23 |
24 | const isAlreadyPretty = async (
25 | filepath: string,
26 | options: { body: boolean },
27 | ): Promise<[boolean | null, string, string]> => {
28 | const content = fs.readFileSync(filepath, "utf-8");
29 | const document = DocumentParser.parse(content);
30 | if (!document) {
31 | return [null, content, ""];
32 | }
33 | const build = await DocumentBuilder.build(document, options.body);
34 | return [content === build, content, build];
35 | };
36 |
37 | const makeFilePretty = async (
38 | filepath: string,
39 | formatBody: boolean,
40 | ): Promise => {
41 | const content = fs.readFileSync(filepath, "utf-8");
42 | const document = DocumentParser.parse(content);
43 | // if document is null, that means we had an error parsing the file
44 | if (!document) {
45 | return null;
46 | }
47 | const build = await DocumentBuilder.build(document, formatBody);
48 | const isPretty = content === build;
49 | if (!isPretty) {
50 | fs.writeFileSync(filepath, build, "utf-8");
51 | }
52 | return isPretty;
53 | };
54 |
55 | const getStdinContent = async () => {
56 | try {
57 | let content = "";
58 |
59 | for await (const chunk of process.stdin) {
60 | content += chunk.toString();
61 | }
62 |
63 | return content;
64 | } catch (error) {
65 | console.error(
66 | chalk.red(
67 | `Error reading stdin: ${error instanceof Error ? error?.message || error : error}`,
68 | ),
69 | );
70 |
71 | return process.exit(1);
72 | }
73 | };
74 |
75 | const checkStdin = async (options: { body: boolean; verbose: boolean }) => {
76 | const content = await getStdinContent();
77 |
78 | const document = DocumentParser.parse(content);
79 |
80 | if (!document) {
81 | console.error(chalk.red("Error parsing input"));
82 | return process.exit(1);
83 | }
84 |
85 | const formattedDocument = await DocumentBuilder.build(document, options.body);
86 |
87 | const isPretty = formattedDocument === content;
88 |
89 | if (isPretty) {
90 | console.log(chalk.green("Input is pretty"));
91 | } else {
92 | console.error(chalk.yellow("Input is not pretty"));
93 |
94 | if (options.verbose) {
95 | Diff(formattedDocument, content);
96 | }
97 |
98 | return process.exit(1);
99 | }
100 | };
101 |
102 | /**
103 | * Checks the validity of HTTP files in the given directory.
104 | *
105 | * @param {string} dirPath The directory path to check.
106 | * @param {{ verbose: boolean; body: boolean }} options The options to use.
107 | * @param {string[]} extensions An array of file extensions to filter by (e.g., ['.http', '.rest']).
108 | * @returns {CheckedFiles[]} An array of CheckedFiles objects.
109 | */
110 | export const check = async (
111 | dirPath: string | null,
112 | options: { verbose: boolean; body: boolean; stdin: boolean },
113 | extensions: string[] | undefined = undefined,
114 | ): Promise => {
115 | if (options.stdin) {
116 | await checkStdin(options);
117 | return;
118 | }
119 |
120 | if (!dirPath) {
121 | dirPath = process.cwd();
122 | }
123 | if (!extensions) {
124 | extensions = [".http", ".rest"];
125 | }
126 | let errorHappened = false;
127 | const files = fileWalker(dirPath, extensions);
128 | for (const file of files) {
129 | const [isPretty, content, build] = await isAlreadyPretty(file, options);
130 | if (isPretty === false) {
131 | console.log(chalk.yellow(`File not pretty: ${file}`));
132 | if (options.verbose) {
133 | Diff(build, content);
134 | }
135 | } else if (isPretty === null) {
136 | console.log(chalk.red(`Error parsing file: ${file}`));
137 | errorHappened = true;
138 | } else {
139 | console.log(chalk.green(`Valid file: ${file}`));
140 | }
141 | }
142 | if (errorHappened) {
143 | process.exit(1);
144 | }
145 | };
146 |
147 | export const formatStdin = async (formatBody: boolean) => {
148 | const content = await getStdinContent();
149 |
150 | const document = DocumentParser.parse(content);
151 |
152 | if (!document) {
153 | console.error(chalk.red("Error parsing input"));
154 | return process.exit(1);
155 | }
156 |
157 | const formattedDocument = await DocumentBuilder.build(document, formatBody);
158 |
159 | process.stdout.write(formattedDocument);
160 | };
161 |
162 | export const format = async (
163 | dirPath: string | null,
164 | options: { body: boolean; stdin: boolean },
165 | extensions: string[] | undefined = undefined,
166 | ): Promise => {
167 | if (options?.stdin) {
168 | await formatStdin(options.body);
169 | return;
170 | }
171 |
172 | if (!dirPath) {
173 | dirPath = process.cwd();
174 | }
175 | if (!extensions) {
176 | extensions = [".http", ".rest"];
177 | }
178 | let errorHappened = false;
179 | const files = fileWalker(dirPath, extensions);
180 | for (const file of files) {
181 | const isPretty = await makeFilePretty(file, options.body);
182 | if (isPretty === false) {
183 | console.log(chalk.yellow(`Formatted file: ${file}`));
184 | } else if (isPretty === null) {
185 | console.log(chalk.red(`Error parsing file: ${file}`));
186 | errorHappened = true;
187 | } else {
188 | console.log(chalk.green(`Valid file: ${file}`));
189 | }
190 | }
191 | if (errorHappened) {
192 | process.exit(1);
193 | }
194 | };
195 |
196 | const convertFromOpenAPI = async (files: string[]): Promise => {
197 | for (const file of files) {
198 | const json = getOpenAPISpecAsJSON(file);
199 | const { documents, serverUrls } = OpenAPIParser.parse(json);
200 | serverUrls.forEach(async (serverUrl, index) => {
201 | const build = await DocumentBuilder.build(documents[index]);
202 | const outputFilename = file.replace(/\.[^/.]+$/, `.${serverUrl}.http`);
203 | fs.writeFileSync(outputFilename, build, "utf-8");
204 | console.log(
205 | chalk.green(
206 | `Converted OpenAPI spec file: ${file} --> ${outputFilename}`,
207 | ),
208 | );
209 | });
210 | }
211 | };
212 |
213 | const convertFromPostman = async (files: string[]): Promise => {
214 | for (const file of files) {
215 | const json = JSON.parse(fs.readFileSync(file, "utf-8"));
216 | const { document } = PostmanParser.parse(json);
217 | const build = await DocumentBuilder.build(document);
218 | const outputFilename = file.replace(/\.[^/.]+$/, ".http");
219 | fs.writeFileSync(outputFilename, build, "utf-8");
220 | console.log(
221 | chalk.green(
222 | `Converted PostMan Collection file: ${file} --> ${outputFilename}`,
223 | ),
224 | );
225 | }
226 | };
227 |
228 | const convertFromBruno = async (files: string[]): Promise => {
229 | const { documents, environmentNames, collectionName } = BrunoParser.parse(
230 | files[0],
231 | );
232 | environmentNames.forEach(async (envName, idx) => {
233 | const build = await DocumentBuilder.build(documents[idx]);
234 | const outputFilename = `${collectionName}.${envName}.http`;
235 | fs.writeFileSync(outputFilename, build, "utf-8");
236 | console.log(
237 | chalk.green(
238 | `Converted Bruno collection: ${files[0]} --> ${outputFilename}`,
239 | ),
240 | );
241 | });
242 | };
243 |
244 | const invalidFormat = (t: "src" | "dest", format: string): void => {
245 | console.log(chalk.red(`Invalid ${t} format ${format}.`));
246 | process.exit(1);
247 | };
248 |
249 | export const convert = async (
250 | options: { from: string; to: string },
251 | files: string[],
252 | ): Promise => {
253 | switch (options.from) {
254 | case "openapi":
255 | switch (options.to) {
256 | case "http":
257 | await convertFromOpenAPI(files);
258 | break;
259 | default:
260 | invalidFormat("dest", options.to);
261 | }
262 | break;
263 | case "postman":
264 | switch (options.to) {
265 | case "http":
266 | await convertFromPostman(files);
267 | break;
268 | default:
269 | invalidFormat("dest", options.to);
270 | }
271 | break;
272 | case "bruno":
273 | switch (options.to) {
274 | case "http":
275 | await convertFromBruno(files);
276 | break;
277 | default:
278 | invalidFormat("dest", options.to);
279 | }
280 | break;
281 | default:
282 | invalidFormat("src", options.from);
283 | }
284 | };
285 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "esnext",
4 | "strict": true,
5 | "outDir": "./dist",
6 | "rootDir": ".",
7 | "skipLibCheck": true,
8 | "moduleResolution": "node",
9 | "target": "esnext",
10 | "esModuleInterop": true,
11 | "resolveJsonModule": true
12 | },
13 | "include": ["src/*.ts", "src/**/*.ts"],
14 | "exclude": ["dist", "node_modules"]
15 | }
16 |
--------------------------------------------------------------------------------
/web/.envrc:
--------------------------------------------------------------------------------
1 | layout node
2 |
--------------------------------------------------------------------------------
/web/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 |
3 | # Output
4 | .output
5 | .vercel
6 | .netlify
7 | .wrangler
8 | /.svelte-kit
9 | /build
10 |
11 | # OS
12 | .DS_Store
13 | Thumbs.db
14 |
15 | # Env
16 | .env
17 | .env.*
18 | !.env.example
19 | !.env.test
20 |
21 | # Vite
22 | vite.config.js.timestamp-*
23 | vite.config.ts.timestamp-*
24 |
--------------------------------------------------------------------------------
/web/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 |
--------------------------------------------------------------------------------
/web/.nvmrc:
--------------------------------------------------------------------------------
1 | 22
2 |
--------------------------------------------------------------------------------
/web/.prettierignore:
--------------------------------------------------------------------------------
1 | # Package Managers
2 | package-lock.json
3 | pnpm-lock.yaml
4 | yarn.lock
5 |
--------------------------------------------------------------------------------
/web/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true,
3 | "singleQuote": true,
4 | "trailingComma": "none",
5 | "printWidth": 100,
6 | "plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
7 | "overrides": [
8 | {
9 | "files": "*.svelte",
10 | "options": {
11 | "parser": "svelte"
12 | }
13 | }
14 | ]
15 | }
16 |
--------------------------------------------------------------------------------
/web/.yamllint.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | extends: default
3 | rules:
4 | truthy:
5 | check-keys: false
6 |
--------------------------------------------------------------------------------
/web/README.md:
--------------------------------------------------------------------------------
1 | ## Developing
2 |
3 | Once you've created a project and installed dependencies with `bun install --frozen-lockfile`.
4 |
5 | ```bash
6 | bun run dev
7 |
8 | # or start the server and open the app in a new browser tab
9 | bun run dev -- --open
10 | ```
11 |
12 | ## Building
13 |
14 | To create a production version of your app:
15 |
16 | ```bash
17 | bun run build
18 | ```
19 |
20 | You can preview the production build with `bun run preview`.
21 |
--------------------------------------------------------------------------------
/web/eslint.config.js:
--------------------------------------------------------------------------------
1 | import prettier from 'eslint-config-prettier';
2 | import svelte from 'eslint-plugin-svelte';
3 | export default [prettier, ...svelte.configs.prettier];
4 |
--------------------------------------------------------------------------------
/web/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "private": true,
4 | "version": "0.0.1",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite dev",
8 | "build": "vite build",
9 | "preview": "vite preview",
10 | "prepare": "svelte-kit sync || echo ''",
11 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
12 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
13 | "format": "prettier --write .",
14 | "lint": "prettier --check . && eslint ."
15 | },
16 | "devDependencies": {
17 | "@eslint/compat": "^1.2.7",
18 | "@eslint/js": "^9.22.0",
19 | "@sveltejs/adapter-static": "^3.0.8",
20 | "@sveltejs/kit": "^2.19.0",
21 | "@sveltejs/vite-plugin-svelte": "^5.0.3",
22 | "@tailwindcss/vite": "^4.0.13",
23 | "@types/prismjs": "1.26.5",
24 | "daisyui": "5.0.2",
25 | "eslint": "^9.22.0",
26 | "eslint-config-prettier": "^10.1.1",
27 | "eslint-plugin-svelte": "^3.1.0",
28 | "globals": "^16.0.0",
29 | "prettier": "^3.5.3",
30 | "prettier-plugin-svelte": "^3.3.3",
31 | "prettier-plugin-tailwindcss": "^0.6.11",
32 | "prismjs": "1.30.0",
33 | "svelte": "^5.23.0",
34 | "svelte-check": "^4.1.5",
35 | "tailwindcss": "^4.0.13",
36 | "typescript": "^5.8.2",
37 | "typescript-eslint": "^8.26.1",
38 | "vite": "^6.2.1"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/web/src/app.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss';
2 | @plugin 'daisyui' {
3 | themes: dark --default;
4 | }
5 |
6 | div.code-toolbar {
7 | position: relative;
8 | display: grid;
9 | }
10 | div.code-toolbar > .toolbar {
11 | position: absolute;
12 | z-index: 10;
13 | top: -16px;
14 | right: -16px;
15 | transition: opacity 0.3s ease-in-out;
16 | opacity: 1;
17 | }
18 | div.code-toolbar > .toolbar > .toolbar-item {
19 | display: inline-block;
20 | }
21 | div.code-toolbar > .toolbar > .toolbar-item > a {
22 | cursor: pointer;
23 | }
24 | /* icon copy */
25 | div.code-toolbar > .toolbar > .toolbar-item > button {
26 | font-size: 2rem;
27 | color: #e6caa8;
28 | }
29 | /* color of "Copied!" */
30 | .copy-to-clipboard-button[data-copy-state~='copy-success'] > span {
31 | color: #10b981;
32 | }
33 |
34 | /* default item color */
35 | div.code-toolbar > .toolbar > .toolbar-item {
36 | color: #e6caa8;
37 | }
38 |
--------------------------------------------------------------------------------
/web/src/app.d.ts:
--------------------------------------------------------------------------------
1 | // See https://svelte.dev/docs/kit/types#app.d.ts
2 | // for information about these interfaces
3 | declare global {
4 | namespace App {
5 | // interface Error {}
6 | // interface Locals {}
7 | // interface PageData {}
8 | // interface PageState {}
9 | // interface Platform {}
10 | }
11 | }
12 |
13 | export {};
14 |
--------------------------------------------------------------------------------
/web/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %sveltekit.head%
8 |
9 |
10 | %sveltekit.body%
11 |
12 |
13 |
--------------------------------------------------------------------------------
/web/src/lib/HeadComponent.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 | {data.title}
7 |
8 |
9 |
--------------------------------------------------------------------------------
/web/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | // place files you want to import through the `$lib` alias in this folder.
2 |
--------------------------------------------------------------------------------
/web/src/routes/+layout.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 | {@render children()}
7 |
--------------------------------------------------------------------------------
/web/src/routes/+layout.ts:
--------------------------------------------------------------------------------
1 | export const prerender = true;
2 |
--------------------------------------------------------------------------------
/web/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |

52 |
kulala-fmt
53 |
An opinionated 🦄 .http and .rest 🐼 files linter 💄 and formatter ⚡.
54 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
Install ⚡
64 |
Install kulala-fmt using ...
65 |
71 |
72 |
npm install -g @mistweaverco/kulala-fmt
77 |
78 |
79 |
yarn add --global @mistweaverco/kulala-fmt
84 |
85 |
86 |
bun install -g @mistweaverco/kulala-fmt
91 |
92 |
93 |
pnpm install -g @mistweaverco/kulala-fmt
98 |
99 |
100 |
103 |
104 |
105 |
106 |
107 |
148 |
149 |
150 |
151 |
Usage 🐆
152 |
Run kulala-fmt to format your .http files.
153 |
kulala-fmt format /tmp/test.http test.http
156 |
157 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
Get involved 📦
168 |
kulala-fmt is open-source and we welcome contributions.
169 |
170 | View the code,
171 | and/or check out the docs.
172 |
173 |
174 |
175 |
176 |
--------------------------------------------------------------------------------
/web/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mistweaverco/kulala-fmt/304929661c3aa218f1903376c6db5153b4ee443f/web/static/favicon.png
--------------------------------------------------------------------------------
/web/static/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mistweaverco/kulala-fmt/304929661c3aa218f1903376c6db5153b4ee443f/web/static/logo.png
--------------------------------------------------------------------------------
/web/static/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/web/static/schema.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "http://json-schema.org/draft-07/schema#",
3 | "type": "object",
4 | "properties": {
5 | "defaults": {
6 | "type": "object",
7 | "properties": {
8 | "http_method": {
9 | "type": "string",
10 | "enum": [
11 | "GET",
12 | "POST",
13 | "PUT",
14 | "DELETE",
15 | "PATCH",
16 | "HEAD",
17 | "OPTIONS",
18 | "TRACE",
19 | "CONNECT",
20 | "WEBSOCKET",
21 | "GRAPHQL"
22 | ]
23 | },
24 | "http_version": {
25 | "type": "string",
26 | "enum": [
27 | "HTTP/1.0",
28 | "HTTP/1.1",
29 | "HTTP/2.0",
30 | "HTTP/3.0"
31 | ]
32 | }
33 | }
34 | }
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/web/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from '@sveltejs/adapter-static';
2 | import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
3 |
4 | /** @type {import('@sveltejs/kit').Config} */
5 | const config = {
6 | preprocess: vitePreprocess(),
7 |
8 | kit: {
9 | adapter: adapter()
10 | },
11 |
12 | extensions: ['.svelte']
13 | };
14 |
15 | export default config;
16 |
--------------------------------------------------------------------------------
/web/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true,
12 | "moduleResolution": "bundler",
13 | "target": "esnext"
14 | }
15 | // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias
16 | // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files
17 | //
18 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
19 | // from the referenced tsconfig.json - TypeScript does not merge them in
20 | }
21 |
--------------------------------------------------------------------------------
/web/vite.config.ts:
--------------------------------------------------------------------------------
1 | import tailwindcss from '@tailwindcss/vite';
2 | import { sveltekit } from '@sveltejs/kit/vite';
3 | import { defineConfig } from 'vite';
4 |
5 | export default defineConfig({
6 | plugins: [sveltekit(), tailwindcss()]
7 | });
8 |
--------------------------------------------------------------------------------