Skip to content

Commit f3dde78

Browse files
committed
rename to createCredentials-to-createSecureContext
1 parent b3f8bef commit f3dde78

File tree

17 files changed

+273
-399
lines changed

17 files changed

+273
-399
lines changed
File renamed without changes.

recipes/crypto-create-credentials/codemod.yaml renamed to recipes/createCredentials-to-createSecureContext/codemod.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
schema_version: "1.0"
2-
name: "@nodejs/crypto-create-credentials"
2+
name: "@nodejs/createCredentials-to-createSecureContext"
33
version: 1.0.0
44
description: Handle DEP0010 via transforming `crypto.createCredentials` to `tls.createSecureContext`
55
author: 0hmx

recipes/crypto-create-credentials/package.json renamed to recipes/createCredentials-to-createSecureContext/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"name": "@nodejs/crypto-create-credentials",
2+
"name": "@nodejs/createCredentials-to-createSecureContext",
33
"version": "1.0.0",
44
"description": "Handle DEP0010 via transforming `crypto.createCredentials` to `tls.createSecureContext` with the appropriate options.",
55
"type": "module",
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
1+
import * as os from 'node:os';
2+
import type { SgRoot, Edit, SgNode } from "@codemod.com/jssg-types/main";
3+
4+
type TlsImportState = {
5+
added: boolean;
6+
identifier: string;
7+
};
8+
9+
type HandlerResult = {
10+
edits: Edit[];
11+
wasTransformed: boolean;
12+
newState: TlsImportState;
13+
};
14+
15+
type EnsureTlsResult = {
16+
edits: Edit[];
17+
identifier: string; // The identifier that was used or found
18+
newState: TlsImportState;
19+
};
20+
21+
type ImportType =
22+
| 'DESTRUCTURED_REQUIRE'
23+
| 'NAMESPACE_REQUIRE'
24+
| 'DESTRUCTURED_IMPORT'
25+
| 'NAMESPACE_IMPORT';
26+
27+
const newImportModule = 'node:tls';
28+
const newImportFunction = 'createSecureContext';
29+
30+
function getImportHelpers(importType: ImportType) {
31+
const isEsm =
32+
importType === 'DESTRUCTURED_IMPORT' || importType === 'NAMESPACE_IMPORT';
33+
34+
return {
35+
isEsm,
36+
createNamespaceImport: (id: string, mod: string) =>
37+
isEsm
38+
? `import * as ${id} from '${mod}'`
39+
: `const ${id} = require('${mod}')`,
40+
createDestructuredImport: (specifier: string, mod: string) =>
41+
isEsm
42+
? `import { ${specifier} } from '${mod}'`
43+
: `const { ${specifier} } = require('${mod}')`,
44+
getStatementNode: (match: SgNode): SgNode | null =>
45+
isEsm ? match : match.parent(),
46+
};
47+
}
48+
49+
50+
function ensureTlsImport(
51+
rootNode: SgNode,
52+
importType: ImportType,
53+
currentState: TlsImportState,
54+
): EnsureTlsResult {
55+
if (currentState.added) {
56+
return { edits: [], identifier: currentState.identifier, newState: currentState };
57+
}
58+
59+
const helpers = getImportHelpers(importType);
60+
const findRule = {
61+
rule: {
62+
kind: helpers.isEsm ? 'import_statement' : 'variable_declarator',
63+
has: {
64+
field: helpers.isEsm ? 'source' : 'value',
65+
has: { regex: `^['"](node:)?tls['"]$` },
66+
},
67+
},
68+
};
69+
const existingTlsImport = rootNode.find(findRule);
70+
71+
if (existingTlsImport) {
72+
const nameNode = existingTlsImport.field('name');
73+
if (nameNode?.is('identifier')) {
74+
const foundIdentifier = nameNode.text();
75+
return {
76+
edits: [],
77+
identifier: foundIdentifier,
78+
newState: { added: true, identifier: foundIdentifier },
79+
};
80+
}
81+
}
82+
83+
const firstNode = rootNode.children()[0];
84+
if (!firstNode) {
85+
return { edits: [], identifier: currentState.identifier, newState: currentState };
86+
}
87+
88+
const tlsIdentifier = currentState.identifier;
89+
const newImportText = `${helpers.createNamespaceImport(tlsIdentifier, 'node:tls')};${os.EOL}`;
90+
const edit: Edit = {
91+
startPos: firstNode.range().start.index,
92+
endPos: firstNode.range().start.index,
93+
insertedText: newImportText,
94+
};
95+
96+
return {
97+
edits: [edit],
98+
identifier: tlsIdentifier,
99+
newState: { added: true, identifier: tlsIdentifier },
100+
};
101+
}
102+
103+
function handleDestructuredImport(
104+
statement: SgNode,
105+
rootNode: SgNode,
106+
currentState: TlsImportState,
107+
importType: ImportType,
108+
): HandlerResult {
109+
const localEdits: Edit[] = [];
110+
const helpers = getImportHelpers(importType);
111+
112+
const specifiersNode = helpers.isEsm
113+
? statement.find({ rule: { kind: 'named_imports' } }) ?? statement.field('imports')
114+
: statement.find({ rule: { kind: 'variable_declarator' } })?.field('name');
115+
116+
if (!specifiersNode) {
117+
return { edits: [], wasTransformed: false, newState: currentState };
118+
}
119+
120+
const destructuredProps = specifiersNode.findAll({ rule: { any: [{ kind: 'shorthand_property_identifier_pattern' }, { kind: 'import_specifier' }]}});
121+
const createCredentialsNode = destructuredProps.find((id) => id.text() === 'createCredentials');
122+
123+
if (!createCredentialsNode) {
124+
return { edits: [], wasTransformed: false, newState: currentState };
125+
}
126+
127+
const usages = rootNode.findAll({ rule: { kind: 'call_expression', has: { field: 'function', kind: 'identifier', regex: '^createCredentials$' }}});
128+
for (const usage of usages) {
129+
usage.field('function')?.replace('createSecureContext');
130+
}
131+
132+
const newImportStatement = `${helpers.createDestructuredImport(newImportFunction, newImportModule)};`;
133+
let finalState = currentState;
134+
135+
if (destructuredProps.length === 1) {
136+
localEdits.push(statement.replace(newImportStatement));
137+
finalState = { ...currentState, added: true };
138+
} else {
139+
const otherProps = destructuredProps
140+
.map(d => d.text())
141+
.filter(text => text !== 'createCredentials');
142+
143+
localEdits.push(specifiersNode.replace(`{ ${otherProps.join(', ')} }`));
144+
145+
if (!currentState.added) {
146+
const newEdit = {
147+
startPos: statement.range().end.index,
148+
endPos: statement.range().end.index,
149+
insertedText: `${os.EOL}${newImportStatement}`,
150+
};
151+
localEdits.push(newEdit);
152+
finalState = { ...currentState, added: true };
153+
}
154+
}
155+
156+
return { edits: localEdits, wasTransformed: true, newState: finalState };
157+
}
158+
159+
function handleNamespaceImport(
160+
importOrDeclarator: SgNode,
161+
rootNode: SgNode,
162+
importType: ImportType,
163+
currentState: TlsImportState,
164+
): HandlerResult {
165+
const localEdits: Edit[] = [];
166+
const helpers = getImportHelpers(importType);
167+
168+
const nameNode = helpers.isEsm
169+
? importOrDeclarator.find({ rule: { kind: 'namespace_import' } })?.find({ rule: { kind: 'identifier' } })
170+
: importOrDeclarator.field('name');
171+
172+
if (!nameNode) {
173+
return { edits: [], wasTransformed: false, newState: currentState };
174+
}
175+
const namespaceName = nameNode.text();
176+
177+
const memberAccessUsages = rootNode.findAll({ rule: { kind: 'member_expression', all: [ { has: { field: 'object', regex: `^${namespaceName}$` } }, { has: { field: 'property', regex: '^createCredentials$' } }]}});
178+
if (memberAccessUsages.length === 0) {
179+
return { edits: [], wasTransformed: false, newState: currentState };
180+
}
181+
182+
const allUsages = rootNode.findAll({ rule: { kind: 'member_expression', has: { field: 'object', regex: `^${namespaceName}$` }}});
183+
let finalState = currentState;
184+
185+
if (allUsages.length === memberAccessUsages.length) {
186+
const newTlsIdentifier = currentState.identifier;
187+
const newImportStatement = `${helpers.createNamespaceImport(newTlsIdentifier, newImportModule)};`;
188+
const nodeToReplace = helpers.getStatementNode(importOrDeclarator);
189+
190+
if (nodeToReplace) {
191+
localEdits.push(nodeToReplace.replace(newImportStatement));
192+
}
193+
finalState = { ...currentState, added: true };
194+
for (const usage of memberAccessUsages) {
195+
localEdits.push(usage.replace(`${newTlsIdentifier}.createSecureContext`));
196+
}
197+
} else {
198+
const ensureResult = ensureTlsImport(rootNode, importType, currentState);
199+
localEdits.push(...ensureResult.edits);
200+
finalState = ensureResult.newState;
201+
for (const usage of memberAccessUsages) {
202+
localEdits.push(usage.replace(`${ensureResult.identifier}.createSecureContext`));
203+
}
204+
}
205+
206+
return { edits: localEdits, wasTransformed: true, newState: finalState };
207+
}
208+
209+
export default function transform(root: SgRoot): string | null {
210+
const rootNode = root.root();
211+
const allEdits: Edit[] = [];
212+
let wasTransformed = false;
213+
let currentTlsImportState: TlsImportState = {
214+
added: false,
215+
identifier: 'tls',
216+
};
217+
218+
const cryptoImportsRule = {
219+
rule: {
220+
any: [
221+
{ kind: 'variable_declarator', has: { field: 'value', kind: 'call_expression', has: { field: 'arguments', has: { regex: "^['\"](node:)?crypto['\"]$" }}}},
222+
{ kind: 'import_statement', has: { field: 'source', regex: "^['\"](node:)?crypto['\"]$" }},
223+
],
224+
},
225+
};
226+
const cryptoImports = rootNode.findAll(cryptoImportsRule);
227+
228+
for (const importMatch of cryptoImports) {
229+
const nameNode = importMatch.field('imports') ?? importMatch.field('name');
230+
let importType: ImportType | undefined;
231+
232+
if (importMatch.kind() === 'import_statement') {
233+
importType = importMatch.find({ rule: { kind: 'namespace_import' } })
234+
? 'NAMESPACE_IMPORT' : 'DESTRUCTURED_IMPORT';
235+
} else if (nameNode?.is('object_pattern')) {
236+
importType = 'DESTRUCTURED_REQUIRE';
237+
} else if (nameNode?.is('identifier')) {
238+
importType = 'NAMESPACE_REQUIRE';
239+
}
240+
241+
if (importType === undefined) continue;
242+
243+
let result: HandlerResult | undefined;
244+
const helpers = getImportHelpers(importType);
245+
246+
switch (importType) {
247+
case 'DESTRUCTURED_REQUIRE':
248+
case 'DESTRUCTURED_IMPORT': {
249+
const statement = helpers.getStatementNode(importMatch);
250+
if (statement) {
251+
result = handleDestructuredImport(statement, rootNode, currentTlsImportState, importType);
252+
}
253+
break;
254+
}
255+
case 'NAMESPACE_REQUIRE':
256+
case 'NAMESPACE_IMPORT':
257+
result = handleNamespaceImport(importMatch, rootNode, importType, currentTlsImportState);
258+
break;
259+
}
260+
261+
if (result) {
262+
allEdits.push(...result.edits);
263+
currentTlsImportState = result.newState;
264+
if (result.wasTransformed) {
265+
wasTransformed = true;
266+
}
267+
}
268+
}
269+
270+
return wasTransformed ? rootNode.commitEdits(allEdits) : null;
271+
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.

0 commit comments

Comments
 (0)