包地址:www.npmjs.com/package/@hs... (第一次发包踩坑记录:名字冲突、bin 路径写错、publish 了好几次才成功......名字就此将错就错了 😅)
一、需求背景
当我们把一段脚本封装成全局 npm 包(如 get-dir-tree)时,用户可能处于两种完全不同的使用场景:
- CI / 自动化脚本:参数一次性给足,无人值守。
- 终端随手敲:用户只记得包名,参数记不清,甚至不知道有哪些选项。
如果 CLI 仅支持"一次性传参",体验会过于僵硬;如果强制进入问答,又会让自动化脚本无法运行。最佳实践是"两者同时支持": 有参 → 直接执行;无参 → 友好对话补全。
二、运行示例
场景 A:一次性给足
$ get-dir-tree ./src 2 tree.md .git,node_modules,dist
场景 B:啥都没给,进入对话
$ get-dir-tree ? 请输入目标目录路径(默认为当前目录): ? 请输入应遍历的最大深度......
三、核心代码
javascript
const fs = require('fs');
const path = require('path');
const readline = require('readline');
// 显示帮助信息
function showHelp() {
console.log(`
Usage: get-dir-tree [dirPath] [shouldDeepTraverse] [outputPath] [ignorePatterns]
Options:
dirPath 目标目录路径,默认为当前目录。
shouldDeepTraverse 控制遍历深度,0表示全部遍历,1表示只获取当前层,其他正整数表示对应层数,默认为1。
outputPath 输出文件路径,默认不保存到文件。
ignorePatterns 过滤规则,默认过滤.git,node_modules。
-h, --help 显示帮助信息。
Examples:
get-dir-tree ./ 1
get-dir-tree ./ 2 outputPath
get-dir-tree ./ 0 outputPath
get-dir-tree -h (or --help) for help
`);
}
// 创建readline接口实例
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// 解析命令行参数并开始对话
async function startDialogue() {
// 如果有参数且不是帮助标志,则直接使用这些参数
const args = process.argv.slice(2);
if (args.includes('-h') || args.includes('--help')) {
showHelp();
process.exit(0);
}
let dirPath = args[0];
let shouldDeepTraverse = args[1];
let outputPath = args[2];
// 设置忽略模式
let ignorePatterns = args[3];
// 如果缺少参数,则通过对话方式获取
if (!dirPath) {
dirPath = await askQuestion('请输入目标目录路径(默认为当前目录):') || './';
}
if (shouldDeepTraverse === undefined) {
shouldDeepTraverse = await askQuestion('请输入应遍历的最大深度(0为不限制,但是文件过多或有一些特殊字符的文件可能会出现报错,1为仅当前层,默认为1):') || '1';
shouldDeepTraverse = shouldDeepTraverse.toLowerCase() === '0' ? Infinity : parseInt(shouldDeepTraverse, 10) || 1;
}
if (outputPath === undefined) {
let fileName = await askQuestion('请输入输出文件名称(留空则不保存到文件):')
outputPath = fileName ? fileName + '.md' : null;
}
if (ignorePatterns === undefined) {
let filterList = await askQuestion('请输入忽略规则(多个规则用逗号分隔),默认过滤.git,node_modules:')
ignorePatterns = filterList ? filterList?.split(/[,,]/) : ['.git', 'node_modules'];
}
rl.close(); // 关闭readline接口
四、获取文件目录树完整代码如下
JavaScript
const fs = require('fs');
const path = require('path');
const readline = require('readline');
// 显示帮助信息
function showHelp() {
console.log(`
Usage: get-dir-tree [dirPath] [shouldDeepTraverse] [outputPath] [ignorePatterns]
Options:
dirPath 目标目录路径,默认为当前目录。
shouldDeepTraverse 控制遍历深度,0表示全部遍历,1表示只获取当前层,其他正整数表示对应层数,默认为1。
outputPath 输出文件路径,默认不保存到文件。
ignorePatterns 过滤规则,默认过滤.git,node_modules。
-h, --help 显示帮助信息。
Examples:
get-dir-tree ./ 1
get-dir-tree ./ 2 outputPath
get-dir-tree ./ 0 outputPath
get-dir-tree -h (or --help) for help
`);
}
// 创建readline接口实例
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
// 解析命令行参数并开始对话
async function startDialogue() {
// 如果有参数且不是帮助标志,则直接使用这些参数
const args = process.argv.slice(2);
if (args.includes('-h') || args.includes('--help')) {
showHelp();
process.exit(0);
}
let dirPath = args[0];
let shouldDeepTraverse = args[1];
let outputPath = args[2];
// 设置忽略模式
let ignorePatterns = args[3];
// 如果缺少参数,则通过对话方式获取
if (!dirPath) {
dirPath = await askQuestion('请输入目标目录路径(默认为当前目录):') || './';
}
if (shouldDeepTraverse === undefined) {
shouldDeepTraverse = await askQuestion('请输入应遍历的最大深度(0为不限制,但是文件过多或有一些特殊字符的文件可能会出现报错,1为仅当前层,默认为1):') || '1';
shouldDeepTraverse = shouldDeepTraverse.toLowerCase() === '0' ? Infinity : parseInt(shouldDeepTraverse, 10) || 1;
}
if (outputPath === undefined) {
let fileName = await askQuestion('请输入输出文件名称(留空则不保存到文件):')
outputPath = fileName ? fileName + '.md' : null;
}
if (ignorePatterns === undefined) {
let filterList = await askQuestion('请输入忽略规则(多个规则用逗号分隔),默认过滤.git,node_modules:')
ignorePatterns = filterList ? filterList?.split(/[,,]/) : ['.git', 'node_modules'];
}
rl.close(); // 关闭readline接口
// 打印或保存结果
const result = dirPath + '\n' + treeToMarkdown(dirPath, '', true, ignorePatterns, shouldDeepTraverse);
if (outputPath) {
let text = "```bash\n" + result + "\n```";
fs.writeFileSync(outputPath, text);
console.log(`目录结构已保存至: ${outputPath}`);
} else {
console.log(result);
}
}
// 提问函数
function askQuestion(question) {
return new Promise(resolve => {
// 禁用快速编辑模式
process.stdin.setRawMode(false);
rl.question(question, (answer) => {
// 恢复快速编辑模式
process.stdin.setRawMode(true);
resolve(answer);
});
});
}
// 树状结构转换为Markdown格式函数
function treeToMarkdown(dirPath, prefix = '', isLast = true, ignorePatterns = [], depth = Infinity, currentDepth = 0) {
let markdown = '';
try {
if (currentDepth >= depth) return ''; // 达到最大深度时停止递归
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
entries.sort((a, b) => b.isDirectory() - a.isDirectory() || a.name.localeCompare(b.name));
entries.forEach((entry, index) => {
if (isIgnored(entry.name, ignorePatterns)) return;
const isEntryLast = index === entries.length - 1;
const newPrefix = isEntryLast ? '└── ' : '├── ';
const entryName = entry.isDirectory() ? entry.name + '/' : entry.name;
markdown += `${prefix}${newPrefix}${entryName}\n`;
if (entry.isDirectory()) { // 检查是否继续遍历子目录
const nextPrefix = isEntryLast ? ' ' : '│ ';
markdown += treeToMarkdown(path.join(dirPath, entry.name), prefix + nextPrefix, isEntryLast, ignorePatterns, depth, currentDepth + 1);
} else if (entry.isSymbolicLink()) {
try {
const resolvedPath = fs.readlinkSync(path.join(dirPath, entry.name));
markdown += `${prefix} → ${resolvedPath}\n`;
} catch (err) {
console.error(`无法解析符号链接: ${entry.name}`);
}
}
});
} catch (err) {
console.error(`读取目录时出错: ${dirPath}`, err);
}
return markdown;
}
// 忽略规则函数
function isIgnored(entryName, ignorePatterns) {
return ignorePatterns.some(pattern => entryName.startsWith(pattern));
}
startDialogue();
五、一键安装体验
shell
npm i @hsk766187397/get-tree -g
六、第一次发包的踩坑小结
- 名字冲突:npm 上 get-tree、dir-tree 全被占,只能加前缀。
- package.json 的 bin 字段必须指向最终可执行文件,开发阶段忘改路径,全局安装后报 command not found。
- 发布前务必 npm link 本地试跑。
至此,一个"零配置也能用、传参就能跑"的目录树 CLI 就打包完毕。 欢迎 npm i -g @hsk766187397/get-tree 体验!