diff --git a/package-lock.json b/package-lock.json index c88b3c6a..609092f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -197,7 +197,6 @@ "version": "0.40.0", "resolved": "https://registry.npmjs.org/@ast-grep/napi/-/napi-0.40.0.tgz", "integrity": "sha512-tq6nO/8KwUF/mHuk1ECaAOSOlz2OB/PmygnvprJzyAHGRVzdcffblaOOWe90M9sGz5MAasXoF+PTcayQj9TKKA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 10" @@ -221,7 +220,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -238,7 +236,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -255,7 +252,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -272,7 +268,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -289,7 +284,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -306,7 +300,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -323,7 +316,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -340,7 +332,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -357,7 +348,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ @@ -402,7 +392,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.5", @@ -1540,6 +1529,10 @@ "resolved": "recipes/slow-buffer-to-buffer-alloc-unsafe-slow", "link": true }, + "node_modules/@nodejs/tls-securepair-to-tlssocket": { + "resolved": "recipes/tls-securepair-to-tlssocket", + "link": true + }, "node_modules/@nodejs/tmpdir-to-tmpdir": { "resolved": "recipes/tmpdir-to-tmpdir", "link": true @@ -1578,7 +1571,6 @@ "resolved": "https://registry.npmjs.org/@octokit/core/-/core-5.2.2.tgz", "integrity": "sha512-/g2d4sW9nUDJOMz3mabVQvOGhVa4e/BN/Um7yca9Bb2XTzPPnfTWHWQg+IsEYO7M3Vx+EXvaM/I2pJWIMun1bg==", "license": "MIT", - "peer": true, "dependencies": { "@octokit/auth-token": "^4.0.0", "@octokit/graphql": "^7.1.0", @@ -1776,7 +1768,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -2095,7 +2086,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.19", "caniuse-lite": "^1.0.30001751", @@ -4436,6 +4426,18 @@ "@codemod.com/jssg-types": "^1.3.0" } }, + "recipes/tls-securepair-to-tlssocket": { + "name": "@nodejs/tls-securepair-to-tlssocket", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@ast-grep/napi": "^0.40.0", + "@nodejs/codemod-utils": "*" + }, + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.9" + } + }, "recipes/tmpdir-to-tmpdir": { "name": "@nodejs/tmpdir-to-tmpdir", "version": "1.0.0", diff --git a/recipes/tls-securepair-to-tlssocket/README.md b/recipes/tls-securepair-to-tlssocket/README.md new file mode 100644 index 00000000..46bf06e2 --- /dev/null +++ b/recipes/tls-securepair-to-tlssocket/README.md @@ -0,0 +1,89 @@ +# tls-securepair-to-tlssocket + +Codemod to migrate from the deprecated `tls.SecurePair` class to `tls.TLSSocket` in Node.js applications. `SecurePair` was deprecated and subsequently removed in favor of `TLSSocket`. + +## What it does + +This codemod transforms usages of `tls.SecurePair` into `tls.TLSSocket`. Since `TLSSocket` wraps an existing socket, the codemod injects a `socket` argument that you may need to define or bind in your context. + +Key transformations: +- **Constructor:** Replaces `new SecurePair()` with `new TLSSocket(socket)`. +- **Imports:** Updates `require` and `import` statements from `SecurePair` to `TLSSocket`. +- **Renaming:** Intelligently renames variables (e.g., `pair` → `socket`, `securePairInstance` → `socketInstance`) while preserving CamelCase. +- **Cleanup:** Removes deprecated property accesses like `.cleartext` and `.encrypted`. +- **Annotations:** Adds comments to highlight where manual API verification is needed. + +## Supports + +- **Module Systems:** + - CommonJS: `const tls = require('node:tls')` / `const { SecurePair } = ...` + - ESM: `import tls from 'node:tls'` / `import { SecurePair } ...` +- **Variable Renaming:** + - Updates variable declarations: `const pair = ...` → `const socket = ...` + - Updates references deep in the scope: `pair.on('error')` → `socket.on('error')` + - Handles naming variations: `myPair` → `mySocket`, `securePair` → `secureSocket`. +- **Cleanup:** + - Identifies and removes lines accessing `cleartext` or `encrypted` properties. +- **Namespace Handling:** + - Supports both `new tls.SecurePair()` and `new SecurePair()`. + +## Examples + +### Case 1: CommonJS with namespace access + +```diff + const tls = require('node:tls'); + +- const pair = new tls.SecurePair(); +- const encrypted = pair.encrypted; ++ const socket = new tls.TLSSocket(socket); +``` + +### Case 2: ESM with destructuring + +```diff +- import { SecurePair } from 'node:tls'; ++ import { TLSSocket } from 'node:tls'; + +- const myPair = new SecurePair(); +- myPair.cleartext.write('hello'); ++ const mySocket = new TLSSocket(socket); ++ mySocket.write('hello'); +``` + +### Case 3: Variable renaming across scope + +```diff + const tls = require('node:tls'); + +- const securePair = new tls.SecurePair(); ++ const secureSocket = new tls.TLSSocket(socket); + +- securePair.on('error', (err) => { ++ secureSocket.on('error', (err) => { + console.error(err); + }); +``` + +### Case 4: Multiple variables with cleanup + +```diff +- const { SecurePair } = require('node:tls'); ++ const { TLSSocket } = require('node:tls'); + +- const pair = new SecurePair(); +- const cleartext = pair.cleartext; ++ const socket = new TLSSocket(socket); +``` + +## Warning + +The tls.TLSSocket constructor requires an existing socket instance (net.Socket) as an argument. This codemod automatically inserts socket as the argument: +JavaScript + +```js +``` +new TLSSocket(socket) +``` + +You must ensure that a variable named socket exists in the scope or rename it to match your existing socket variable (e.g., clientSocket, stream, etc.). diff --git a/recipes/tls-securepair-to-tlssocket/codemod.yaml b/recipes/tls-securepair-to-tlssocket/codemod.yaml new file mode 100644 index 00000000..98c9ea41 --- /dev/null +++ b/recipes/tls-securepair-to-tlssocket/codemod.yaml @@ -0,0 +1,23 @@ +schema_version: "1.0" +name: "@nodejs/tls-securepair-to-tlssocket" +version: "1.0.0" +description: Migrate usages of `tls.SecurePair` to `tls.TLSSocket` where possible. +author: Maxime Devillet +license: MIT +workflow: workflow.yaml +category: migration + +targets: + languages: + - javascript + - typescript + +keywords: + - transformation + - migration + - tls + - securepair + +registry: + access: public + visibility: public diff --git a/recipes/tls-securepair-to-tlssocket/package.json b/recipes/tls-securepair-to-tlssocket/package.json new file mode 100644 index 00000000..cb68fab9 --- /dev/null +++ b/recipes/tls-securepair-to-tlssocket/package.json @@ -0,0 +1,25 @@ +{ + "name": "@nodejs/tls-securepair-to-tlssocket", + "version": "1.0.0", + "description": "Migrate usages of tls.SecurePair to tls.TLSSocket.", + "type": "module", + "scripts": { + "test": "npx codemod jssg test -l typescript ./src/workflow.ts ./" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/nodejs/userland-migrations.git", + "directory": "recipes/tls-securepair-to-tlssocket", + "bugs": "https://github.com/nodejs/userland-migrations/issues" + }, + "author": "Maxime Devillet", + "license": "MIT", + "homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/tls-securepair-to-tlssocket/README.md", + "devDependencies": { + "@codemod.com/jssg-types": "^1.0.9" + }, + "dependencies": { + "@nodejs/codemod-utils": "*", + "@ast-grep/napi": "^0.40.0" + } +} \ No newline at end of file diff --git a/recipes/tls-securepair-to-tlssocket/src/workflow.ts b/recipes/tls-securepair-to-tlssocket/src/workflow.ts new file mode 100644 index 00000000..beffc15f --- /dev/null +++ b/recipes/tls-securepair-to-tlssocket/src/workflow.ts @@ -0,0 +1,110 @@ +import { getNodeImportStatements } from "@nodejs/codemod-utils/ast-grep/import-statement"; +import { getNodeRequireCalls } from "@nodejs/codemod-utils/ast-grep/require-call"; +import { updateBinding } from "@nodejs/codemod-utils/ast-grep/update-binding"; +import { removeLines } from "@nodejs/codemod-utils/ast-grep/remove-lines"; +import type { Edit, SgRoot, Range, SgNode } from "@codemod.com/jssg-types/main"; +import type Js from "@codemod.com/jssg-types/langs/javascript"; + +function getClosest(node: SgNode, kinds: string[]): SgNode | null { + let current = node.parent(); + + while (current) { + if (kinds.includes(current.kind())) return current; + current = current.parent(); + } + + return null; +} + +export default function transform(root: SgRoot): string | null { + const rootNode = root.root(); + const edits: Edit[] = []; + const linesToRemove: Range[] = []; + + const importNodes = [ + ...getNodeImportStatements(root, "tls"), + ...getNodeRequireCalls(root, "tls") + ]; + for (const node of importNodes) { + const change = updateBinding(node, { old: "SecurePair", new: "TLSSocket" }); + if (change?.edit) edits.push(change.edit); + if (change?.lineToRemove) linesToRemove.push(change.lineToRemove); + } + + const newExpressions = rootNode.findAll({ + rule: { + kind: "new_expression", + has: { + any: [ + { kind: "member_expression", has: { field: "property", regex: "^SecurePair$" } }, + { kind: "identifier", regex: "^SecurePair$" } + ] + } + } + }); + + for (const node of newExpressions) { + const callee = node.field("constructor"); + if (!callee) continue; + + let newConstructorName = "TLSSocket"; + if (callee.kind() === "member_expression") { + const object = callee.field("object"); + if (object) { + newConstructorName = `${object.text()}.TLSSocket`; + } + } + + edits.push(node.replace(`new ${newConstructorName}(socket)`)); + const declarator = getClosest(node, ["variable_declarator"]); + if (declarator) { + const idNode = declarator.field("name"); + if (idNode) { + const oldName = idNode.text(); + let newName = "socket"; + if (oldName !== "pair" && oldName !== "SecurePair") { + if (oldName.includes("Pair")) newName = oldName.replace("Pair", "Socket"); + else if (oldName.includes("pair")) newName = oldName.replace("pair", "socket"); + } + + const obsoleteUsages = rootNode.findAll({ + rule: { + kind: "member_expression", + all: [ + { has: { field: "object", regex: `^${oldName}$` } }, + { has: { field: "property", regex: "^(cleartext|encrypted)$" } } + ] + } + }); + for (const usage of obsoleteUsages) { + let statement = getClosest(usage, ["lexical_declaration", "expression_statement"]); + if (statement) linesToRemove.push(statement.range()); + } + + edits.push(idNode.replace(newName)); + const references = rootNode.findAll({ + rule: { kind: "identifier", regex: `^${oldName}$` } + }); + + for (const ref of references) { + const parent = ref.parent(); + if (!parent) continue; + + const parentKind = parent.kind(); + if (parentKind === "member_expression") { + const property = parent.field("property"); + if (property && property.id() === ref.id()) continue; + } + if (parentKind === "import_specifier" || parentKind === "shorthand_property_identifier_pattern") continue; + if (ref.id() === idNode.id()) continue; + + edits.push(ref.replace(newName)); + } + } + } + } + + const sourceCode = rootNode.commitEdits(edits); + + return removeLines(sourceCode, linesToRemove); +} \ No newline at end of file diff --git a/recipes/tls-securepair-to-tlssocket/tests/expected/basic-test-file-3.js b/recipes/tls-securepair-to-tlssocket/tests/expected/basic-test-file-3.js new file mode 100644 index 00000000..d8c80c4f --- /dev/null +++ b/recipes/tls-securepair-to-tlssocket/tests/expected/basic-test-file-3.js @@ -0,0 +1,11 @@ +const tls = require('node:tls'); +const fs = require('fs'); // Code non lié + +function createSecureConnection() { + // Using tls.SecurePair constructor + const socket = new tls.TLSSocket(socket); + + // Ces lignes doivent disparaître + + return socket; +} \ No newline at end of file diff --git a/recipes/tls-securepair-to-tlssocket/tests/expected/file-1.js b/recipes/tls-securepair-to-tlssocket/tests/expected/file-1.js new file mode 100644 index 00000000..cea1840a --- /dev/null +++ b/recipes/tls-securepair-to-tlssocket/tests/expected/file-1.js @@ -0,0 +1,8 @@ +const tls = require('node:tls'); + +// Using tls.SecurePair constructor +const socket = new tls.TLSSocket(socket); + +// Direct import +const { TLSSocket } = require('node:tls'); +const socket2 = new TLSSocket(socket); \ No newline at end of file diff --git a/recipes/tls-securepair-to-tlssocket/tests/expected/file-2.js b/recipes/tls-securepair-to-tlssocket/tests/expected/file-2.js new file mode 100644 index 00000000..ef8d2b1f --- /dev/null +++ b/recipes/tls-securepair-to-tlssocket/tests/expected/file-2.js @@ -0,0 +1,8 @@ +import tls from 'node:tls'; + +// Using tls.SecurePair constructor +const socket = new tls.TLSSocket(socket); + +// Direct import +import { TLSSocket } from 'node:tls'; +const socket2 = new TLSSocket(socket); \ No newline at end of file diff --git a/recipes/tls-securepair-to-tlssocket/tests/expected/test-crucial-file.js b/recipes/tls-securepair-to-tlssocket/tests/expected/test-crucial-file.js new file mode 100644 index 00000000..bfec68e3 --- /dev/null +++ b/recipes/tls-securepair-to-tlssocket/tests/expected/test-crucial-file.js @@ -0,0 +1,10 @@ +const { TLSSocket } = require('node:tls'); +const { unrelated } = require('other-module'); + +const socket = new TLSSocket(socket); + +socket.on('error', (err) => { + console.error(err); +}); + +// Obsolete properties \ No newline at end of file diff --git a/recipes/tls-securepair-to-tlssocket/tests/expected/test-edge-variable-name-file.js b/recipes/tls-securepair-to-tlssocket/tests/expected/test-edge-variable-name-file.js new file mode 100644 index 00000000..97a7dbb8 --- /dev/null +++ b/recipes/tls-securepair-to-tlssocket/tests/expected/test-edge-variable-name-file.js @@ -0,0 +1,7 @@ +const { TLSSocket } = require('node:tls'); + +// Completely arbitrary variable name +const socket = new TLSSocket(socket); + +// Usage +socket.doSomething(); \ No newline at end of file diff --git a/recipes/tls-securepair-to-tlssocket/tests/expected/test-esm-file.js b/recipes/tls-securepair-to-tlssocket/tests/expected/test-esm-file.js new file mode 100644 index 00000000..69043738 --- /dev/null +++ b/recipes/tls-securepair-to-tlssocket/tests/expected/test-esm-file.js @@ -0,0 +1,8 @@ +import tls from 'node:tls'; +import { TLSSocket } from 'node:tls'; + +// Case 1: Via namespace +const socket1 = new tls.TLSSocket(socket); + +// Case 2: Direct +const socket2 = new TLSSocket(socket); \ No newline at end of file diff --git a/recipes/tls-securepair-to-tlssocket/tests/expected/test-multiple-variable-file.js b/recipes/tls-securepair-to-tlssocket/tests/expected/test-multiple-variable-file.js new file mode 100644 index 00000000..cfb06417 --- /dev/null +++ b/recipes/tls-securepair-to-tlssocket/tests/expected/test-multiple-variable-file.js @@ -0,0 +1,7 @@ +const tls = require('node:tls'); + +const socket = new tls.TLSSocket(socket); +const mySocket = new tls.TLSSocket(socket); +const secureSocketInstance = new tls.TLSSocket(socket); + +// Specific cleanup for each variable \ No newline at end of file diff --git a/recipes/tls-securepair-to-tlssocket/tests/expected/test-scoping-file.js b/recipes/tls-securepair-to-tlssocket/tests/expected/test-scoping-file.js new file mode 100644 index 00000000..433ca37d --- /dev/null +++ b/recipes/tls-securepair-to-tlssocket/tests/expected/test-scoping-file.js @@ -0,0 +1,14 @@ +const tls = require('node:tls'); + +class ConnectionManager { + constructor() { + if (true) { + const socket = new tls.TLSSocket(socket); + this.init(socket); + + if (socket.cleartext) { + console.log("cleaning"); + } + } + } +} \ No newline at end of file diff --git a/recipes/tls-securepair-to-tlssocket/tests/input/basic-test-file-3.js b/recipes/tls-securepair-to-tlssocket/tests/input/basic-test-file-3.js new file mode 100644 index 00000000..a17b4487 --- /dev/null +++ b/recipes/tls-securepair-to-tlssocket/tests/input/basic-test-file-3.js @@ -0,0 +1,13 @@ +const tls = require('node:tls'); +const fs = require('fs'); // Code non lié + +function createSecureConnection() { + // Using tls.SecurePair constructor + const pair = new tls.SecurePair(); + + // Ces lignes doivent disparaître + const cleartext = pair.cleartext; + const encrypted = pair.encrypted; + + return pair; +} \ No newline at end of file diff --git a/recipes/tls-securepair-to-tlssocket/tests/input/file-1.js b/recipes/tls-securepair-to-tlssocket/tests/input/file-1.js new file mode 100644 index 00000000..9ce8efdd --- /dev/null +++ b/recipes/tls-securepair-to-tlssocket/tests/input/file-1.js @@ -0,0 +1,10 @@ +const tls = require('node:tls'); + +// Using tls.SecurePair constructor +const pair = new tls.SecurePair(); +const cleartext = pair.cleartext; +const encrypted = pair.encrypted; + +// Direct import +const { SecurePair } = require('node:tls'); +const pair2 = new SecurePair(); \ No newline at end of file diff --git a/recipes/tls-securepair-to-tlssocket/tests/input/file-2.js b/recipes/tls-securepair-to-tlssocket/tests/input/file-2.js new file mode 100644 index 00000000..fe8c30bc --- /dev/null +++ b/recipes/tls-securepair-to-tlssocket/tests/input/file-2.js @@ -0,0 +1,10 @@ +import tls from 'node:tls'; + +// Using tls.SecurePair constructor +const pair = new tls.SecurePair(); +const cleartext = pair.cleartext; +const encrypted = pair.encrypted; + +// Direct import +import { SecurePair } from 'node:tls'; +const pair2 = new SecurePair(); \ No newline at end of file diff --git a/recipes/tls-securepair-to-tlssocket/tests/input/test-crucial-file.js b/recipes/tls-securepair-to-tlssocket/tests/input/test-crucial-file.js new file mode 100644 index 00000000..bdf18fb4 --- /dev/null +++ b/recipes/tls-securepair-to-tlssocket/tests/input/test-crucial-file.js @@ -0,0 +1,12 @@ +const { SecurePair } = require('node:tls'); +const { unrelated } = require('other-module'); + +const pair = new SecurePair(); + +pair.on('error', (err) => { + console.error(err); +}); + +// Obsolete properties +console.log(pair.cleartext); +console.log(pair.encrypted); \ No newline at end of file diff --git a/recipes/tls-securepair-to-tlssocket/tests/input/test-edge-variable-name-file.js b/recipes/tls-securepair-to-tlssocket/tests/input/test-edge-variable-name-file.js new file mode 100644 index 00000000..95934a99 --- /dev/null +++ b/recipes/tls-securepair-to-tlssocket/tests/input/test-edge-variable-name-file.js @@ -0,0 +1,8 @@ +const { SecurePair } = require('node:tls'); + +// Completely arbitrary variable name +const item = new SecurePair(); + +// Usage +item.doSomething(); +item.cleartext.read(); \ No newline at end of file diff --git a/recipes/tls-securepair-to-tlssocket/tests/input/test-esm-file.js b/recipes/tls-securepair-to-tlssocket/tests/input/test-esm-file.js new file mode 100644 index 00000000..cc3ea4b8 --- /dev/null +++ b/recipes/tls-securepair-to-tlssocket/tests/input/test-esm-file.js @@ -0,0 +1,10 @@ +import tls from 'node:tls'; +import { SecurePair } from 'node:tls'; + +// Case 1: Via namespace +const pair1 = new tls.SecurePair(); +const t1 = pair1.cleartext; + +// Case 2: Direct +const pair2 = new SecurePair(); +const t2 = pair2.encrypted; \ No newline at end of file diff --git a/recipes/tls-securepair-to-tlssocket/tests/input/test-multiple-variable-file.js b/recipes/tls-securepair-to-tlssocket/tests/input/test-multiple-variable-file.js new file mode 100644 index 00000000..59b6a641 --- /dev/null +++ b/recipes/tls-securepair-to-tlssocket/tests/input/test-multiple-variable-file.js @@ -0,0 +1,9 @@ +const tls = require('node:tls'); + +const pair = new tls.SecurePair(); +const myPair = new tls.SecurePair(); +const securePairInstance = new tls.SecurePair(); + +// Specific cleanup for each variable +pair.cleartext.write('hello'); +myPair.encrypted.write('world'); \ No newline at end of file diff --git a/recipes/tls-securepair-to-tlssocket/tests/input/test-scoping-file.js b/recipes/tls-securepair-to-tlssocket/tests/input/test-scoping-file.js new file mode 100644 index 00000000..ded038b5 --- /dev/null +++ b/recipes/tls-securepair-to-tlssocket/tests/input/test-scoping-file.js @@ -0,0 +1,14 @@ +const tls = require('node:tls'); + +class ConnectionManager { + constructor() { + if (true) { + const pair = new tls.SecurePair(); + this.init(pair); + + if (pair.cleartext) { + console.log("cleaning"); + } + } + } +} \ No newline at end of file diff --git a/recipes/tls-securepair-to-tlssocket/workflow.yaml b/recipes/tls-securepair-to-tlssocket/workflow.yaml new file mode 100644 index 00000000..6b7a4d32 --- /dev/null +++ b/recipes/tls-securepair-to-tlssocket/workflow.yaml @@ -0,0 +1,25 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/codemod-com/codemod/refs/heads/main/schemas/workflow.json + +version: "1" + +nodes: + - id: apply-transforms + name: Apply AST Transformations + type: automatic + steps: + - name: "Handle deprecation: migrate `tls.SecurePair` to `tls.TLSSocket`." + js-ast-grep: + js_file: src/workflow.ts + base_path: . + include: + - "**/*.cjs" + - "**/*.js" + - "**/*.jsx" + - "**/*.mjs" + - "**/*.cts" + - "**/*.mts" + - "**/*.ts" + - "**/*.tsx" + exclude: + - "**/node_modules/**" + language: typescript