ast-search 0.2.3 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  A CLI tool for searching source files using AST patterns, designed to facilitate large-scale refactors.
4
4
 
5
- Accepts an [esquery](https://github.com/estools/esquery) CSS-selector-style query and searches all supported files under a directory, printing each match with its file path, line, and column.
5
+ Accepts a query and searches all supported files under a directory, printing each match with its file path, line, and column. Language support is provided by plugins — the core handles JS/TS/Vue; additional languages are opt-in.
6
6
 
7
7
  ## Example
8
8
 
@@ -31,17 +31,28 @@ src/components/Foo.vue:5:13: return this.testValue
31
31
  src/components/Bar.vue:9:18: return this.otherProp
32
32
  ```
33
33
 
34
+ ## Installation
35
+
36
+ ```bash
37
+ npm install -g ast-search
38
+
39
+ # Optional: add Python support
40
+ npm install -g ast-search-python
41
+ ```
42
+
34
43
  ## Usage
35
44
 
36
45
  ```
37
- ast-search <query> [--dir <path>] [--format <fmt>]
46
+ ast-search <query> [--dir <path>] [--format <fmt>] [--lang <id>] [--plugin <pkg>]
38
47
  ```
39
48
 
40
- | Argument | Description | Default |
41
- | ---------------- | -------------------------------------------------------- | ------------ |
42
- | `<query>` | esquery selector string (see below) | required |
43
- | `-d, --dir` | root directory to search | current dir |
44
- | `-f, --format` | output format: `text`, `json`, or `files` | `text` |
49
+ | Argument | Description | Default |
50
+ | ------------------ | ------------------------------------------------------------- | ------------ |
51
+ | `<query>` | Query string (see Query Syntax below) | required |
52
+ | `-d, --dir` | Root directory to search | current dir |
53
+ | `-f, --format` | Output format: `text`, `json`, or `files` | `text` |
54
+ | `-l, --lang` | Restrict search to one language backend (e.g. `js`, `python`) | all languages |
55
+ | `-p, --plugin` | Load a language plugin package (repeatable) | none |
45
56
 
46
57
  ### Output formats
47
58
 
@@ -51,6 +62,8 @@ ast-search <query> [--dir <path>] [--format <fmt>]
51
62
 
52
63
  ## Query syntax
53
64
 
65
+ ### JavaScript / TypeScript / Vue
66
+
54
67
  Queries use [esquery](https://github.com/estools/esquery) CSS selector syntax over Babel AST node types. A few examples:
55
68
 
56
69
  ```bash
@@ -64,7 +77,7 @@ ast-search 'AwaitExpression'
64
77
  ast-search 'CatchClause AssignmentExpression'
65
78
  ```
66
79
 
67
- ### Shorthands
80
+ #### Shorthands
68
81
 
69
82
  Common node types can be written as short keywords:
70
83
 
@@ -92,7 +105,7 @@ The original Vue `this` example using shorthands:
92
105
  ast-search 'ObjectMethod[key.name="setup"] this'
93
106
  ```
94
107
 
95
- ### Optional chaining
108
+ #### Optional chaining
96
109
 
97
110
  Optional chains (`?.`) are normalized transparently — `CallExpression` and `MemberExpression` selectors match both regular and optional-chain variants:
98
111
 
@@ -107,6 +120,81 @@ The `optional` flag is preserved on matched nodes, so you can still narrow to st
107
120
  ast-search 'CallExpression[optional=true]'
108
121
  ```
109
122
 
123
+ ### Python (via `ast-search-python`)
124
+
125
+ Python queries use [tree-sitter](https://tree-sitter.github.io/tree-sitter/) S-expression syntax. Install the plugin first:
126
+
127
+ ```bash
128
+ npm install -g ast-search-python
129
+ ```
130
+
131
+ Then pass `--plugin ast-search-python` to enable `.py` / `.pyw` file support:
132
+
133
+ ```bash
134
+ # Find all function definitions (shorthand)
135
+ ast-search 'fn' --plugin ast-search-python
136
+
137
+ # Find all class definitions (raw S-expression)
138
+ ast-search '(class_definition) @cls' --plugin ast-search-python
139
+
140
+ # Find all calls to a specific function by name
141
+ ast-search '(call function: (identifier) @n (#eq? @n "my_func")) @c' --plugin ast-search-python
142
+
143
+ # Restrict to only Python files in a mixed-language repo
144
+ ast-search 'fn' --lang python --plugin ast-search-python
145
+ ```
146
+
147
+ #### Python shorthands
148
+
149
+ | Shorthand | Expands to |
150
+ | ----------- | ----------------------------------- |
151
+ | `fn` | `(function_definition) @_` |
152
+ | `call` | `(call) @_` |
153
+ | `class` | `(class_definition) @_` |
154
+ | `assign` | `(assignment) @_` |
155
+ | `return` | `(return_statement) @_` |
156
+ | `await` | `(await) @_` |
157
+ | `yield` | `(yield) @_` |
158
+ | `import` | `(import_statement) @_` |
159
+ | `from` | `(import_from_statement) @_` |
160
+ | `if` | `(if_statement) @_` |
161
+ | `for` | `(for_statement) @_` |
162
+ | `while` | `(while_statement) @_` |
163
+ | `raise` | `(raise_statement) @_` |
164
+ | `with` | `(with_statement) @_` |
165
+ | `lambda` | `(lambda) @_` |
166
+ | `decorator` | `(decorator) @_` |
167
+ | `augassign` | `(augmented_assignment) @_` |
168
+
169
+ For raw S-expression queries, include at least one `@capture_name` — tree-sitter's `captures()` requires it to return results.
170
+
171
+ > **Note:** In tree-sitter-python 0.21+, `async def` functions are still typed as `function_definition` (no separate `async_function_definition` node). To match only async functions, use a full predicate query.
172
+
110
173
  ## Supported file types
111
174
 
112
- `.js`, `.ts`, `.jsx`, `.tsx`, `.mjs`, `.cjs`, `.vue`
175
+ **Core:** `.js` `.ts` `.jsx` `.tsx` `.mjs` `.cjs` `.vue`
176
+
177
+ **With `ast-search-python`:** `.py` `.pyw`
178
+
179
+ ## Plugin API
180
+
181
+ To write a language plugin, implement the `LanguageBackend` interface exported from `ast-search/plugin` and export a `register` function:
182
+
183
+ ```typescript
184
+ import type { LanguageBackend, LanguageRegistry } from 'ast-search/plugin';
185
+
186
+ class MyLanguageBackend implements LanguageBackend {
187
+ readonly langId = 'mylang';
188
+ readonly name = 'My Language';
189
+ readonly extensions = new Set(['.ml']);
190
+ parse(source: string, filePath: string) { /* return opaque AST */ }
191
+ query(ast: unknown, selector: string, source: string, filePath: string) { /* return Match[] */ }
192
+ validateSelector(selector: string) { /* throw on invalid */ }
193
+ }
194
+
195
+ export function register(registry: LanguageRegistry) {
196
+ registry.register(new MyLanguageBackend());
197
+ }
198
+ ```
199
+
200
+ Name your package `ast-search-<lang>` and users load it with `--plugin ast-search-<lang>`.
@@ -0,0 +1,30 @@
1
+ import { extname } from "node:path";
2
+ import { getAst, parseVueSFC } from "../../file.js";
3
+ import { runQuery, validateSelector as validate, SHORTHANDS, expandShorthands } from "../../search.js";
4
+ export class JSLanguageBackend {
5
+ constructor() {
6
+ this.langId = "js";
7
+ this.name = "JavaScript/TypeScript";
8
+ this.extensions = new Set([
9
+ ".js",
10
+ ".ts",
11
+ ".jsx",
12
+ ".tsx",
13
+ ".mjs",
14
+ ".cjs",
15
+ ".vue",
16
+ ]);
17
+ }
18
+ parse(source, filePath) {
19
+ const ext = extname(filePath);
20
+ const content = ext === ".vue" ? parseVueSFC(Buffer.from(source)) : source;
21
+ return getAst(content);
22
+ }
23
+ query(ast, selector, source, filePath) {
24
+ return runQuery(selector, ast, source, filePath);
25
+ }
26
+ validateSelector(selector) {
27
+ validate(selector);
28
+ }
29
+ }
30
+ export { SHORTHANDS, expandShorthands };
@@ -0,0 +1,22 @@
1
+ import { FileHandle } from "node:fs/promises";
2
+ import * as parser from "@babel/parser";
3
+ import { File } from "@babel/types";
4
+ import type { LanguageBackend } from "./language.js";
5
+ import type { LanguageRegistry } from "./registry.js";
6
+ export declare function getAst(contents: string): parser.ParseResult<File>;
7
+ export declare const SCRIPT_OPEN: RegExp;
8
+ export declare const SCRIPT_CLOSE: RegExp;
9
+ export declare function parseVueSFC(lines: Buffer): string;
10
+ interface ParseReturn {
11
+ ast: File;
12
+ file: FileHandle;
13
+ source: string;
14
+ }
15
+ export declare function getAstFromPath(path: string): Promise<ParseReturn>;
16
+ export interface ParsedFile {
17
+ ast: unknown;
18
+ source: string;
19
+ backend: LanguageBackend;
20
+ }
21
+ export declare function parseFile(path: string, registry: LanguageRegistry): Promise<ParsedFile>;
22
+ export {};
package/build/file.js CHANGED
@@ -56,3 +56,21 @@ export function getAstFromPath(path) {
56
56
  return { ast, file, source: fileContents };
57
57
  });
58
58
  }
59
+ export function parseFile(path, registry) {
60
+ return __awaiter(this, void 0, void 0, function* () {
61
+ const ext = extname(path);
62
+ const backend = registry.getByExtension(ext);
63
+ if (!backend) {
64
+ throw new Error(`No backend registered for extension "${ext}"`);
65
+ }
66
+ const file = yield open(path);
67
+ try {
68
+ const source = (yield file.readFile()).toString();
69
+ const ast = yield backend.parse(source, path);
70
+ return { ast, source, backend };
71
+ }
72
+ finally {
73
+ yield file.close();
74
+ }
75
+ });
76
+ }
@@ -0,0 +1,23 @@
1
+ import type { Match } from "./types.js";
2
+ export type { Match };
3
+ export interface LanguageBackend {
4
+ /** Short identifier used with --lang flag, e.g. "js", "python" */
5
+ readonly langId: string;
6
+ /** File extensions this backend handles, e.g. new Set([".py"]) */
7
+ readonly extensions: ReadonlySet<string>;
8
+ /** Human-readable name for error messages */
9
+ readonly name: string;
10
+ /** Parse source text into an opaque AST. Throws on unrecoverable parse error. */
11
+ parse(source: string, filePath: string): Promise<unknown> | unknown;
12
+ /**
13
+ * Run a selector query against a parsed AST. The selector is in the
14
+ * backend's native query syntax (after shorthand expansion). Returns matches
15
+ * with file/line/col/source fields.
16
+ */
17
+ query(ast: unknown, selector: string, source: string, filePath: string): Match[];
18
+ /**
19
+ * Validate a selector string. Expands shorthands internally, then checks
20
+ * syntax. Throws with a descriptive message on invalid syntax.
21
+ */
22
+ validateSelector(selector: string): void;
23
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import type { Match } from "./types.js";
3
+ export declare function searchRepo(selector: string, dir: string, registry?: import("./registry.js").LanguageRegistry): Promise<Match[]>;
package/build/main.js CHANGED
@@ -16,35 +16,34 @@ var __asyncValues = (this && this.__asyncValues) || function (o) {
16
16
  function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
17
17
  };
18
18
  import yargs from "yargs/yargs";
19
- import { createRequire } from "module";
20
19
  import { walkRepoFiles } from "./walk.js";
21
- import { getAstFromPath } from "./file.js";
22
- import { runQuery, validateSelector } from "./search.js";
20
+ import { parseFile } from "./file.js";
23
21
  import { formatMatches } from "./output.js";
24
- const require = createRequire(import.meta.url);
25
- const { version } = require("../package.json");
26
- export function searchRepo(selector, dir) {
27
- return __awaiter(this, void 0, void 0, function* () {
22
+ import { defaultRegistry } from "./registry.js";
23
+ import { JSLanguageBackend } from "./backends/js/index.js";
24
+ import { VERSION } from "./version.js";
25
+ // Register built-in JS/TS/Vue backend
26
+ defaultRegistry.register(new JSLanguageBackend());
27
+ export function searchRepo(selector_1, dir_1) {
28
+ return __awaiter(this, arguments, void 0, function* (selector, dir, registry = defaultRegistry) {
28
29
  var _a, e_1, _b, _c;
29
- validateSelector(selector); // throws early on invalid selector syntax
30
+ // Early validation when only one backend is registered (common JS-only case)
31
+ if (registry.allBackends.length === 1) {
32
+ registry.allBackends[0].validateSelector(selector);
33
+ }
30
34
  const results = [];
31
35
  try {
32
- for (var _d = true, _e = __asyncValues(walkRepoFiles(dir)), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
36
+ for (var _d = true, _e = __asyncValues(walkRepoFiles(dir, registry.allExtensions)), _f; _f = yield _e.next(), _a = _f.done, !_a; _d = true) {
33
37
  _c = _f.value;
34
38
  _d = false;
35
39
  const filePath = _c;
36
- let file;
37
40
  try {
38
- const result = yield getAstFromPath(filePath);
39
- file = result.file;
40
- const matches = runQuery(selector, result.ast, result.source, filePath);
41
+ const { ast, source, backend } = yield parseFile(filePath, registry);
42
+ const matches = backend.query(ast, selector, source, filePath);
41
43
  results.push(...matches);
42
44
  }
43
45
  catch (_g) {
44
- // skip unparseable files
45
- }
46
- finally {
47
- yield (file === null || file === void 0 ? void 0 : file.close());
46
+ // skip unparseable files / unsupported extensions
48
47
  }
49
48
  }
50
49
  }
@@ -64,7 +63,7 @@ const y = yargs(process.argv.slice(2))
64
63
  .command("$0 <query>", "Search a repo for AST patterns using CSS selector syntax", (yargs) => yargs
65
64
  .positional("query", {
66
65
  type: "string",
67
- describe: "esquery CSS selector string",
66
+ describe: "Query string (esquery CSS selector for JS/TS; tree-sitter S-expression for Python)",
68
67
  demandOption: true,
69
68
  })
70
69
  .option("dir", {
@@ -79,12 +78,47 @@ const y = yargs(process.argv.slice(2))
79
78
  describe: "output format: text (default), json, files",
80
79
  default: "text",
81
80
  choices: ["text", "json", "files"],
81
+ })
82
+ .option("lang", {
83
+ alias: "l",
84
+ type: "string",
85
+ describe: "restrict search to a specific language backend by langId (e.g. js, python)",
86
+ })
87
+ .option("plugin", {
88
+ alias: "p",
89
+ type: "string",
90
+ array: true,
91
+ describe: "load a language plugin package (e.g. ast-search-python)",
82
92
  }), (argv) => __awaiter(void 0, void 0, void 0, function* () {
83
- var _a;
84
- const { query, dir, format } = argv;
93
+ var _a, _b, _c;
94
+ const { query, dir, format, lang, plugin } = argv;
85
95
  try {
86
- const matches = yield searchRepo(query, dir);
87
- const isTTY = (_a = process.stdout.isTTY) !== null && _a !== void 0 ? _a : false;
96
+ // Load plugins before searching
97
+ for (const pkg of plugin !== null && plugin !== void 0 ? plugin : []) {
98
+ const mod = yield import(pkg);
99
+ const reg = (_a = mod.register) !== null && _a !== void 0 ? _a : (_b = mod.default) === null || _b === void 0 ? void 0 : _b.register;
100
+ if (typeof reg !== "function") {
101
+ throw new Error(`Plugin "${pkg}" does not export a register() function`);
102
+ }
103
+ reg(defaultRegistry);
104
+ }
105
+ // Build a scoped registry if --lang is specified
106
+ let registry = defaultRegistry;
107
+ if (lang) {
108
+ const backend = defaultRegistry.getByLangId(lang);
109
+ if (!backend) {
110
+ const available = defaultRegistry.allBackends.map((b) => b.langId).join(", ");
111
+ throw new Error(`Unknown language "${lang}". Available: ${available}`);
112
+ }
113
+ const { LanguageRegistry } = yield import("./registry.js");
114
+ const scoped = new LanguageRegistry();
115
+ scoped.register(backend);
116
+ registry = scoped;
117
+ // Always validate early when --lang restricts to a single backend
118
+ backend.validateSelector(query);
119
+ }
120
+ const matches = yield searchRepo(query, dir, registry);
121
+ const isTTY = (_c = process.stdout.isTTY) !== null && _c !== void 0 ? _c : false;
88
122
  for (const line of formatMatches(matches, isTTY, format)) {
89
123
  console.log(line);
90
124
  }
@@ -117,8 +151,16 @@ const y = yargs(process.argv.slice(2))
117
151
  "$0 'FunctionDeclaration[async=true]' --format files | xargs prettier --write",
118
152
  "reformat all files containing async functions",
119
153
  ],
154
+ [
155
+ "$0 'fn' --dir src --plugin ast-search-python",
156
+ "find all function definitions in Python files",
157
+ ],
158
+ [
159
+ "$0 '(class_definition)' --lang python --plugin ast-search-python",
160
+ "find all Python classes (tree-sitter S-expression syntax)",
161
+ ],
120
162
  ])
121
- .version(version)
163
+ .version(VERSION)
122
164
  .alias("version", "V")
123
165
  .help();
124
166
  if (process.env.NODE_ENV !== "test") {
@@ -0,0 +1,3 @@
1
+ import type { Match } from "./search.js";
2
+ export type OutputFormat = "text" | "json" | "files";
3
+ export declare function formatMatches(matches: Match[], isTTY: boolean, format?: OutputFormat): string[];
@@ -0,0 +1,3 @@
1
+ export type { Match } from "./types.js";
2
+ export type { LanguageBackend } from "./language.js";
3
+ export { LanguageRegistry } from "./registry.js";
@@ -0,0 +1 @@
1
+ export { LanguageRegistry } from "./registry.js";
@@ -0,0 +1,11 @@
1
+ import type { LanguageBackend } from "./language.js";
2
+ export declare class LanguageRegistry {
3
+ private readonly byExt;
4
+ private readonly byId;
5
+ register(backend: LanguageBackend): void;
6
+ getByExtension(ext: string): LanguageBackend | undefined;
7
+ getByLangId(langId: string): LanguageBackend | undefined;
8
+ get allExtensions(): ReadonlySet<string>;
9
+ get allBackends(): LanguageBackend[];
10
+ }
11
+ export declare const defaultRegistry: LanguageRegistry;
@@ -0,0 +1,25 @@
1
+ export class LanguageRegistry {
2
+ constructor() {
3
+ this.byExt = new Map();
4
+ this.byId = new Map();
5
+ }
6
+ register(backend) {
7
+ this.byId.set(backend.langId, backend);
8
+ for (const ext of backend.extensions) {
9
+ this.byExt.set(ext, backend);
10
+ }
11
+ }
12
+ getByExtension(ext) {
13
+ return this.byExt.get(ext);
14
+ }
15
+ getByLangId(langId) {
16
+ return this.byId.get(langId);
17
+ }
18
+ get allExtensions() {
19
+ return new Set(this.byExt.keys());
20
+ }
21
+ get allBackends() {
22
+ return [...this.byId.values()];
23
+ }
24
+ }
25
+ export const defaultRegistry = new LanguageRegistry();
@@ -0,0 +1,8 @@
1
+ import type { File } from "@babel/types";
2
+ import type { Match } from "./types.js";
3
+ export type { Match };
4
+ export declare const SHORTHANDS: Record<string, string>;
5
+ export declare function expandShorthands(selector: string): string;
6
+ export declare function validateSelector(selector: string): void;
7
+ export declare function normalizeOptionalChaining(node: any): void;
8
+ export declare function runQuery(selector: string, ast: File, source?: string, filename?: string): Match[];
@@ -0,0 +1,6 @@
1
+ export interface Match {
2
+ file: string;
3
+ line: number;
4
+ col: number;
5
+ source: string;
6
+ }
package/build/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare const VERSION = "0.3.0";
@@ -0,0 +1,2 @@
1
+ // Auto-updated by semantic-release via @semantic-release/exec — do not edit manually.
2
+ export const VERSION = "0.3.0";
@@ -0,0 +1 @@
1
+ export declare function walkRepoFiles(dir: string, extensions: ReadonlySet<string>): AsyncGenerator<string>;
package/build/walk.js CHANGED
@@ -25,16 +25,7 @@ var __asyncGenerator = (this && this.__asyncGenerator) || function (thisArg, _ar
25
25
  };
26
26
  import { readdir } from "node:fs/promises";
27
27
  import { extname, join } from "node:path";
28
- const SUPPORTED_EXTENSIONS = new Set([
29
- ".js",
30
- ".ts",
31
- ".jsx",
32
- ".tsx",
33
- ".mjs",
34
- ".cjs",
35
- ".vue",
36
- ]);
37
- export function walkRepoFiles(dir) {
28
+ export function walkRepoFiles(dir, extensions) {
38
29
  return __asyncGenerator(this, arguments, function* walkRepoFiles_1() {
39
30
  const entries = yield __await(readdir(dir, { withFileTypes: true }));
40
31
  for (const entry of entries) {
@@ -44,9 +35,9 @@ export function walkRepoFiles(dir) {
44
35
  continue;
45
36
  const fullPath = join(dir, entry.name);
46
37
  if (entry.isDirectory()) {
47
- yield __await(yield* __asyncDelegator(__asyncValues(walkRepoFiles(fullPath))));
38
+ yield __await(yield* __asyncDelegator(__asyncValues(walkRepoFiles(fullPath, extensions))));
48
39
  }
49
- else if (entry.isFile() && SUPPORTED_EXTENSIONS.has(extname(entry.name))) {
40
+ else if (entry.isFile() && extensions.has(extname(entry.name))) {
50
41
  yield yield __await(fullPath);
51
42
  }
52
43
  }
package/package.json CHANGED
@@ -1,15 +1,25 @@
1
1
  {
2
2
  "name": "ast-search",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "",
5
- "main": "main.js",
5
+ "main": "build/main.js",
6
+ "exports": {
7
+ ".": {
8
+ "import": "./build/main.js",
9
+ "types": "./build/main.d.ts"
10
+ },
11
+ "./plugin": {
12
+ "import": "./build/plugin.js",
13
+ "types": "./build/plugin.d.ts"
14
+ }
15
+ },
6
16
  "scripts": {
7
17
  "dev": "npx tsc --watch",
8
18
  "debug:vue": "npx tsc && node ./build/main.js 'setup > this' --dir src/__tests__/dummyFiles",
9
19
  "debug:react": "npx tsc && node ./build/main.js 'useState' --dir src/__tests__/dummyFiles",
10
20
  "debug:empty": "npx tsc && node ./build/main.js 'ValidationsTab > this' --dir src/__tests__/dummyFiles",
11
21
  "debug:basics": "npx tsc && node ./build/main.js 'arrowFunction' --dir src/__tests__/dummyFiles",
12
- "test": "jest",
22
+ "test": "jest --testPathPattern='src/__tests__'",
13
23
  "build": "npx tsc && cp build/main.js ast-search",
14
24
  "prepublishOnly": "npx tsc"
15
25
  },
@@ -25,6 +35,12 @@
25
35
  "url": "https://github.com/willey-shiplet/ast-search.git"
26
36
  },
27
37
  "packageManager": "pnpm@10.33.0",
38
+ "pnpm": {
39
+ "onlyBuiltDependencies": [
40
+ "tree-sitter",
41
+ "tree-sitter-python"
42
+ ]
43
+ },
28
44
  "dependencies": {
29
45
  "@babel/parser": "^7.24.4",
30
46
  "@babel/types": "^7.24.0",
@@ -36,7 +52,9 @@
36
52
  },
37
53
  "files": [
38
54
  "build/*.js",
55
+ "build/*.d.ts",
39
56
  "build/helpers/*.js",
57
+ "build/backends/**/*.js",
40
58
  "README.md"
41
59
  ],
42
60
  "pkg": {
@@ -49,6 +67,7 @@
49
67
  "@babel/preset-typescript": "^7.24.1",
50
68
  "@jest/globals": "^29.7.0",
51
69
  "@semantic-release/changelog": "^6.0.3",
70
+ "@semantic-release/exec": "^6.0.3",
52
71
  "@semantic-release/git": "^10.0.1",
53
72
  "@semantic-release/github": "^10.0.0",
54
73
  "@semantic-release/npm": "^13.1.3",