用 Node 写一个批量删除 node_modules 的工具
ztj100 2024-11-07 13:39 13 浏览 0 评论
今天我用 npm 安装包的时候,报错说磁盘空间不够用了:
我想我也没有下什么很大的东西啊,大概是我项目比较多,node_modules 比较多。
而 node_modules 一般是比较大的。
比如我一个 nest 项目的 node_modules 就有 275 M 呢:
当然,如果你用 pnpm 安装包,可能没这个问题
因为 pnpm 是把依赖安装到全局 store,然后用的硬链接的方式从全局 store 连接到当前项目的 node_modules/.pnpm 下
node_modules 下的依赖再从这个 .pnpm 目录软链接过去。
所以同样的依赖只会全局安装一次,并且存在全局 store,根本不用担心磁盘空间占用问题。
文档里也提到了这个优势:
但问题是我很多项目用的是 yarn 和 npm,依赖保存在每个 node_modules 下,所以占用空间会很大。
所以我就想着写个自动化工具找到这些 node_modules 并删除它。
先来分析下思路:
要找到 node_modules 的目录,只要递归遍历目录和它的子目录,判断是否是 node_modules,如果是的话,就记录下来就好了。之后批量删除。
思路很清晰,但有一个要注意的点,就是软链接文件。
假设我有一个文件是 src/index.ts
想读取它的内容就用 fs.readFileSync
那我又基于它创建了一个 test/index.ts 的软链接文件呢?怎么读取?
ln -s src/index.ts ./test/index.ts
这时候如果你还是用 fs.readFileSync 就会报错了:
说是文件找不到。
软链接文件的读取要用 fs.readlinkSync
可以看到,读取出的是链接到的原文件的地址。
这样只要再 fs.readFileSync 就能读到原始内容了。
那如何判断一个文件是否是软链接文件呢?
可以通过 fs.lstatSync 拿到文件的信息,然后调用 isSymbolicLink 判断是否是符号链接,也就是软链接。
注意,这里是 lstat 不是 stat,如果用 stat 方法,依然会有文件不存在的问题。
思路理清了,我们来写下代码。
创建个项目:
mkdir node_modules_killer
cd node_modules_killer
npm init -y
npm install typescript --save-dev
创建项目目录、package.json 、安装 typescript
然后创建这样一个 tsconfig.json
{
"compilerOptions": {
"lib": ["ES2015"],
"types": ["node"],
"target": "es2016",
"outDir": "./dist",
"module": "commonjs",
"sourceMap": true,
"esModuleInterop": true,
"skipLibCheck": true
},
"include": ["src/**/*.ts"]
}
lib 是引入 ts 内置的类型,这里引入 es2015 的 api 的类型。
types 是引入第三方类型,这里引入 node api 的类型。
并且安装 @types/node
npm install @types/node --save-dev
指定 target 和 module 也就是编译后的语言的版本和模块类型。
指定 outDir,也就是输出目录。
指定 include 包含编译的文件。
生成 sourcemap,待会我们调试用。
我们加一个 src/index.ts 试一下:
现在直接这样引入 node 模块会报错,要这样才可以:
如果你还是想用上面的方式,可以加一个 ts 编译选项:
这样就好了:
因为 node 的 内置模块是 commonjs 的,默认需要 import * as xxx from,这个选项会生成一些额外的代码来让模块可以 import 引入:
然后我们先加一段代码用来测试:
import os from 'os';
console.log(os.homedir());
在 package.json 里添加 dev 的 scripts
执行 npm run dev
可以看到 dist 下有了编译后的文件和 sourcemap。
node 跑一下:
没啥问题。
然后再来试下调试:
在 debug 面板点击 create a launch.json file
创建一个调试配置文件:
新建调试 node 的配置:
创建这样一个调试配置:
在代码里打个断点:
然后点击调试启动:
代码就会在断点处断住:
可以单步调试。
左边可以看到作用域,调用栈。
编译和调试都搞定了,我们来写下具体的逻辑:
import fs from 'fs/promises';
import os from 'os';
import path from 'path';
const homedir = os.homedir();
const foundDirs = [];
async function searchDir(dirPath: string, searchName: string) {
}
async function main() {
await searchDir(homedir, 'node_modules');
await fs.writeFile('./found', foundDirs.join(os.EOL));
console.log('done');
}
main();
框架大概是这样的:
从 home 目录开始递归查找 node_modules,然后把查到的路径放到 foundDirs 里,最后按照每行一个路径写入文件。
这里用的 fs/promises 是 promise 版本的 fs api。
os.EOL 是 end of line,也就是换行符,不同操作系统的换行符不同,所以从 os 模块拿。
而 searchDir 的逻辑如下:
async function searchDir(dirPath: string, searchName: string) {
const children = await fs.readdir(dirPath);
for(let i = 0; i< children.length; i++) {
const child = children[i];
const childPath = path.join(dirPath, child);
const res = await fs.lstat(childPath);
if(await res.isSymbolicLink()) {
break;
}
if(res.isDirectory() && !child.startsWith('.')) {
if(child === 'node_modules') {
console.log(childPath)
foundDirs.push(childPath);
} else {
await searchDir(childPath, searchName);
}
}
}
}
用 readdir 读取目录的内容,依次判断每个文件是否软链接文件。
如果是软链接,直接 return,不然读取它会报错。
再判断下是否是目录,如果是 node_modules 目录就把路径放入 foundDirs,否则递归查找。
这里排除掉 . 开头的目录,这些一般是隐藏目录,不需要查找。
跑一下:
确实查找到了一些 node_modules 目录。
但是一些目录提示没有权限。
这种目录直接跳过就好了,没有权限的目录一般都不是项目目录。
也就是这样:
读取目录的时候没有权限直接跳过。
这样,就会打印出所有的 node_modules 目录:
并且会写入这个 found 文件:
好家伙,21648 个 node_modules
这样,第一个阶段的任务就完成了,也就是找到所有 node_modules:
import fs from 'fs/promises';
import os from 'os';
import path from 'path';
const homedir = os.homedir();
const foundDirs = [];
async function searchDir(dirPath: string, searchName: string) {
let children;
try {
children = await fs.readdir(dirPath);
} catch(e) {
return;
}
for(let i = 0; i< children.length; i++) {
const child = children[i];
const childPath = path.join(dirPath, child);
const res = await fs.lstat(childPath);
if(await res.isSymbolicLink()) {
break;
}
if(res.isDirectory() && !child.startsWith('.')) {
if(child === 'node_modules') {
console.log(childPath)
foundDirs.push(childPath);
} else {
await searchDir(childPath, searchName);
}
}
}
}
async function main() {
await searchDir(homedir, 'node_modules');
await fs.writeFile('./found', foundDirs.join(os.EOL));
console.log('done');
}
main();
然后,我们如何知道一个 node_modules 的大小呢?
其实和递归查找是一样的,只不过现在是递归累加文件大小了:
import fs from 'fs/promises';
import path from 'path';
async function dirSize(dirPath) {
let totalSize = 0;
let children;
try {
children = await fs.readdir(dirPath);
} catch(e) {
return;
}
for(let i = 0; i< children.length; i++) {
const child = children[i];
const childPath = path.join(dirPath, child);
const res = await fs.lstat(childPath);
if(await res.isSymbolicLink()) {
break;
}
if(res.isDirectory()) {
totalSize += await dirSize(childPath);
} else {
totalSize += res.size;
}
}
return totalSize;
}
async function main() {
const size = await dirSize('./node_modules');
console.log(size);
}
main();
其余的文件权限、软链接的判断逻辑一样,只不过现在会拿到 size 累加起来。
测试下:
基本是一样的。
这样,就可以在找到 node_modules 目录之后,用这个 dirSize 来计算下大小。
然后我们实现最终的目的,删除。
这个就比较简单了,我们可以从 found 目录读取文件路径,然后依次删除。
import fs from 'fs/promises';
import os from 'os';
async function fileExists(filePath) {
try {
await fs.access(filePath);
return true;
} catch(e){
return false;
}
}
async function removeFileOrDir(dirs: string[]) {
for (let i = 0; i < dirs.length; i++) {
if(await fileExists(dirs[i])) {
await fs.rm(dirs[i], { recursive: true });
console.log(dirs[i], 'removed')
}
}
}
async function main() {
const str = await fs.readFile('./found', {encoding: 'utf-8'});
const dirs = str.split(os.EOL);
await removeFileOrDir(dirs);
}
main();
这里要先判断目录是否存在,因为如果已经不存在了,rm 会报错:
判断文件是否存在,用 access 的 api,如果访问报错就是不存在,否则就是存在。
在 found 里放两个目录试试:
执行 cd 不报错,说明目录存在:
然后执行下 node 脚本删除它们:
之后再 cd 就报错了,说明目录已经被删除了:
这样,我们就完成了扫描出所有 node_modules、计算大小、批量删除的功能。
总结
用 npm 或者 yarn 安装依赖,依赖直接保存在 node_modules 下,会占用很大的磁盘空间。
如果是 pnpm,因为用的是从全局 store 硬链接过来的方式,全局只会保存一份。
今天我磁盘空间满了,所以想批量清理下 node_modules,于是用 node + ts 写了一个小工具。
首先,递归遍历目录,查找出所有的 node_modules 的路径,写入文件中
然后遍历目录,累加计算 fileSize。
之后读取文件,根据其中的路径批量删除 node_modules。
用到了这些 node api:
- os.homedir 拿到 home 目录
- os.EOL 拿到当前系统的换行符
- path.join 拼接文件路径
- fs.readdir 读取目录
- fs.lstat 读取文件或者目录的信息,同时支持 link 文件,建议只用 lstat 不用 stat
- fs.lstat(xxx).isSymbolicLink 判断软链接文件
- fs.lstat(xxx).isDirectory 是否是目录
- fs.writeFile 写文件
- fs.readFile 读文件
- fs.access 判断文件或者目录是否存在,如果不存在,会抛出异常
- fs.rm 删除文件或目录
要注意的是链接文件直接 readdir 会提示文件或者目录不存在,要用 fs.lstat(xxx).isSymbolicLink 的方式判断下,如果是软链接就跳过。
有了这个工具,就可以批量查找、删除 node_module 以及计算它们的大小了,是释放磁盘空间的利器。
作者:zxg_神说要有光
链接:https://juejin.cn/post/7263744906681073724
相关推荐
- 如何将数据仓库迁移到阿里云 AnalyticDB for PostgreSQL
-
阿里云AnalyticDBforPostgreSQL(以下简称ADBPG,即原HybridDBforPostgreSQL)为基于PostgreSQL内核的MPP架构的实时数据仓库服务,可以...
- Python数据分析:探索性分析
-
写在前面如果你忘记了前面的文章,可以看看加深印象:Python数据处理...
- C++基础语法梳理:算法丨十大排序算法(二)
-
本期是C++基础语法分享的第十六节,今天给大家来梳理一下十大排序算法后五个!归并排序...
- C 语言的标准库有哪些
-
C语言的标准库并不是一个单一的实体,而是由一系列头文件(headerfiles)组成的集合。每个头文件声明了一组相关的函数、宏、类型和常量。程序员通过在代码中使用#include<...
- [深度学习] ncnn安装和调用基础教程
-
1介绍ncnn是腾讯开发的一个为手机端极致优化的高性能神经网络前向计算框架,无第三方依赖,跨平台,但是通常都需要protobuf和opencv。ncnn目前已在腾讯多款应用中使用,如QQ,Qzon...
- 用rust实现经典的冒泡排序和快速排序
-
1.假设待排序数组如下letmutarr=[5,3,8,4,2,7,1];...
- ncnn+PPYOLOv2首次结合!全网最详细代码解读来了
-
编辑:好困LRS【新智元导读】今天给大家安利一个宝藏仓库miemiedetection,该仓库集合了PPYOLO、PPYOLOv2、PPYOLOE三个算法pytorch实现三合一,其中的PPYOL...
- C++特性使用建议
-
1.引用参数使用引用替代指针且所有不变的引用参数必须加上const。在C语言中,如果函数需要修改变量的值,参数必须为指针,如...
- Qt4/5升级到Qt6吐血经验总结V202308
-
00:直观总结增加了很多轮子,同时原有模块拆分的也更细致,估计为了方便拓展个管理。把一些过度封装的东西移除了(比如同样的功能有多个函数),保证了只有一个函数执行该功能。把一些Qt5中兼容Qt4的方法废...
- 到底什么是C++11新特性,请看下文
-
C++11是一个比较大的更新,引入了很多新特性,以下是对这些特性的详细解释,帮助您快速理解C++11的内容1.自动类型推导(auto和decltype)...
- 掌握C++11这些特性,代码简洁性、安全性和性能轻松跃升!
-
C++11(又称C++0x)是C++编程语言的一次重大更新,引入了许多新特性,显著提升了代码简洁性、安全性和性能。以下是主要特性的分类介绍及示例:一、核心语言特性1.自动类型推导(auto)编译器自...
- 经典算法——凸包算法
-
凸包算法(ConvexHull)一、概念与问题描述凸包是指在平面上给定一组点,找到包含这些点的最小面积或最小周长的凸多边形。这个多边形没有任何内凹部分,即从一个多边形内的任意一点画一条线到多边形边界...
- 一起学习c++11——c++11中的新增的容器
-
c++11新增的容器1:array当时的初衷是希望提供一个在栈上分配的,定长数组,而且可以使用stl中的模板算法。array的用法如下:#include<string>#includ...
- C++ 编程中的一些最佳实践
-
1.遵循代码简洁原则尽量避免冗余代码,通过模块化设计、清晰的命名和良好的结构,让代码更易于阅读和维护...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- node卸载 (33)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- exceptionininitializererror (33)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)