UNPKG

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
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