[Node.js] Create a note cli
Create a node cli
Init a project
Run: npm run init
Let's say we want to create a cli command call note-dev
, let's add this into package.json
file:
"bin": {
"note-dev": "./index.js"
},
Create the entry file:
"#!/usr/bin/env node";
// index.js
console.log("hello cli");
We need to add our cli note-dev
to local env, run:
npm link
inside project root folder. This way we can test the change in local quickly, we don't need to publish a package and reinstall again and again.
We can test it by runing which note-dev
, will get result /<PATH_PROEJCT>/bin/note-dev
.
If we run note-dev
, we will see the console output.
Conclusion
- Need to have
"#!/usr/bin/env node"
on top of yourindex.js
file to tell which runtime env it should be. - Need to have
bin
inpackage.json
to tell the name of the cli command. - Need to run
npm link
in your local dev everytime code changes in order to test locally.
Prepare
We want to use Node.js version 18 in order to use import / export
ES6 module syntax.
In order not to change our default node.js installed on our machine.
Let's use volta: Install volta
Then run volta pin node@18
After run, you will find in your package.json
:
"volta": {
"node": "18.17.1"
}
Then to enable import / export
ES6 module syntax, let's add into package.json
"type": "module",
Install a package call yargs
: npm i yargs
CLI implementation
// db.js
import fs from "node:fs/promises";
const DB_PATH = new URL("../db.json", import.meta.url).pathname;
export const getDB = async () => {
const db = await fs.readFile(DB_PATH, "utf-8");
return JSON.parse(db);
};
export const saveDB = async (db) => {
await fs.writeFile(DB_PATH, JSON.stringify(db, null, 2));
return db;
};
export const insertDB = async (note) => {
const db = await getDB();
db.notes.push(note);
await saveDB(db);
return note;
};
// notes.js
import { insertDB, saveDB, getDB } from "./db.js";
export const newNote = async (note, tags = []) => {
const newNote = {
tags,
id: Date.now(),
content: note,
};
await insertDB(newNote);
return newNote;
};
export const getAllNotes = async () => {
const db = await getDB();
return db.notes;
};
export const findNotes = async (filter) => {
const notes = await getAllNotes();
return notes.filter((note) =>
note.content.toLowerCase().includes(filter.toLowerCase())
);
};
export const removeNote = async (id) => {
const notes = await getAllNotes();
const match = notes.find((note) => note.id === id);
if (match) {
const newNotes = notes.filter((note) => note.id !== id);
await saveDB({ notes: newNotes });
return id;
}
};
export const removeAllNotes = async () => {
await saveDB({ notes: [] });
};
// server.js
import fs from "node:fs/promises";
import http from "node:http";
import open from "open";
const interpolate = (html, data) => {
return html.replace(/\{\{\s*(\w+)\s*\}\}/g, (match, placeholder) => {
return data[placeholder] || "";
});
};
const formatNotes = (notes) => {
return notes
.map((note) => {
return `
<div class="note">
<p>${note.content}</p>
<div class="tags">
${note.tags.map((tag) => `<span class="tag">${tag}</span>`).join("")}
</div>
</div>
`;
})
.join("\n");
};
const createServer = (notes) => {
return http.createServer(async (req, res) => {
const HTML_PATH = new URL("./template.html", import.meta.url).pathname;
const template = await fs.readFile(HTML_PATH, "utf-8");
const html = interpolate(template, { notes: formatNotes(notes) });
res.writeHead(200, { "Content-Type": "text/html" });
res.end(html);
});
};
export const start = (notes, port) => {
const server = createServer(notes);
server.listen(port, () => {
console.log(`Server is listening on port ${port}`);
});
open(`http://localhost:${port}`);
};
// commands.js
import yargs from "yargs";
import { hideBin } from "yargs/helpers";
import {
newNote,
getAllNotes,
findNotes,
removeNote,
removeAllNotes,
} from "./notes.js";
import { start } from "./server.js";
const listNotes = (notes) => {
notes.forEach((note) => {
console.log("\n");
console.log("id: ", note.id);
console.log("tags: ", note.tags.join(", ")),
console.log("note: ", note.content);
});
};
yargs(hideBin(process.argv))
.command(
"new <note>",
"create a new note",
(yargs) => {
return yargs.positional("note", {
describe: "The content of the note you want to create",
type: "string",
});
},
async (argv) => {
const tags = argv.tags ? argv.tags.split(",") : [];
const note = await newNote(argv.note, tags);
console.log("new note created", note);
}
)
.option("tags", {
alias: "t",
type: "string",
description: "tags to add to the note",
})
.command(
"all",
"get all notes",
() => {},
async (argv) => {
const notes = await getAllNotes();
listNotes(notes);
}
)
.command(
"find <filter>", // required <filter>
"get matching notes",
(yargs) => {
return yargs.positional("filter", {
describe:
"The search term to filter notes by, will be applied to note.content",
type: "string",
});
},
async (argv) => {
const matches = await findNotes(argv.filter);
listNotes(matches);
}
)
.command(
"remove <id>",
"remove a note by id",
(yargs) => {
return yargs.positional("id", {
type: "number",
description: "The id of the note you want to remove",
});
},
async (argv) => {
const id = await removeNote(argv.id);
console.log("note removed", id);
}
)
.command(
"web [port]", // optional [port]
"launch website to see notes",
(yargs) => {
return yargs.positional("port", {
describe: "port to bind on",
default: 5000,
type: "number",
});
},
async (argv) => {
const notes = await getAllNotes();
start(notes, argv.port);
}
)
.command(
"clean",
"remove all notes",
() => {},
async (argv) => {
await removeAllNotes();
console.log("db reseted");
}
)
.demandCommand(1)
.parse();
Testing
With ES6 import / export syntax which is pretty new, this is how to run jest:
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js"
Example test:
import { jest } from "@jest/globals";
jest.unstable_mockModule("../src/db.js", () => ({
insertDB: jest.fn(),
getDB: jest.fn(),
saveDB: jest.fn(),
}));
const { insertDB, getDB, saveDB } = await import("../src/db.js");
const { newNote, getAllNotes, removeNote } = await import("../src/notes.js");
beforeEach(() => {
insertDB.mockClear();
getDB.mockClear();
saveDB.mockClear();
});
test("newNote inserts data and return it", async () => {
const note = {
content: "this is my note",
id: 1,
tags: ["hello"],
};
insertDB.mockResolvedValue(note);
const result = await newNote(note.content, note.tags);
expect(result.content).toEqual(note.content);
expect(result.tags).toEqual(note.tags);
});
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
2022-08-26 [Web] Preload & Prefetch
2022-08-26 [React] Route-based Splitting
2022-08-26 [React] Import on Visibility
2022-08-26 [React] Compound Pattern
2022-08-26 [React] SWR for data fetching
2022-08-26 [Javascript] Prototype Pattern
2022-08-26 [Javascript] Factory pattern vs Class instance