node-notifier
Version:
A Node.js module for sending notifications on native Mac, Windows (post and pre 8) and Linux (or Growl as fallback)
396 lines (395 loc) • 11.9 kB
JavaScript
import os from "node:os";
import cp from "node:child_process";
import fs from "node:fs";
import net from "node:net";
import path from "node:path";
import isWsl from "is-wsl";
// src/lib/utils.ts
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
}
function escapeQuotes(str) {
return typeof str === "string" ? str.replace(/(["$`\\])/g, "\\$1") : str;
}
const notifySendFlags = {
u: "urgency",
urgency: "urgency",
t: "expire-time",
time: "expire-time",
timeout: "expire-time",
e: "expire-time",
expire: "expire-time",
"expire-time": "expire-time",
i: "icon",
icon: "icon",
c: "category",
category: "category",
subtitle: "category",
h: "hint",
hint: "hint",
a: "app-name",
"app-name": "app-name"
};
function debug(name, notifier, options) {
if (!process.env.DEBUG?.includes("notifier")) return;
console.info(`node-notifier debug info (${name}):`);
console.info("[notifier path]", notifier);
if (options) console.info("[notifier options]", options.join(" "));
}
function commandWithoutShell(notifier, options, cb) {
debug("commandWithoutShell", notifier, options);
return cp.execFile(notifier, options, (error, stdout, stderr) => {
if (error) return cb(error);
cb(stderr, stdout);
});
}
function fileCommand(notifier, options, cb) {
debug("fileCommand", notifier, options);
return cp.execFile(notifier, options, (error, stdout, stderr) => {
if (error) return cb(error, stdout);
cb(stderr, stdout);
});
}
function immediateFileCommand(notifier, options, cb) {
debug("notifier", notifier);
notifierExists(notifier, (_, exists) => {
if (!exists) return cb(/* @__PURE__ */ new Error(`Notifier (${notifier}) not found on system.`));
cp.execFile(notifier, options);
cb();
});
}
function notifierExists(notifier, cb) {
return fs.stat(notifier, (err, stat) => {
if (!err) return cb(err, stat.isFile());
if (path.extname(notifier)) return cb(err, false);
return fs.stat(`${notifier}.exe`, (err, stat) => {
if (err) return cb(err, false);
cb(err, stat.isFile());
});
});
}
function mapAppIcon(options) {
if (options.appIcon) {
options.icon = options.appIcon;
delete options.appIcon;
}
return options;
}
function mapText(options) {
if (options.text) {
options.message = options.text;
delete options.text;
}
return options;
}
function mapIconShorthand(options) {
if (options.i) {
options.icon = options.i;
delete options.i;
}
return options;
}
function mapToNotifySend(options) {
options = mapAppIcon(options);
options = mapText(options);
if (options.timeout === false) delete options.timeout;
if (options.wait === true) options["expire-time"] = 5;
for (const key in options) {
if (key === "message" || key === "title") continue;
const flag = Object.hasOwn(notifySendFlags, key) ? notifySendFlags[key] : void 0;
if (Object.hasOwn(options, key) && flag !== key) {
if (flag !== void 0) options[flag] = options[key];
delete options[key];
}
}
if (options["expire-time"] === void 0) options["expire-time"] = 1e4;
else if (typeof options["expire-time"] === "number") options["expire-time"] = options["expire-time"] * 1e3;
return options;
}
function mapToGrowl(options) {
options = mapAppIcon(options);
options = mapIconShorthand(options);
options = mapText(options);
if (options.icon && !Buffer.isBuffer(options.icon)) try {
options.icon = fs.readFileSync(options.icon);
} catch {}
return options;
}
function mapToMac(options) {
options = mapIconShorthand(options);
options = mapText(options);
delete options.icon;
delete options.appIcon;
delete options.sender;
delete options.closeLabel;
delete options.dropdownLabel;
if (typeof options.actions === "string") options.actions = [options.actions];
if (options.reply === true) options.reply = "";
else if (options.reply === false) delete options.reply;
if (options.sound === true) options.sound = "Bottle";
if (options.sound === false) delete options.sound;
if (options.sound && options.sound.indexOf("Notification.") === 0) options.sound = "Bottle";
if (options.wait === true) {
if (!options.timeout) options.timeout = 5;
delete options.wait;
}
if (!options.wait && !options.timeout) {
if (options.timeout === false) delete options.timeout;
else options.timeout = 10;
}
if (!options.actions && options.reply === void 0) delete options.timeout;
return options;
}
function noop() {}
function actionJackerDecorator(emitter, options, fn, mapper) {
options = clone(options);
fn = fn || noop;
if (typeof fn !== "function") throw new TypeError(`The second argument must be a function callback. You have passed ${typeof fn}`);
const callback = fn;
return (err, data) => {
let resultantData = data;
let metadata = {};
if (data && typeof data === "object") {
metadata = data;
resultantData = metadata.activationType;
}
if (resultantData) {
resultantData = resultantData.toLowerCase().trim();
if (/^activate|clicked$/.test(resultantData)) resultantData = "activate";
if (/^timedout$/.test(resultantData)) resultantData = "timeout";
}
callback.call(emitter, err, resultantData, metadata);
if (!mapper || !resultantData) return;
const key = mapper(resultantData);
if (!key) return;
emitter.emit(key, emitter, options, metadata);
};
}
function constructArgumentList(options, extra = {}) {
const args = [];
const initial = extra.initial || [];
const keyExtra = extra.keyExtra || "";
const allowedArguments = extra.allowedArguments || [];
const noEscape = extra.noEscape !== void 0;
const checkForAllowed = extra.allowedArguments !== void 0;
const explicitTrue = !!extra.explicitTrue;
const keepNewlines = !!extra.keepNewlines;
const wrapper = extra.wrapper === void 0 ? "\"" : extra.wrapper;
const escapeFn = (arg) => {
if (Array.isArray(arg)) return removeNewLines(arg.map(escapeFn).join(","));
if (!noEscape) arg = escapeQuotes(arg);
if (typeof arg === "string" && !keepNewlines) arg = removeNewLines(arg);
return wrapper + arg + wrapper;
};
for (const val of initial) args.push(escapeFn(val));
for (const key in options) if (Object.hasOwn(options, key) && (!checkForAllowed || allowedArguments.includes(key))) {
if (explicitTrue && options[key] === true) args.push(`-${keyExtra}${key}`);
else if (explicitTrue && options[key] === false) continue;
else args.push(`-${keyExtra}${key}`, escapeFn(options[key]));
}
return args;
}
function removeNewLines(str) {
const escapedNewline = process.platform === "win32" ? "\\r\\n" : "\\n";
return str.replace(/\r?\n/g, escapedNewline);
}
const allowedToasterFlags = [
"t",
"m",
"b",
"tb",
"p",
"id",
"s",
"silent",
"appID",
"pid",
"pipeName",
"close",
"install",
"d",
"application"
];
const toasterSoundPrefix = "Notification.";
const toasterDefaultSound = "Notification.Default";
function toWslIconPath(icon) {
if (/^\/?[a-zA-Z]:[\\/]/.test(icon)) return icon.replace(/^\//, "").replace(/\//g, "\\");
const [, drive, rest] = /^\/mnt\/([a-z])\/(.+)$/.exec(icon) ?? [];
if (!drive || !rest || rest.split("/").includes("..")) return;
return `${drive.toUpperCase()}:\\${rest.replace(/\//g, "\\")}`;
}
function mapToWin8(options) {
options = mapAppIcon(options);
options = mapText(options);
if (options.icon) {
const isFileUrl = /^file:\/+/.test(options.icon);
const icon = isFileUrl ? new URL(options.icon).pathname : options.icon;
if (utils.isWSL()) {
const wslIcon = toWslIconPath(icon);
if (wslIcon) options.p = wslIcon;
} else if (isFileUrl) options.p = icon.replace(/^\/(\w:\/)/, "$1").replace(/\//g, "\\");
else options.p = icon;
delete options.icon;
}
if (options.message) {
options.m = options.message.replace(/\x1b/g, "");
delete options.message;
}
if (options.title) {
options.t = options.title;
delete options.title;
}
if (options.appName) {
options.appID = options.appName;
delete options.appName;
}
if (options.remove !== void 0) {
options.close = options.remove;
delete options.remove;
}
if (options.quiet || options.silent) {
options.silent = options.quiet || options.silent;
delete options.quiet;
}
if (options.sound !== void 0) {
options.s = options.sound;
delete options.sound;
}
if (options.s === false) {
options.silent = true;
delete options.s;
}
if (options.s && options.silent) delete options.s;
if (options.s === true) options.s = toasterDefaultSound;
if (options.s && options.s.indexOf(toasterSoundPrefix) !== 0) options.s = toasterDefaultSound;
if (typeof options.application !== "string" || !options.application) delete options.application;
if (options.actions && Array.isArray(options.actions)) {
options.b = options.actions.join(";");
delete options.actions;
}
const duration = typeof options.duration === "string" ? options.duration.toLowerCase() : "";
delete options.duration;
delete options.d;
if (duration === "short" || duration === "long") options.d = duration;
for (const key in options) if (Object.hasOwn(options, key) && !allowedToasterFlags.includes(key)) delete options[key];
return options;
}
function mapToNotifu(options) {
options = mapAppIcon(options);
options = mapText(options);
if (options.icon) {
options.i = options.icon;
delete options.icon;
}
if (options.message) {
options.m = options.message;
delete options.message;
}
if (options.title) {
options.p = options.title;
delete options.title;
}
if (options.time) {
options.d = options.time;
delete options.time;
}
if (options.q !== false) options.q = true;
else delete options.q;
if (options.quiet === false) {
delete options.q;
delete options.quiet;
}
if (options.sound) {
delete options.q;
delete options.sound;
}
if (options.t) {
options.d = options.t;
delete options.t;
}
if (options.type) {
options.t = sanitizeNotifuTypeArgument(options.type);
delete options.type;
}
return options;
}
function isMac() {
return os.type() === "Darwin";
}
function isMojaveOrLater() {
return os.type() === "Darwin" && releaseSatisfies(">=", "18.0.0");
}
function isWin8() {
return os.type() === "Windows_NT" && releaseSatisfies(">=", "6.2.9200");
}
function isWSL() {
return isWsl;
}
function isLessThanWin8() {
return os.type() === "Windows_NT" && releaseSatisfies("<", "6.2.9200");
}
function guaranteeSemverFormat(version) {
if (version.split(".").length === 2) version += ".0";
return version;
}
function parseVersion(version) {
const match = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(\+.*)?$/.exec(version.trim());
return match ? match.slice(1, 4).map(Number) : null;
}
function releaseSatisfies(operator, version) {
const current = parseVersion(guaranteeSemverFormat(os.release()));
const target = parseVersion(version);
if (!current || !target) return false;
let diff = 0;
for (let i = 0; i < 3 && diff === 0; i++) diff = (current[i] ?? 0) - (target[i] ?? 0);
return operator === "<" ? diff < 0 : diff >= 0;
}
function sanitizeNotifuTypeArgument(type) {
if (typeof type === "string" || type instanceof String) {
const lower = type.toLowerCase();
if (lower === "info" || lower === "warn" || lower === "error") return lower;
}
return "info";
}
function createNamedPipe(server) {
const chunks = [];
return new Promise((resolve) => {
const instance = net.createServer((stream) => {
stream.on("data", (c) => {
chunks.push(c);
});
stream.on("end", () => {
instance.close();
});
});
server.instance = instance;
instance.listen({
path: server.namedPipe,
exclusive: true
}, () => {
resolve(chunks);
});
});
}
const utils = {
clone,
commandWithoutShell,
fileCommand,
immediateFileCommand,
mapToNotifySend,
mapToGrowl,
mapToMac,
isArray: Array.isArray,
actionJackerDecorator,
constructArgumentList,
mapToWin8,
mapToNotifu,
isMac,
isMojaveOrLater,
isWin8,
isWSL,
isLessThanWin8,
createNamedPipe
};
export { utils as default };
//# sourceMappingURL=utils.js.map