Skip to content

Commit c0035c8

Browse files
feat(rmdir): introduce (#110)
Co-authored-by: Aviv Keller <[email protected]>
1 parent 707c460 commit c0035c8

File tree

12 files changed

+394
-0
lines changed

12 files changed

+394
-0
lines changed

package-lock.json

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

recipes/rmdir/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# `fs.rmdir` DEP0147
2+
3+
This recipe provides a guide for migrating from the deprecated `fs.rmdir` and its synchronous and promise-based counterparts to the new `fs.rm` method in Node.js.
4+
5+
See [DEP0147](https://nodejs.org/api/deprecations.html#DEP0147).
6+
7+
## Examples
8+
9+
**Before:**
10+
11+
```js
12+
// Using fs.rmdir with the recursive option
13+
fs.rmdir(path, { recursive: true }, callback);
14+
15+
// Using fs.rmdirSync with the recursive option
16+
fs.rmdirSync(path, { recursive: true });
17+
18+
// Using fs.promises.rmdir with the recursive option
19+
fs.promises.rmdir(path, { recursive: true });
20+
```
21+
22+
**After:**
23+
24+
```js
25+
// Using fs.rm with recursive and force options
26+
fs.rm(path, { recursive: true, force: true }, callback);
27+
28+
// Using fs.rmSync with recursive and force options
29+
fs.rmSync(path, { recursive: true, force: true });
30+
31+
// Using fs.promises.rm with recursive and force options
32+
fs.promises.rm(path, { recursive: true, force: true });
33+
```

recipes/rmdir/codemod.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
schema_version: "1.0"
2+
name: nodejs/rmdir
3+
version: 0.0.1
4+
description: Handle DEP0147 via transforming `fs.rmdir` to `fs.rm`
5+
author: Augustin Mauroy
6+
license: MIT
7+
workflow: workflow.yaml
8+
category: migration
9+
10+
targets:
11+
languages:
12+
- javascript
13+
- typescript
14+
15+
keywords:
16+
- transformation
17+
- migration
18+
19+
registry:
20+
access: public
21+
visibility: public

recipes/rmdir/package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "@nodejs/rmdir",
3+
"version": "0.0.1",
4+
"description": "Handle DEP0147 via transforming `fs.rmdir` to `fs.rm`` with the appropriate options.",
5+
"type": "module",
6+
"scripts": {
7+
"test": "npx codemod@next jssg test -l typescript ./src/workflow.ts ./"
8+
},
9+
"repository": {
10+
"type": "git",
11+
"url": "git+https://github.com/nodejs/userland-migrations.git",
12+
"directory": "recipes/rmdirs",
13+
"bugs": "https://github.com/nodejs/userland-migrations/issues"
14+
},
15+
"author": "Augustin Mauroy",
16+
"license": "MIT",
17+
"homepage": "https://github.com/nodejs/userland-migrations/blob/main/recipes/rmdirs/README.md",
18+
"devDependencies": {
19+
"@types/node": "^24.0.3",
20+
"@codemod.com/jssg-types": "^1.0.3"
21+
},
22+
"dependencies": {
23+
"@nodejs/codemod-utils": "*"
24+
}
25+
}

recipes/rmdir/src/workflow.ts

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
import { getNodeImportStatements } from "@nodejs/codemod-utils/ast-grep/import-statement";
2+
import { getNodeRequireCalls } from "@nodejs/codemod-utils/ast-grep/require-call";
3+
import type { SgRoot, Edit } from "@ast-grep/napi";
4+
5+
/**
6+
* Transform function that converts deprecated fs.rmdir calls
7+
* with recursive: true option to the new fs.rm API.
8+
*
9+
* Handles:
10+
* 1. fs.rmdir(path, { recursive: true }, callback) -> fs.rm(path, { recursive: true, force: true }, callback)
11+
* 2. fs.rmdir(path, { recursive: true }) -> fs.rm(path, { recursive: true, force: true })
12+
* 3. fs.rmdirSync(path, { recursive: true }) -> fs.rmSync(path, { recursive: true, force: true })
13+
* 4. fs.promises.rmdir(path, { recursive: true }) -> fs.promises.rm(path, { recursive: true, force: true })
14+
*/
15+
export default function transform(root: SgRoot): string | null {
16+
const rootNode = root.root();
17+
let hasChanges = false;
18+
const edits: Edit[] = [];
19+
20+
// Find all rmdir calls that need transformation
21+
const rmdirSyncCalls = rootNode.findAll({
22+
rule: {
23+
any: [
24+
{ pattern: "fs.rmdirSync($PATH, $OPTIONS)" },
25+
{ pattern: "rmdirSync($PATH, $OPTIONS)" }
26+
]
27+
}
28+
});
29+
30+
const rmdirCalls = rootNode.findAll({
31+
rule: {
32+
any: [
33+
{ pattern: "fs.rmdir($PATH, $OPTIONS, $CALLBACK)" },
34+
{ pattern: "fs.rmdir($PATH, $OPTIONS)" },
35+
{ pattern: "rmdir($PATH, $OPTIONS, $CALLBACK)" },
36+
{ pattern: "rmdir($PATH, $OPTIONS)" }
37+
]
38+
}
39+
});
40+
41+
const promisesRmdirCalls = rootNode.findAll({
42+
rule: {
43+
any: [
44+
{ pattern: "fs.promises.rmdir($PATH, $OPTIONS)" },
45+
{ pattern: "promises.rmdir($PATH, $OPTIONS)" }
46+
]
47+
}
48+
});
49+
50+
let needsRmImport = false;
51+
let needsRmSyncImport = false;
52+
53+
// Transform rmdirSync calls
54+
for (const call of rmdirSyncCalls) {
55+
const optionsMatch = call.getMatch("OPTIONS");
56+
if (!optionsMatch) continue;
57+
const optionsText = optionsMatch.text();
58+
if (!optionsText.includes("recursive") || !optionsText.includes("true")) {
59+
continue;
60+
}
61+
62+
const path = call.getMatch("PATH")?.text();
63+
const callText = call.text();
64+
65+
if (callText.includes("fs.rmdirSync(")) {
66+
const newCallText = `fs.rmSync(${path}, { recursive: true, force: true })`;
67+
edits.push(call.replace(newCallText));
68+
} else {
69+
// destructured call like rmdirSync(...)
70+
const newCallText = `rmSync(${path}, { recursive: true, force: true })`;
71+
edits.push(call.replace(newCallText));
72+
needsRmSyncImport = true;
73+
}
74+
hasChanges = true;
75+
}
76+
77+
// Transform rmdir calls
78+
for (const call of rmdirCalls) {
79+
const optionsMatch = call.getMatch("OPTIONS");
80+
if (!optionsMatch) continue;
81+
const optionsText = optionsMatch.text();
82+
if (!optionsText.includes("recursive") || !optionsText.includes("true")) {
83+
continue;
84+
}
85+
86+
const path = call.getMatch("PATH")?.text();
87+
const callText = call.text();
88+
89+
if (callText.includes("fs.rmdir(")) {
90+
// Handle fs.rmdir → fs.rm
91+
if (call.getMatch("CALLBACK")) {
92+
// Has callback
93+
const callback = call.getMatch("CALLBACK")?.text();
94+
const newCallText = `fs.rm(${path}, { recursive: true, force: true }, ${callback})`;
95+
edits.push(call.replace(newCallText));
96+
} else {
97+
// No callback
98+
const newCallText = `fs.rm(${path}, { recursive: true, force: true })`;
99+
edits.push(call.replace(newCallText));
100+
}
101+
} else {
102+
// destructured call like rmdir(...)
103+
if (call.getMatch("CALLBACK")) {
104+
// Has callback
105+
const callback = call.getMatch("CALLBACK")?.text();
106+
const newCallText = `rm(${path}, { recursive: true, force: true }, ${callback})`;
107+
edits.push(call.replace(newCallText));
108+
} else {
109+
// No callback
110+
const newCallText = `rm(${path}, { recursive: true, force: true })`;
111+
edits.push(call.replace(newCallText));
112+
}
113+
needsRmImport = true;
114+
}
115+
hasChanges = true;
116+
}
117+
118+
// Transform fs.promises.rmdir calls
119+
for (const call of promisesRmdirCalls) {
120+
const optionsMatch = call.getMatch("OPTIONS");
121+
if (!optionsMatch) continue;
122+
const optionsText = optionsMatch.text();
123+
if (!optionsText.includes("recursive") || !optionsText.includes("true")) {
124+
continue;
125+
}
126+
127+
const path = call.getMatch("PATH")?.text();
128+
const callText = call.text();
129+
130+
if (callText.includes("fs.promises.rmdir(")) {
131+
const newCallText = `fs.promises.rm(${path}, { recursive: true, force: true })`;
132+
edits.push(call.replace(newCallText));
133+
} else {
134+
// destructured call like promises.rmdir(...)
135+
const newCallText = `promises.rm(${path}, { recursive: true, force: true })`;
136+
edits.push(call.replace(newCallText));
137+
needsRmImport = true;
138+
}
139+
hasChanges = true;
140+
}
141+
142+
// Update imports/requires only if we have destructured calls that need new imports
143+
if (needsRmImport || needsRmSyncImport) {
144+
const importStatements = getNodeImportStatements(root, 'fs');
145+
146+
// Update import statements
147+
for (const importNode of importStatements) {
148+
// Check if it's a named import (destructured)
149+
const namedImports = importNode.find({ rule: { kind: 'named_imports' } });
150+
if (!namedImports) continue;
151+
152+
let importText = importNode.text();
153+
let updated = false;
154+
155+
if (needsRmImport
156+
&& importText.includes("rmdir")
157+
&& !importText.includes(" rm,")
158+
&& !importText.includes(" rm ")
159+
&& !importText.includes("{rm,")
160+
&& !importText.includes("{rm }")
161+
) {
162+
// Add rm to imports
163+
importText = importText.replace(/{\s*/, "{ rm, ");
164+
updated = true;
165+
}
166+
167+
if (needsRmSyncImport && importText.includes("rmdirSync")) {
168+
// Replace rmdirSync with rmSync
169+
importText = importText.replace(/rmdirSync/g, "rmSync");
170+
updated = true;
171+
}
172+
173+
if (updated) {
174+
edits.push(importNode.replace(importText));
175+
hasChanges = true;
176+
}
177+
}
178+
179+
const requireStatements = getNodeRequireCalls(root, 'fs');
180+
181+
// Update require statements
182+
for (const requireNode of requireStatements) {
183+
let requireText = requireNode.text();
184+
let updated = false;
185+
186+
if (needsRmImport
187+
&& requireText.includes("rmdir")
188+
&& !requireText.includes(" rm,")
189+
&& !requireText.includes(" rm ")
190+
&& !requireText.includes("{rm,")
191+
&& !requireText.includes("{rm }")
192+
) {
193+
// Add rm to requires
194+
requireText = requireText.replace(/{\s*/, "{ rm, ");
195+
updated = true;
196+
}
197+
198+
if (needsRmSyncImport && requireText.includes("rmdirSync")) {
199+
// Replace rmdirSync with rmSync
200+
requireText = requireText.replace(/rmdirSync/g, "rmSync");
201+
updated = true;
202+
}
203+
204+
if (updated) {
205+
edits.push(requireNode.replace(requireText));
206+
hasChanges = true;
207+
}
208+
}
209+
}
210+
211+
if (!hasChanges) return null;
212+
213+
return rootNode.commitEdits(edits);
214+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const fs = require("node:fs");
2+
3+
const pathName = "path/to/directory";
4+
5+
fs.rm(pathName, { recursive: true, force: true }, () => { });
6+
fs.rmSync(pathName, { recursive: true, force: true });
7+
fs.promises.rm(pathName, { recursive: true, force: true });
8+
fs.rmdir(pathName, { recursive: false }); // should not be transformed
9+
fs.rmdir(pathName); // should not be transformed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const { rm, rmdir, rmSync, promises } = require("node:fs");
2+
3+
const pathName = "path/to/directory";
4+
5+
rm(pathName, { recursive: true, force: true }, () => { });
6+
rmSync(pathName, { recursive: true, force: true });
7+
promises.rm(pathName, { recursive: true, force: true });
8+
rmdir(pathName, { recursive: false }); // should not be transformed
9+
rmdir(pathName); // should not be transformed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const fs = require("node:fs");
2+
3+
const pathName = "path/to/directory";
4+
5+
fs.rmdir(pathName, { recursive: true }, () => { });
6+
fs.rmdirSync(pathName, { recursive: true });
7+
fs.promises.rmdir(pathName, { recursive: true });
8+
fs.rmdir(pathName, { recursive: false }); // should not be transformed
9+
fs.rmdir(pathName); // should not be transformed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
const { rmdir, rmdirSync, promises } = require("node:fs");
2+
3+
const pathName = "path/to/directory";
4+
5+
rmdir(pathName, { recursive: true }, () => { });
6+
rmdirSync(pathName, { recursive: true });
7+
promises.rmdir(pathName, { recursive: true });
8+
rmdir(pathName, { recursive: false }); // should not be transformed
9+
rmdir(pathName); // should not be transformed

recipes/rmdir/tsconfig.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"compilerOptions": {
3+
"allowImportingTsExtensions": true,
4+
"allowJs": true,
5+
"alwaysStrict": true,
6+
"baseUrl": "./",
7+
"declaration": true,
8+
"declarationMap": true,
9+
"emitDeclarationOnly": true,
10+
"lib": ["ESNext", "DOM"],
11+
"module": "NodeNext",
12+
"moduleResolution": "NodeNext",
13+
"noImplicitThis": true,
14+
"removeComments": true,
15+
"strict": true,
16+
"stripInternal": true,
17+
"target": "esnext"
18+
},
19+
"include": ["./"],
20+
"exclude": [
21+
"tests/**"
22+
]
23+
}

0 commit comments

Comments
 (0)