百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

用 Node 写一个批量删除 node_modules 的工具

ztj100 2024-11-07 13:39 11 浏览 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

相关推荐

11《Nginx 入门教程》Nginx反向代理(下)

本小节,我们继续学习Nginx在七层反向代理中的其它几种比较常见的情况,比如web服务中的WebSocket协议的反向代理和uwsgi协议的反向代理。1.WebSocket的反向代...

nginx 代理设置一 之常见的设置(nginx代理wsdl)

1、nginx代理配置proxy_passserver{listener8099;location=/test{proxy_passhttp://192.168.18.1...

性能测试之tomcat+nginx负载均衡(nginx tomcat keepalive)

nginxtomcat配置准备工作:两个tomcat执行命令cp-rapache-tomcat-8.5.56apache-tomcat-8.5.56_2修改被复制的tomcat2下con...

配置Nginx TCP转发(nginxtcp端口转发)

Nginx一般用在HTTP的转发,TCP的转发大都会使用HAProxy。工作中遇到一个需求,用到了Nginx服务作为TCP转发。场景是这样,数据采集设备通过公网将数据推送到后端应用服务,服务部署在业主...

如何在 PhpStudy 中配置 Nginx 反向代理功能

如何在PhpStudy中配置Nginx反向代理功能在开发和部署Web应用时,反向代理是一个非常实用的功能。它可以将请求转发到不同的后端服务,例如将HTTP请求转发到运行在其他端口上的...

实战录 | 今天聊聊Nginx反向代理使用

《实战录》导语本期分享人为云端卫士SOC工程师田全磊,将带来Nginx反向代理使用。本文涉及到nignx的安装、nginx的配置说明、nginx的负载均衡、nginx的反向代理和nginx的ssl方反...

Nginx如何配置正向代理:一步步教你轻松上手

Nginx作为一个高性能的HTTP和反向代理服务器,广泛应用于各类网站和服务中。然而,很多人可能不知道,Nginx同样可以配置为正向代理。今天我们就来详细讲解一下如何配置Nginx作为正向代理,让你的...

Nginx最全教程(万字图文总结)(nginx1)

大家好,我是mikechen。...

Nginx配置前后端服务(nginx部署前后端项目)

nginx安装完成后,可以通过命令查看配置文件nginx-t配置文件nginx.conf,是总的配置,有的人会把配置全部配置到这个文件中,如果服务很多,这个文件变得非常庞大,我见过一个配置很大的,在...

Nginx正向代理配置(nginx正反向代理配置)

一、nginx正向代理介绍及配置(需要在客户端配置代理服务器进行指定网站访问)#模块ngx_http_proxy_module:http://nginx.org/en/docs/http/ngx_...

nginx配置websocket代理(nginx配置web服务器)

什么是websocketwebsocket是浏览器实现全双工通信的一种方式,让客户端与服务端互相发送消息成为可能,常用于需要实时消息的场景。nginx对websocket的支持如果没有对nginx进行...

Nginx服务器深度指南:安装、配置、优化指令超详解

在当今数字化时代,Web服务器是支撑互联网应用的关键基础设施。Nginx作为一款高性能的开源Web服务器,凭借卓越的性能、丰富的功能和出色的稳定性,在Web服务器领域占据了重要地位。无论是大型互联网公...

Java开发者也能玩转的Nginx反向代理配置实战

Java开发者也能玩转的Nginx反向代理配置实战作为一名Java开发者,你可能觉得Nginx和自己没什么关系,毕竟你主要负责后端代码的编写。但实际上,Nginx常常作为反向代理服务器来优化Java应...

使用Nginx配置TCP负载均衡(nginx tcp keepalive)

假设Kubernetes集群已经配置好,我们将基于CentOS为Nginx创建一个虚拟机。...

Nginx反向代理:让Java后端飞起来的秘密武器

Nginx反向代理:让Java后端飞起来的秘密武器当涉及到Java后端性能优化时,Nginx这个低调却强大的工具常常被忽视。作为一款高性能的HTTP和反向代理服务器,Nginx能够显著提升Java应用...

取消回复欢迎 发表评论: