npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ix-xs/node-comfort

v1.1.31

Published

Small collection of Node.js utilities: colorful logger, filesystem helper, SQLite wrapper, runtime type checks and async/process helpers.

Readme

@ix-xs/node-comfort

Node.js JavaScript NPM Github

npm version Downloads

Small collection of Node.js utilities I use across projects: a logger, a file-system helper, a SQLite wrapper, a runtime type checker, and a few async/process helpers.

Everything is plain CommonJS with JSDoc. The package ships with type information for TypeScript and modern editors.


Installation

npm install @ix-xs/node-comfort
# or
yarn add @ix-xs/node-comfort
# or
pnpm add @ix-xs/node-comfort

Requires Node 20+ (uses node:sqlite and process.loadEnvFile).


Import and global usage

The package exposes a flat API: all utilities are directly on the nodeComfort object.

const nodeComfort = require("@ix-xs/node-comfort");


// Logger
nodeComfort.log("Hello");
nodeComfort.log("<% greenBright Success %>");


// FS
const files = nodeComfort.getFilesIn(".", true);
nodeComfort.createFolder("./dist");


// Checker
if (nodeComfort.isArray(files)) {
  nodeComfort.log(`<% cyan Found ${files.length} files %>`);
}


// Utils
await nodeComfort.wait(500);
nodeComfort.dontCrash();


// SQLite
const db = new nodeComfort.SQLite();
await db.createTable({
  name: "users",
  columns: {
    id:   { type: "INTEGER", primaryKey: true, autoincrement: true },
    name: { type: "TEXT", notNull: true },
  },
});

Logger

Colorized console logger with a minimal markup syntax and optional timestamps.

You wrap styles in delimiters (by default <% and %>):

  • styles: bold, italic, underline, overline
  • colors: red, green, yellow, blue, magenta, cyan, white, gray, black
  • bright: redBright, greenBright, yellowBright, blueBright, magentaBright, cyanBright, whiteBright
  • background: bgRed, bgBlue, etc.
  • RGB: rgb(255, 0, 0)

Basic usage

nodeComfort.log("Plain log");
nodeComfort.log("<% green Success %>");
nodeComfort.log("<% red bold Error:%> something went wrong");
nodeComfort.log("<% bgBlue white INFO %> message");

Multi-line strings keep indentation and timestamps readable:

nodeComfort.log(`
<% yellow Multi-line %>
This is line 2
This is line 3
`);

Timestamp and delimiters

// Disable timestamps
nodeComfort.setTimestamp(false);


// Change delimiters if "<%" conflicts with other template syntax
nodeComfort.setDelimiter({ open: "{{", close: "}}" });
nodeComfort.log("{{red Hello}} world");

Groups

Groups add indentation to related logs.

nodeComfort.group("Startup");


nodeComfort.log("Loading config…");
nodeComfort.log("<% green OK %>");


nodeComfort.group("DB");
nodeComfort.log("Connecting…");
// ...
nodeComfort.groupEnd(); // end "DB"


nodeComfort.groupEnd(); // end "Startup"

FS

File-system helper with safe path resolution, recursive walking, copy/move helpers, and a small watcher abstraction.

It resolves relative paths from the caller file, not just from process.cwd(), which makes it nicer inside libraries.

Path helpers

// Absolute path, even if it doesn't exist yet
const configPath = nodeComfort.createPath("./config/app.json");


// Existing folder/file or undefined
const folder = nodeComfort.getFolder("./src");
const file = nodeComfort.getFile("package.json");

Create folders and files

// Folders
nodeComfort.createFolder("./dist");        // true if created
nodeComfort.createFolder("./dist", true);  // force recreate


// Files
nodeComfort.createFile("./dist/info.txt", false, "hello");
nodeComfort.createFile("./dist/data.json", true, { ok: true });

List and delete

// Folders inside a path
const dirs = nodeComfort.getFoldersIn("./src", true);


// Delete folders matching a filter
nodeComfort.deleteFoldersIn("./tmp", (folder) =>
  folder.endsWith(".cache"),
);


// Files
const files = nodeComfort.getFilesIn("./src", true);


// Delete files matching a filter
nodeComfort.deleteFilesIn("./logs", false, (file) =>
  file.endsWith(".log"),
);

Copy

// Copy one folder
nodeComfort.copyFolder({
  path: "./templates",
  dest: "./dist/templates",
  recursive: true,
  withFiles: true,
  force: true,
});


// Copy only some folders in a tree
nodeComfort.copyFoldersIn({
  path: "./src",
  dest: "./dist",
  recursive: true,
  withFiles: false,
  filter: (folder) => folder.endsWith("components"),
});


// Copy files
nodeComfort.copyFilesIn({
  path: "./src",
  dest: "./dist",
  recursive: true,
  force: true,
  filter: (file) => file.endsWith(".js"),
});


// Copy a single file
nodeComfort.copyFile({
  path: "./src/index.js",
  dest: "./dist", // or "./dist/index.js"
  force: true,
});

Move

Move helpers are basically copy + delete.

// Move a folder tree
nodeComfort.moveFolder({
  path: "./build",
  dest: "./dist",
  recursive: true,
  withFiles: true,
  force: true,
});


// Move some folders under a path
nodeComfort.moveFoldersIn({
  path: "./tmp",
  dest: "./archive",
  recursive: true,
  withFiles: false,
  filter: (folder) => folder.includes("session"),
});


// Move files
nodeComfort.moveFilesIn({
  path: "./logs",
  dest: "./logs/archive",
  recursive: false,
  force: true,
  filter: (file) => file.endsWith(".log"),
});


// Move a single file
nodeComfort.moveFile({
  path: "./logs/app.log",
  dest: "./logs/archive/app.log",
  force: true,
});

Read and watch

// Read a UTF-8 file
const content = nodeComfort.readFile("./README.md");


// Watch a directory
const watcher = nodeComfort.watch({
  path: "./src",
  recursive: true,
  filter: (event, file) => file.endsWith(".js"),
});


watcher
  .on("change", (file) => {
    nodeComfort.log(`<% cyan File changed: %> ${file}`);
  })
  .on("rename", (file) => {
    nodeComfort.log(`<% yellow File renamed: %> ${file}`);
  });

You can pause, resume and stop watching:

watcher.pause();
// ...
watcher.resume();
// ...
watcher.stop();

Checker

Runtime type-checking helpers. Everything is just a boolean guard that works on any input.

nodeComfort.isArray();        // true[1][2]
nodeComfort.isArray("nope");        // false


nodeComfort.isNumber(42);           // true
nodeComfort.isNumber(NaN);          // false


nodeComfort.isFunction(() => {});   // true
nodeComfort.isFunction(class {});   // true


nodeComfort.isObject({});           // true
nodeComfort.isObject([]);           // true
nodeComfort.isObject(null);         // false


nodeComfort.isBoolean(false);       // true
nodeComfort.isString("hello");      // true
nodeComfort.isUndefined(undefined); // true
nodeComfort.isNull(null);           // true


nodeComfort.isDate(new Date());     // true
nodeComfort.isMap(new Map());       // true
nodeComfort.isSet(new Set());       // true


nodeComfort.isError(new Error("x")); // true
nodeComfort.isError({ message: "x" }); // false

Utils

Small async and process helpers.

wait(ms)

await nodeComfort.wait(500);
// code here runs after ~500ms

when(predicate, payload?, options?)

Polling helper that emits events until a condition is met.

const task = nodeComfort.when(
  () => Math.random() > 0.8,
  { label: "Lucky" },
  { interval: 200, max: 3, timeout: 5000 },
);


task
  .on("trigger", (payload) => {
    nodeComfort.log(`<% green Triggered: %> ${payload.label}`);
  })
  .on("timeout", () => {
    nodeComfort.log("<% red Timeout %>");
  })
  .on("error", (err) => {
    nodeComfort.log(`<% red Error in predicate %>\n${err}`);
  })
  .start();
  • predicate: boolean, promise, or function (sync/async) returning a boolean.
  • payload: value passed to listeners on trigger / timeout.
  • options:
    • interval (ms, default 50)
    • timeout (ms or null)
    • max (number of triggers or null for infinite)

dontCrash()

Installs process-level safety nets for errors, exits and signals.

nodeComfort.dontCrash();


// Or customize:
nodeComfort.dontCrash()
  .on("error", (err) => {
    console.error("Global error:", err);
    process.exit(1);
  })
  .on("exit", (code) => {
    nodeComfort.log(`<% gray Process exited with code ${code} %>`);
  })
  .on("sig", (signal) => {
    nodeComfort.log(`<% yellow Caught signal ${signal}, exiting… %>`);
    process.exit(0);
  });

Events:

  • "error" – uncaught exceptions + unhandled rejections
  • "exit"process.on("exit")
  • "sig"SIGINT, SIGTERM, SIGQUIT
  • "beforeExit"process.on("beforeExit")

JSON helpers

const json = nodeComfort.JSONString({ hello: "world" });
// pretty-printed with 4 spaces


const data = nodeComfort.JSONParse(json);

SQLite

Thin wrapper over the built-in node:sqlite DatabaseSync with a small "model" system and CRUD-style helpers.

Creating an instance

// Default: ./db.sqlite (relative to cwd)
const db = new nodeComfort.SQLite();


// Custom path (folder created if needed)
const db2 = new nodeComfort.SQLite("data/app.sqlite");

The constructor ensures the parent folder exists, and enables WAL journal mode and foreign keys by default.

Defining a table

await db.createTable({
  name: "users",
  columns: {
    id: {
      type: "INTEGER",
      primaryKey: true,
      autoincrement: true,
    },
    name: {
      type: "TEXT",
      notNull: true,
    },
    role: {
      type: "TEXT",
      values: ["admin", "user"],
      defaultValue: "user",
    },
    createdAt: {
      type: "TEXT",
      defaultValue: new Date().toISOString(),
    },
  },
});

Models are kept in memory and used for defaults like ordering.

The columns property is the new, recommended way. The legacy options property is still supported for backward compatibility.

Insert / update / upsert

// Insert a new row
await db.insert("users", { name: "John" });


// Update rows matching a where clause
await db.update("users", { role: "admin" }, { name: "John" });


// Upsert (INSERT with ON CONFLICT DO UPDATE)
await db.upsert("users", { id: 1, role: "user" }, ["id"]);

When where or data.id is provided, set (legacy helper):

  1. loads the existing row if it exists,
  2. merges it with data,
  3. inserts or updates depending on whether the row exists.
// Insert
await db.set("users", { name: "John" });


// Upsert by id
await db.set("users", { id: 1, role: "admin" });


// Upsert with custom where
await db.set("users", { role: "user" }, { name: "John" });

Get / list

// One row or undefined
const user = await db.get("users", { name: "John" });


// Multiple rows with pagination and ordering
const admins = await db.getAll(
  "users",
  { role: "admin" }, // where
  {
    limit: 100,
    offset: 0,
    orderBy: "createdAt",
    direction: "DESC",
  },
);

If the table is not registered, getAll returns an empty array.

Delete / clear / count

// Delete rows
const res = await db.delete("users", { role: "user" });
// res.ok, res.changes


// Delete all rows
await db.clearTable("users");


// Count rows
const totalUsers = await db.count("users");
const adminCount = await db.count("users", { role: "admin" });

Indexes and constraints

// Create a composite unique index
await db.createIndex("UserAuthMethod", {
  columns: ["provider", "providerUserId"],
  unique: true,
});


// Create a non-unique index
await db.createIndex("Post", {
  columns: ["userId"],
});
// Table-level constraints
await db.createTable({
  name: "UserAuthMethod",
  columns: {
    id: { type: "INTEGER", primaryKey: true, autoincrement: true },
    userId: { type: "INTEGER", notNull: true },
    provider: { type: "TEXT", notNull: true },
    providerUserId: { type: "TEXT", notNull: true },
  },
  constraints: [
    {
      type: "unique",
      columns: ["provider", "providerUserId"],
    },
    {
      type: "foreignKey",
      columns: ["userId"],
      references: { table: "User", columns: ["id"] },
      onDelete: "CASCADE",
    },
  ],
});

Transactions

const result = await db.transaction(async () => {
  await db.insert("User", {
    email: "[email protected]",
    createdAt: Date.now(),
  });
  await db.insert("Profile", {
    userId: 1,
    nickname: "John",
  });
  return { ok: true };
});

If the callback throws, the transaction is rolled back.

Raw SQL helpers

// Execute a write statement (INSERT/UPDATE/DELETE/CREATE/PRAGMA)
await db.exec("CREATE INDEX IF NOT EXISTS idx_email ON users (email)");


// Execute a query and get one row
const user = await db.queryOne("SELECT * FROM users WHERE id = ?", );[1]


// Execute a query and get all rows
const users = await db.queryAll("SELECT * FROM users WHERE role = ?", ["admin"]);

Drop and close

await db.deleteTable("users");
db.close();

License

MIT.