Add support for logging to console and files (#159)

* Add support for logging to console and files

* Added support for internal and custom log categories
Added support for disabling log categories
Added support for setting desired log levels for both console and file
This commit is contained in:
Lennard Fonteijn 2023-03-26 20:59:57 +02:00 committed by GitHub
parent 3c25f20174
commit f732942e39
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 404 additions and 34 deletions

View File

@ -7,3 +7,4 @@ webui/dist
*.plugin.js
*Plugin.js
components/contracts.json
logs

View File

@ -1,3 +1,6 @@
@echo off
SET LOG_LEVEL_CONSOLE=info
SET LOG_CATEGORY_DISABLED=
SET LOG_MAX_FILES=14d
.\nodedist\node.exe chunk0.js
PAUSE

View File

@ -19,6 +19,16 @@
import type { NextFunction, Response } from "express"
import type { RequestWithJwt } from "./types/types"
import picocolors from "picocolors"
import winston from "winston"
import "winston-daily-rotate-file"
const isDebug = ["*", "true", "peacock", "yes"].includes(
process.env.DEBUG || "false",
)
const isTest = ["*", "true", "peacock", "yes"].includes(
process.env.TEST || "false",
)
/**
* Represents the different log levels.
@ -27,30 +37,110 @@ export enum LogLevel {
/**
* For errors. Displays in red.
*/
ERROR,
ERROR = "error",
/**
* For warnings. Displays in yellow.
*/
WARN,
WARN = "warn",
/**
* For information. Displays in blue.
* This is also the fallback for invalid log level values.
*/
INFO,
INFO = "info",
/**
* For debugging.
* Displays in light blue, but only if the `DEBUG` environment variable is set to "*", "yes", "true", or "peacock".
* For debugging. Displays in light blue.
*/
DEBUG,
DEBUG = "debug",
/**
* For outputting stacktraces.
*/
TRACE,
TRACE = "trace",
/**
* For extremely verbose purposes.
*/
SILLY = "silly",
}
const isDebug = ["*", "true", "peacock", "yes"].includes(
process.env.DEBUG || "false",
)
/**
* Represents the different internal log categories used by Peacock.
*/
export enum LogCategory {
/**
* Remove the category from the log
*/
NONE = "none",
/**
* Set the category to the name of the calling function
*/
CALLER = "caller",
/**
* Used for logging HTTP request
*/
HTTP = "http",
}
const LOG_LEVEL_NONE = "none"
//NOTE: Tests run in "strict mode", so only enable CALLER by default for debug-mode.
const LOG_CATEGORY_DEFAULT =
isDebug && !isTest ? LogCategory.CALLER : LogCategory.NONE
const fileLogLevel = process.env.LOG_LEVEL_FILE || LogLevel.TRACE
const consoleLogLevel = process.env.LOG_LEVEL_CONSOLE || LogLevel.INFO
const disabledLogCategories =
process.env.LOG_CATEGORY_DISABLED?.split(",") || []
const transports = []
if (fileLogLevel !== LOG_LEVEL_NONE) {
const fileTransport = new winston.transports.DailyRotateFile({
filename: "logs/peacock-%DATE%.json",
datePattern: "YYYYMMDDHHmmss",
frequency: "1d",
maxFiles: process.env.LOG_MAX_FILES,
level: fileLogLevel,
format: winston.format.printf((info) => {
return JSON.stringify(info.data)
}),
})
transports.push(fileTransport)
}
if (consoleLogLevel !== LOG_LEVEL_NONE) {
const consoleTransport = new winston.transports.Console({
level: consoleLogLevel,
format: winston.format.combine(
winston.format((info) => {
if (
!info.data.category ||
!disabledLogCategories.includes(info.data.category)
) {
return info
}
return false
})(),
winston.format.printf((info) => {
if (info.data.stack) {
return `${info.message}\n${info.data.stack}`
}
return info.message
}),
),
})
transports.push(consoleTransport)
}
const winstonLogLevel = {}
Object.values(LogLevel).forEach((e, i) => (winstonLogLevel[e] = i))
const logger = winston.createLogger({
levels: winstonLogLevel,
transports: transports,
})
/**
* Adds leading zeros to a number so that the length of the string will always
@ -78,10 +168,22 @@ export function logDebug(...args: unknown[]): void {
*
* @param level The message's level.
* @param data The data to output.
* @param category The message's category.
* @see LogLevel
*
* @function log
*/
export function log(level: LogLevel, data: string): void {
const m = data ?? "No message specified"
export function log(
level: LogLevel,
data: string,
category: LogCategory | string = LOG_CATEGORY_DEFAULT,
): void {
if (category === LogCategory.CALLER) {
category = log.caller?.name.toString() ?? "unknown"
}
const message = data ?? "No message specified"
const now = new Date()
const stampParts: number[] = [
now.getHours(),
@ -93,41 +195,56 @@ export function log(level: LogLevel, data: string): void {
.map((part) => zeroPad(part, 2))
.join(":")}:${millis}`
const header = picocolors.gray(timestamp)
let outputTransport: (
message?: unknown,
...optionalParams: unknown[]
) => void
const timestampColored = picocolors.gray(timestamp)
const categoryColored = picocolors.gray(category)
let levelString: string
let levelStringColored: string
let stack = undefined
switch (level) {
case LogLevel.ERROR:
outputTransport = console.error
levelString = picocolors.red("Error")
levelString = "Error"
levelStringColored = picocolors.red(levelString)
break
case LogLevel.WARN:
outputTransport = console.warn
levelString = picocolors.yellow("Warn")
levelString = "Warn"
levelStringColored = picocolors.yellow(levelString)
break
case LogLevel.INFO:
default:
outputTransport = console.log
levelString = picocolors.blue("Info")
levelString = "Info"
levelStringColored = picocolors.blue(levelString)
break
case LogLevel.DEBUG:
if (!isDebug) {
return
}
outputTransport = console.log
levelString = picocolors.blueBright("Debug")
levelString = "Debug"
levelStringColored = picocolors.blueBright(levelString)
break
case LogLevel.TRACE:
outputTransport = console.trace
levelString = picocolors.bgYellow("Trace")
levelString = "Trace"
levelStringColored = picocolors.bgYellow(levelString)
stack = new Error("Trace").stack
break
}
outputTransport(`[${header}] [${levelString}] ${m}`)
const categoryAndLevel =
category === LogCategory.NONE
? levelStringColored
: `${levelStringColored} | ${categoryColored}`
logger.log(
level,
`[${timestampColored}] [${categoryAndLevel}] ${message}`,
{
data: {
timestamp: timestamp,
category: category,
level: levelString,
message: message,
stack: stack,
},
},
)
}
/**
@ -146,6 +263,7 @@ export function loggingMiddleware(
log(
LogLevel.INFO,
`${picocolors.green(req.method)} ${picocolors.underline(req.url)}`,
LogCategory.HTTP,
)
next?.()
}

View File

@ -90,7 +90,9 @@
"rimraf": "^4.4.0",
"terser": "^5.16.6",
"typescript": "4.9.5",
"vitest": "0.29.6"
"vitest": "0.29.6",
"winston": "3.8.2",
"winston-daily-rotate-file": "4.7.1"
},
"engines": {
"node": "18.x || 19.x",

View File

@ -34,6 +34,7 @@ global.HUMAN_VERSION = version
global.REV_IDENT = revisionIdent
process.env.DEBUG = "peacock"
process.env.LOG_MAX_FILES = ""
// now we launch the server

View File

@ -4,4 +4,7 @@ Object.assign(globalThis, {
REV_IDENT: 1,
})
process.env.TEST = "peacock"
process.env.LOG_LEVEL_FILE = "none"
export {}

246
yarn.lock
View File

@ -5,6 +5,24 @@ __metadata:
version: 7
cacheKey: 9
"@colors/colors@npm:1.5.0":
version: 1.5.0
resolution: "@colors/colors@npm:1.5.0"
checksum: 5e08870799494f68e5b3b79e9a337bbf5fd7e634904fbbe642769921bf158fe458c41c888f88edf051b78c5325e3339970f00b24e31421c3480bb58f02687218
languageName: node
linkType: hard
"@dabh/diagnostics@npm:^2.0.2":
version: 2.0.3
resolution: "@dabh/diagnostics@npm:2.0.3"
dependencies:
colorspace: "npm:1.1.x"
enabled: "npm:2.0.x"
kuler: "npm:^2.0.0"
checksum: 6e55110ee3d975baaa03dd87bc7bd9acf69242c27436355f6827d3182af09fdd1d249ff4c0635c95ca8ba47f3c2175ada844d1cc93ecb6adf00f12f5a3198697
languageName: node
linkType: hard
"@esbuild/android-arm64@npm:0.17.12":
version: 0.17.12
resolution: "@esbuild/android-arm64@npm:0.17.12"
@ -484,6 +502,8 @@ __metadata:
terser: "npm:^5.16.6"
typescript: "npm:4.9.5"
vitest: "npm:0.29.6"
winston: "npm:3.8.2"
winston-daily-rotate-file: "npm:4.7.1"
languageName: unknown
linkType: soft
@ -772,6 +792,13 @@ __metadata:
languageName: node
linkType: hard
"@types/triple-beam@npm:^1.3.2":
version: 1.3.2
resolution: "@types/triple-beam@npm:1.3.2"
checksum: 75d86c5425ce28bbcbb3b1d246f521f9f0dec0350c4d0ef8cc812fa6e69f4b79886ddb7e8cb62adf9bbeecb50bed92ec3b00d53ff933b8f2589934c8b3ca51f1
languageName: node
linkType: hard
"@types/url-parse@npm:^1.4.8":
version: 1.4.8
resolution: "@types/url-parse@npm:1.4.8"
@ -1169,6 +1196,13 @@ __metadata:
languageName: node
linkType: hard
"async@npm:^3.2.3":
version: 3.2.4
resolution: "async@npm:3.2.4"
checksum: 9719e38d24e9922c255ee9ae925fb668ef52243f9866a1b59e423a3bb6150a886b3c37287348ceefa09cd3f6fa1a29dcc770eeb70642acb13674363b2d5b2b21
languageName: node
linkType: hard
"asynckit@npm:^0.4.0":
version: 0.4.0
resolution: "asynckit@npm:0.4.0"
@ -1443,6 +1477,15 @@ __metadata:
languageName: node
linkType: hard
"color-convert@npm:^1.9.3":
version: 1.9.3
resolution: "color-convert@npm:1.9.3"
dependencies:
color-name: "npm:1.1.3"
checksum: 42f852d574dc58609bba286cd7d10a407e213e20515c0d5d1dd8059b3d4373cd76d1057c3a242f441f2dfc6667badeb790a792662082c8038889c9235f4cd9fa
languageName: node
linkType: hard
"color-convert@npm:^2.0.1":
version: 2.0.1
resolution: "color-convert@npm:2.0.1"
@ -1452,13 +1495,30 @@ __metadata:
languageName: node
linkType: hard
"color-name@npm:~1.1.4":
"color-name@npm:1.1.3":
version: 1.1.3
resolution: "color-name@npm:1.1.3"
checksum: b7313c98fd745336a5e1d64921591bcd60e4e0b3894afb56286a4793c4fd304d4a38b00b514845381215ca5ed2994be05d2e1a5a80860b996d26f5f285c77dda
languageName: node
linkType: hard
"color-name@npm:^1.0.0, color-name@npm:~1.1.4":
version: 1.1.4
resolution: "color-name@npm:1.1.4"
checksum: 80acf64638343898f5b36825f4c9715ced380e738400b308f3f90ca2327f2f98f0c2cfb1f1a6447f267a2e1d1ea2214f26e948d8acab547e5478e2b0816c7c30
languageName: node
linkType: hard
"color-string@npm:^1.6.0":
version: 1.9.1
resolution: "color-string@npm:1.9.1"
dependencies:
color-name: "npm:^1.0.0"
simple-swizzle: "npm:^0.2.2"
checksum: cf76db4143e9d375401d56831ec6bffdfff17aa90276a41dcbdb1723fd7242b2cb6ed2058901544af5823fdf152cdea02eda8546cdd3fe96d4a6a16920166902
languageName: node
linkType: hard
"color-support@npm:^1.1.3":
version: 1.1.3
resolution: "color-support@npm:1.1.3"
@ -1468,6 +1528,26 @@ __metadata:
languageName: node
linkType: hard
"color@npm:^3.1.3":
version: 3.2.1
resolution: "color@npm:3.2.1"
dependencies:
color-convert: "npm:^1.9.3"
color-string: "npm:^1.6.0"
checksum: 480f06a09a02d40fba097b8a88616f449929e8ba33efbfba2838805e8742effcc0b89c7d223fcf2a2964961ac782d7ea6edc3a26adddc564a3ae768edc48b77c
languageName: node
linkType: hard
"colorspace@npm:1.1.x":
version: 1.1.4
resolution: "colorspace@npm:1.1.4"
dependencies:
color: "npm:^3.1.3"
text-hex: "npm:1.0.x"
checksum: 97577bbe4b3039775ad70979bddbc23cc5714406fdaa622d108e7994a32c69f18f32a15511a5708afa3e3c10e93d667abd0ce3a8e7c51e44566d1b2975a00b4d
languageName: node
linkType: hard
"combined-stream@npm:^1.0.8":
version: 1.0.8
resolution: "combined-stream@npm:1.0.8"
@ -1727,6 +1807,13 @@ __metadata:
languageName: node
linkType: hard
"enabled@npm:2.0.x":
version: 2.0.0
resolution: "enabled@npm:2.0.0"
checksum: 722182ea7481286907a44024bd84ed5f063cc5a3f9ccf143b3456dbbfb31e49fc81ce6bf9c44026a23b2a411999c39c4402c10540a72497d2db96c120f8ee77b
languageName: node
linkType: hard
"encodeurl@npm:~1.0.2":
version: 1.0.2
resolution: "encodeurl@npm:1.0.2"
@ -2219,6 +2306,13 @@ __metadata:
languageName: node
linkType: hard
"fecha@npm:^4.2.0":
version: 4.2.3
resolution: "fecha@npm:4.2.3"
checksum: e3764f1c8738b9b261a5dd515f70f4a7c4802f1239c6f075f0ab9990e36b1dfbb0610b0fa81cd9f95afe11a9e687e3c231fd1e368d918ac35cce2f5b0739005e
languageName: node
linkType: hard
"file-entry-cache@npm:^6.0.1":
version: 6.0.1
resolution: "file-entry-cache@npm:6.0.1"
@ -2228,6 +2322,15 @@ __metadata:
languageName: node
linkType: hard
"file-stream-rotator@npm:^0.6.1":
version: 0.6.1
resolution: "file-stream-rotator@npm:0.6.1"
dependencies:
moment: "npm:^2.29.1"
checksum: 7459c7c762658714afebe0d1ef5a0d09b0a0eabcb1ab500edbaf3e76ead88d9188a9e1855655dbdc94d968a5d14385592462a390c239cd24173738b5870c97b2
languageName: node
linkType: hard
"fill-range@npm:^7.0.1":
version: 7.0.1
resolution: "fill-range@npm:7.0.1"
@ -2279,6 +2382,13 @@ __metadata:
languageName: node
linkType: hard
"fn.name@npm:1.x.x":
version: 1.1.0
resolution: "fn.name@npm:1.1.0"
checksum: 54a27208733c14b3bae6118a6cdb6aa03b108f53491dd95fd956f31f2715ce977f48805c473fd0fdc0f93c15e8ffb7f5eaf36a953f781c21450221a2536fa2e7
languageName: node
linkType: hard
"follow-redirects@npm:^1.15.0":
version: 1.15.2
resolution: "follow-redirects@npm:1.15.2"
@ -2720,6 +2830,13 @@ __metadata:
languageName: node
linkType: hard
"is-arrayish@npm:^0.3.1":
version: 0.3.2
resolution: "is-arrayish@npm:0.3.2"
checksum: aed0a701c526d97138e196db5e445da84fea5b649e9466c1d592d2fa7a2a12aa37acb03ca313c38341787dcec5c45b20559bb2abc101dad585d82227e6bc5480
languageName: node
linkType: hard
"is-core-module@npm:^2.9.0":
version: 2.11.0
resolution: "is-core-module@npm:2.11.0"
@ -2803,6 +2920,13 @@ __metadata:
languageName: node
linkType: hard
"is-stream@npm:^2.0.0":
version: 2.0.1
resolution: "is-stream@npm:2.0.1"
checksum: 763e33689433924775b560e63fb7c0f7fae6cbc54fd9c410bb3536341b96fca85ce26720ba13ffb9b46446bdf540308771fe5910462b47b1e7d4c42dbd230f46
languageName: node
linkType: hard
"is-unc-path@npm:^1.0.0":
version: 1.0.0
resolution: "is-unc-path@npm:1.0.0"
@ -2968,6 +3092,13 @@ __metadata:
languageName: node
linkType: hard
"kuler@npm:^2.0.0":
version: 2.0.0
resolution: "kuler@npm:2.0.0"
checksum: a3c55e149703e317718ded3d9eb3eaafd6ada006899feb0fabe076904a687f7b84af63532e372ce18b408fc96683b2b4eaa05f67fd93c588e752a486ff43a3ca
languageName: node
linkType: hard
"levn@npm:^0.4.1":
version: 0.4.1
resolution: "levn@npm:0.4.1"
@ -3018,6 +3149,20 @@ __metadata:
languageName: node
linkType: hard
"logform@npm:^2.3.2, logform@npm:^2.4.0":
version: 2.5.1
resolution: "logform@npm:2.5.1"
dependencies:
"@colors/colors": "npm:1.5.0"
"@types/triple-beam": "npm:^1.3.2"
fecha: "npm:^4.2.0"
ms: "npm:^2.1.1"
safe-stable-stringify: "npm:^2.3.1"
triple-beam: "npm:^1.3.0"
checksum: dbcb67e42fc45e7e5b15850492066fa11712993466a6fe1c456893cb212a61f1aa1ced5aa9e99382ec1f433939b27bf048ffa50b0a97778db9c0e0e809dce599
languageName: node
linkType: hard
"loose-envify@npm:^1.1.0":
version: 1.4.0
resolution: "loose-envify@npm:1.4.0"
@ -3291,7 +3436,7 @@ __metadata:
languageName: node
linkType: hard
"moment@npm:~2.29.3":
"moment@npm:^2.29.1, moment@npm:~2.29.3":
version: 2.29.4
resolution: "moment@npm:2.29.4"
checksum: d275537a30f155cae7f53f6e6d164ccb59b560935f3c6ed0f4a2e6f3503063b491ffa6b4f7e7207501f6fcab92a2d0bb78ed539cde33cadcb91df6b254f7328d
@ -3455,6 +3600,13 @@ __metadata:
languageName: node
linkType: hard
"object-hash@npm:^2.0.1":
version: 2.2.0
resolution: "object-hash@npm:2.2.0"
checksum: 40373e057e54ed3385e3097f36dd40922940932dc0c06a3f5540b82dff473333db28135162065863d075962cbc88068c4221b3b8459702fcddcecaaadb22b6ba
languageName: node
linkType: hard
"object-inspect@npm:^1.9.0":
version: 1.12.2
resolution: "object-inspect@npm:1.12.2"
@ -3480,6 +3632,15 @@ __metadata:
languageName: node
linkType: hard
"one-time@npm:^1.0.0":
version: 1.0.0
resolution: "one-time@npm:1.0.0"
dependencies:
fn.name: "npm:1.x.x"
checksum: 6edebb11434f4a7bb7cc86fee314b26e222c9a15f0e714c9cbbc3ed1fe096c50af8a7cf6d1c64ab8ba086cca7b360b738d96799a544caa28d6c174abc796480b
languageName: node
linkType: hard
"onetime@npm:^5.1.0":
version: 5.1.2
resolution: "onetime@npm:5.1.2"
@ -4095,6 +4256,13 @@ __metadata:
languageName: node
linkType: hard
"safe-stable-stringify@npm:^2.3.1":
version: 2.4.3
resolution: "safe-stable-stringify@npm:2.4.3"
checksum: a948b6699f0399445821754f73144dcc8c2e746eb972d9722b100c43f78e8fc38b21163d9429b3460f6b4f38caf4fb454f57cd9fb2a01568f7463607bd1f6d22
languageName: node
linkType: hard
"safer-buffer@npm:>= 2.1.2 < 3.0.0":
version: 2.1.2
resolution: "safer-buffer@npm:2.1.2"
@ -4238,6 +4406,15 @@ __metadata:
languageName: node
linkType: hard
"simple-swizzle@npm:^0.2.2":
version: 0.2.2
resolution: "simple-swizzle@npm:0.2.2"
dependencies:
is-arrayish: "npm:^0.3.1"
checksum: da2f0812cd395009bbe2fd2fe803300a63025f7f330c1492ea41e2b4a819138806a2a99c05ae1527cb750da43ff9dc2ccde294ad1e998cedbd459cb068dc68a3
languageName: node
linkType: hard
"sirv@npm:^2.0.2":
version: 2.0.2
resolution: "sirv@npm:2.0.2"
@ -4403,6 +4580,13 @@ __metadata:
languageName: node
linkType: hard
"stack-trace@npm:0.0.x":
version: 0.0.10
resolution: "stack-trace@npm:0.0.10"
checksum: f9a4244c4ba2523b79c8eee52c6dd6505289c7d13ae06664baa36b7482f5e6556564ac2d8442a604fc43a519c3217499572100a51956c0d5b521e05bbc9c4433
languageName: node
linkType: hard
"stackback@npm:0.0.2":
version: 0.0.2
resolution: "stackback@npm:0.0.2"
@ -4575,6 +4759,13 @@ __metadata:
languageName: node
linkType: hard
"text-hex@npm:1.0.x":
version: 1.0.0
resolution: "text-hex@npm:1.0.0"
checksum: e80d704a0ccc53d0ca2e4a74c1a8d0a3e5bb718dab9e3694042e00d60fab56b542e05442e28589a05ea8a2e1ea6c6b5cf7956a8176d982aa1b29f5d94e5f8edc
languageName: node
linkType: hard
"text-table@npm:^0.2.0":
version: 0.2.0
resolution: "text-table@npm:0.2.0"
@ -4656,6 +4847,13 @@ __metadata:
languageName: node
linkType: hard
"triple-beam@npm:^1.3.0":
version: 1.3.0
resolution: "triple-beam@npm:1.3.0"
checksum: 112538d46be29b6213ff13ce577867c4ab3cbc28b0b9094ed6687d1a1a79f79085861b1b3cff78b1083c293a05d1437c4138522d0fa2683ae54fbe5453971521
languageName: node
linkType: hard
"tslib@npm:^1.8.1, tslib@npm:^1.9.0":
version: 1.14.1
resolution: "tslib@npm:1.14.1"
@ -5022,6 +5220,50 @@ __metadata:
languageName: node
linkType: hard
"winston-daily-rotate-file@npm:4.7.1":
version: 4.7.1
resolution: "winston-daily-rotate-file@npm:4.7.1"
dependencies:
file-stream-rotator: "npm:^0.6.1"
object-hash: "npm:^2.0.1"
triple-beam: "npm:^1.3.0"
winston-transport: "npm:^4.4.0"
peerDependencies:
winston: ^3
checksum: 23ef922a69f9df3c644cf44ef213d38141f383abecbb797a1aa4d2c46c754cbefcdb0972c1d5a31f5c321931903ab53b64671727a2732b48d8419c9e1dc5ffd0
languageName: node
linkType: hard
"winston-transport@npm:^4.4.0, winston-transport@npm:^4.5.0":
version: 4.5.0
resolution: "winston-transport@npm:4.5.0"
dependencies:
logform: "npm:^2.3.2"
readable-stream: "npm:^3.6.0"
triple-beam: "npm:^1.3.0"
checksum: 7eadbadff21b747e8f1beea1bfc6bb9ed7e29e66e4b01875044b4e16438f7557ae41dde8a7316fb3568681101f7cb2d1adaad3e14e52b69e32799619640a8685
languageName: node
linkType: hard
"winston@npm:3.8.2":
version: 3.8.2
resolution: "winston@npm:3.8.2"
dependencies:
"@colors/colors": "npm:1.5.0"
"@dabh/diagnostics": "npm:^2.0.2"
async: "npm:^3.2.3"
is-stream: "npm:^2.0.0"
logform: "npm:^2.4.0"
one-time: "npm:^1.0.0"
readable-stream: "npm:^3.4.0"
safe-stable-stringify: "npm:^2.3.1"
stack-trace: "npm:0.0.x"
triple-beam: "npm:^1.3.0"
winston-transport: "npm:^4.5.0"
checksum: 50d7712f49ebb22317b9619334f2dca55f5760257e44915013fd2c060267064386347048e6ecf9f550dfcfba705dc94f58163b44eca72abff388037e792af524
languageName: node
linkType: hard
"word-wrap@npm:^1.2.3":
version: 1.2.3
resolution: "word-wrap@npm:1.2.3"