Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions recipes/tls-create-secure-pair-to-tls-socket/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# `tls.createSecurePair` deprecation DEP0064

This recipe transforms the usage from the deprecated `createSecurePair()` to `TLSSocket()`.

See [DEP0064](https://nodejs.org/api/deprecations.html#dep0064-tlscreatesecurepair).

## Examples

### Case 1: Basic `createSecurePair` usage

**Before:**
```js
const { createSecurePair } = require('node:tls');

const pair = createSecurePair(credentials);
```

**After:**
```js
const { TLSSocket } = require('node:tls');

const socket = new TLSSocket(underlyingSocket, { secureContext: credentials });
```

---

### Case 2: Namespace import

**Before:**
```js
const tls = require('node:tls');

const pair = tls.createSecurePair(credentials);
```

**After:**
```js
const tls = require('node:tls');

const socket = new tls.TLSSocket(underlyingSocket, { secureContext: credentials });
```

---

### Case 3: With server context

**Before:**
```js
const { createSecurePair } = require('node:tls');

const pair = createSecurePair(credentials, true, true, false);
```

**After:**
```js
const { TLSSocket } = require('node:tls');

const socket = new TLSSocket(underlyingSocket, {
secureContext: credentials,
isServer: true,
requestCert: true,
rejectUnauthorized: false
});
```

---

### Case 4: ESM import

**Before:**
```js
import { createSecurePair } from 'node:tls';

const pair = createSecurePair(credentials);
```

**After:**
```js
import { TLSSocket } from 'node:tls';

const socket = new TLSSocket(underlyingSocket, { secureContext: credentials });
```

---

### Case 5: ESM namespace import

**Before:**
```js
import * as tls from 'node:tls';

const pair = tls.createSecurePair(credentials);
```

**After:**
```js
import * as tls from 'node:tls';

const socket = new tls.TLSSocket(underlyingSocket, { secureContext: credentials });
```

---

### Case 6: Mixed usage with other TLS functions

**Before:**
```js
const { createSecurePair, createServer } = require('node:tls');

const pair = createSecurePair(credentials);
const server = createServer(options);
```

**After:**
```js
const { TLSSocket, createServer } = require('node:tls');

const socket = new TLSSocket(underlyingSocket, { secureContext: credentials });
const server = createServer(options);
```
24 changes: 24 additions & 0 deletions recipes/tls-create-secure-pair-to-tls-socket/codemod.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
schema_version: "1.0"
name: "@nodejs/tls-create-secure-pair-to-tls-socket"
version: 1.0.0
description: Handle DEP0064 by transforming `createSecurePair` to `TLSSocket`
author: Leonardo Trevizo
license: MIT
workflow: workflow.yaml
category: migration

targets:
languages:
- javascript
- typescript

keywords:
- transformation
- migration
- tls
- createSecurePair
- TLSSocket

registry:
access: public
visibility: public
24 changes: 24 additions & 0 deletions recipes/tls-create-secure-pair-to-tls-socket/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "@nodejs/tls-create-secure-pair-to-tls-socket",
"version": "1.0.0",
"description": "Handle DEP0064 replacing `tls.createSecurePair()` with `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-create-secure-pair-to-tls-socket",
"bugs": "https://github.com/nodejs/userland-migrations/issues"
},
"author": "Leo Trevizo",
"license": "MIT",
"homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/tls-create-secure-pair-to-tls-socket/README.md",
"devDependencies": {
"@codemod.com/jssg-types": "^1.0.9"
},
"dependencies": {
"@nodejs/codemod-utils": "*"
}
}
172 changes: 172 additions & 0 deletions recipes/tls-create-secure-pair-to-tls-socket/src/workflow.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import type { SgRoot, SgNode, Edit } from '@codemod.com/jssg-types/main';
import type JS from '@codemod.com/jssg-types/langs/javascript';
import { getNodeImportStatements } from '@nodejs/codemod-utils/ast-grep/import-statement';
import { getNodeRequireCalls } from '@nodejs/codemod-utils/ast-grep/require-call';
import { resolveBindingPath } from '@nodejs/codemod-utils/ast-grep/resolve-binding-path';

type CallSite = { call: SgNode<JS>; binding: string };

export default function transform(root: SgRoot<JS>): string | null {
const rootNode = root.root();
const tlsStmts = [
...getNodeImportStatements(root, 'node:tls'),
...getNodeImportStatements(root, 'tls'),
...getNodeRequireCalls(root, 'node:tls'),
...getNodeRequireCalls(root, 'tls'),
];
if (tlsStmts.length === 0) return null;

const cspBindings = unique(
tlsStmts
.map(s => resolveBindingPath(s as unknown as SgNode<JS>, '$.createSecurePair'))
.filter(Boolean) as string[]
);
if (cspBindings.length === 0) return null;

const callSites = findCreateSecurePairCalls(rootNode, cspBindings);
const edits: Edit[] = [];

for (const { call, binding } of callSites) {
const a = getText(call.getMatch('A'));
const b = getText(call.getMatch('B'));
const c = getText(call.getMatch('C'));
const d = getText(call.getMatch('D'));
const options = buildOptions(a, b, c, d);
const isNamespace = binding.includes('.');
const replacement = isNamespace
? `new ${binding.replace(/\.createSecurePair$/, '.TLSSocket')}(underlyingSocket, ${options})`
: `new TLSSocket(underlyingSocket, ${options})`;
edits.push(call.replace(replacement));
}

edits.push(...renamePairAssignedVariables(rootNode, cspBindings));
edits.push(...rewriteTlsImports(rootNode));
if (edits.length === 0) return null;
return rootNode.commitEdits(edits);
Comment on lines +35 to +37
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
edits.push(...rewriteTlsImports(rootNode));
if (edits.length === 0) return null;
return rootNode.commitEdits(edits);
edits.push(...rewriteTlsImports(rootNode));
if (edits.length === 0) return null;
return rootNode.commitEdits(edits);

}

function findCreateSecurePairCalls(rootNode: SgNode<JS>, bindings: string[]): CallSite[] {
return bindings.flatMap(binding =>
rootNode
.findAll({
rule: {
any: [
{ pattern: `${binding}($A, $B, $C, $D)` },
{ pattern: `${binding}($A, $B, $C)` },
{ pattern: `${binding}($A, $B)` },
{ pattern: `${binding}($A)` },
{ pattern: `${binding}()` },
],
},
})
.map(n => ({ call: n as SgNode<JS>, binding }))
);
}

function buildOptions(a?: string | null, b?: string | null, c?: string | null, d?: string | null): string {
const kv: string[] = [];
if (a) kv.push(`secureContext: ${a}`);
if (b) kv.push(`isServer: ${b}`);
if (c) kv.push(`requestCert: ${c}`);
if (d) kv.push(`rejectUnauthorized: ${d}`);
Comment on lines +65 to +70
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
function buildOptions(a?: string | null, b?: string | null, c?: string | null, d?: string | null): string {
const kv: string[] = [];
if (a) kv.push(`secureContext: ${a}`);
if (b) kv.push(`isServer: ${b}`);
if (c) kv.push(`requestCert: ${c}`);
if (d) kv.push(`rejectUnauthorized: ${d}`);
function buildOptions(
secureContext?: string | null,
isServer?: string | null,
requestCert?: string | null,
rejectUnauthorized?: string | null,
) {
const kv: string[] = [];
if (secureContext) kv.push(`secureContext: ${secureContext}`);
if (isServer) kv.push(`isServer: ${isServer}`);
if (requestCert) kv.push(`requestCert: ${requestCert}`);
if (rejectUnauthorized) kv.push(`rejectUnauthorized: ${rejectUnauthorized}`);

But how can a subsequent argument be supplied when the former isn't without tricking this? Is that why you have the nulls? If so, I think that's not ideal, and passing an object would be better:

Suggested change
function buildOptions(a?: string | null, b?: string | null, c?: string | null, d?: string | null): string {
const kv: string[] = [];
if (a) kv.push(`secureContext: ${a}`);
if (b) kv.push(`isServer: ${b}`);
if (c) kv.push(`requestCert: ${c}`);
if (d) kv.push(`rejectUnauthorized: ${d}`);
function buildOptions({
secureContext,
isServer,
requestCert,
rejectUnauthorized,
}: {
secureContext?: string,
isServer?: string,
requestCert?: string,
rejectUnauthorized?: string,
}) {
const kv: string[] = [];
if (secureContext) kv.push(`secureContext: ${secureContext}`);
if (isServer) kv.push(`isServer: ${isServer}`);
if (requestCert) kv.push(`requestCert: ${requestCert}`);
if (rejectUnauthorized) kv.push(`rejectUnauthorized: ${rejectUnauthorized}`);

return `{ ${kv.join(', ')} }`;
}

function getText(node: SgNode<JS> | undefined): string | null {
const t = node?.text()?.trim();
return t || null;
}
Comment on lines +74 to +77
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you don't really want to literally pass undefined (getText(undefined), you just want node to be optional (getText(maybeUndefined)).

Suggested change
function getText(node: SgNode<JS> | undefined): string | null {
const t = node?.text()?.trim();
return t || null;
}
function getText(node?: SgNode<JS>) {
return node?.text()?.trim() || null;
}

Why return null at all though instead of just an empty string or undefined?


function unique<T>(arr: T[]): T[] {
return Array.from(new Set(arr));
}

function renamePairAssignedVariables(rootNode: SgNode<JS>, bindings: string[]): Edit[] {
const edits: Edit[] = [];
for (const binding of bindings) {
const decls = rootNode.findAll({
rule: {
kind: 'variable_declarator',
has: {
field: 'value',
kind: 'call_expression',
pattern: `${binding}($$$ARGS)`,
},
},
});
for (const decl of decls) {
const name = decl.field('name');
if (name && name.kind() === 'identifier' && name.text() === 'pair') {
edits.push(name.replace('socket'));
}
}
}
return edits;
Comment on lines +103 to +104
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
return edits;
}
return edits;

}


function rewriteTlsImports(rootNode: SgNode<JS>): Edit[] {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function is quite large. I think it would be better to break each of its sections into their own functions.

const edits: Edit[] = [];
const tlsStmts = rootNode.findAll({
rule: {
any: [
{ pattern: `import $$ANY from 'tls'` },
{ pattern: `import $$ANY from 'node:tls'` },
{ pattern: `const $$ANY = require('tls')` },
{ pattern: `const $$ANY = require('node:tls')` },
],
},
});

for (const stmt of tlsStmts) {
const code = stmt.text();
if (/\bimport\s+\*\s+as\s+\w+\s+from\s+['"](tls|node:tls)['"]/.test(code)) continue;
{
const m = code.match(
/import\s+(?:(?<def>[\w$]+)\s*,\s*)?\{\s*(?<names>[^}]*)\s*\}\s*from\s*['"](tls|node:tls)['"]\s*;?/,
);
if (m?.groups) {
const def = m.groups.def || '';
const namesRaw = m.groups.names || '';
const names = namesRaw
.split(',')
.map(s => s.trim())
.filter(Boolean)
.map(s => s.replace(/\s+as\s+\w+$/, ''));

if (names.includes('createSecurePair')) {
const kept = Array.from(
new Set(names.filter(n => n !== 'createSecurePair').concat('TLSSocket')),
);
const left = def ? `${def}, ` : '';
const rebuilt = `import ${left}{ ${kept.join(', ')} } from 'node:tls';`;
edits.push(stmt.replace(rebuilt));
continue;
}
}
}
{
const m = code.match(
/const\s*\{\s*(?<names>[^}]*)\s*\}\s*=\s*require\(\s*['"](tls|node:tls)['"]\s*\)\s*;?/,
);
if (m?.groups) {
const namesRaw = m.groups.names || '';
const names = namesRaw
.split(',')
.map(s => s.trim())
.filter(Boolean);

if (names.includes('createSecurePair')) {
const kept = Array.from(
new Set(names.filter(n => n !== 'createSecurePair').concat('TLSSocket')),
);
const src = /node:tls/.test(code) ? 'node:tls' : 'tls';
const rebuilt = `const { ${kept.join(', ')} } = require('${src}');`;
edits.push(stmt.replace(rebuilt));
}
}
}
}

return edits;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const { TLSSocket } = require('node:tls');
const socket = new TLSSocket(underlyingSocket, { secureContext: credentials });
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const { TLSSocket } = require('node:tls');
const socket = new TLSSocket(underlyingSocket, { secureContext: credentials, isServer: true, requestCert: true, rejectUnauthorized: false });
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const tls = require('node:tls');
const socket = new tls.TLSSocket(underlyingSocket, { secureContext: credentials });
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { TLSSocket } from 'node:tls';
const socket = new TLSSocket(underlyingSocket, { secureContext: credentials });
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import * as tls from 'node:tls';
const socket = new tls.TLSSocket(underlyingSocket, { secureContext: credentials });
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const { createServer, TLSSocket } = require('node:tls');
const socket = new TLSSocket(underlyingSocket, { secureContext: credentials });
const server = createServer(options);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const { createSecurePair } = require('node:tls');
const pair = createSecurePair(credentials);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const { createSecurePair } = require('node:tls');
const pair = createSecurePair(credentials, true, true, false);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
const tls = require('node:tls');
const pair = tls.createSecurePair(credentials);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import { createSecurePair } from 'node:tls';
const pair = createSecurePair(credentials);
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import * as tls from 'node:tls';
const pair = tls.createSecurePair(credentials);
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
const { createSecurePair, createServer } = require('node:tls');
const pair = createSecurePair(credentials);
const server = createServer(options);
Loading
Loading