logan-logger 2.5.1 → 2.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,15 +1,44 @@
1
1
  import { LoggerConfig, LogLevel } from '../core/types';
2
2
  /**
3
- * Whether colored output is appropriate right now.
3
+ * Whether colored output is appropriate by default.
4
4
  *
5
5
  * `capabilities.colorSupport` answers whether the runtime *can* colorize.
6
6
  * This answers whether it *should*: writing ANSI escapes into a redirected
7
7
  * file or a log shipper is worse than writing none, so a non-TTY stdout
8
- * disables color unless the caller forces it. Honors the `NO_COLOR` and
9
- * `FORCE_COLOR` conventions.
10
- * @returns True when the level token should carry ANSI color
8
+ * disables color unless the caller forces it.
9
+ *
10
+ * This resolves the **default** only every source in SPEC §6.2 outranks it.
11
+ * `NO_COLOR` is deliberately not consulted here: §6.4.1 makes it an override
12
+ * that beats every configuration source, so checking it at this layer would
13
+ * let `LOG_COLOR=true` or an explicit `colorize: true` defeat it. It is applied
14
+ * after the merge instead — see `noColorRequested`, `applyNoColorOverride`, and
15
+ * `createTransports`, which applies it again to per-transport options.
16
+ * @returns True when the level token should carry ANSI color by default
11
17
  */
12
18
  export declare function shouldColorize(): boolean;
19
+ /**
20
+ * Whether the user has asked every program in this session not to emit color.
21
+ *
22
+ * SPEC §6.4.1. Presence is the entire signal and the value is meaningless, so
23
+ * `NO_COLOR=0` disables color exactly as `NO_COLOR=1` does. The value is read
24
+ * raw — neither trimmed nor case-folded — so a single space is a non-empty
25
+ * value and disables color.
26
+ *
27
+ * An empty string counts as **unset**, per no-color.org's own wording:
28
+ * "present and not an empty string". That is the exact opposite of the rule
29
+ * §6.3 imposes on `LOG_LEVEL`, `LOG_FORMAT`, `LOG_TIMESTAMP` and `LOG_COLOR`,
30
+ * where an empty value is *set*, matches nothing, and must warn — which is why
31
+ * `loadConfigFromEnvironment` below guards those with `!== undefined` rather
32
+ * than truthiness (#84).
33
+ *
34
+ * The two rules are opposite deliberately and **must not be reconciled**: §6.3
35
+ * governs variables in this library's namespace, where an empty value is a
36
+ * mistake worth reporting, while `NO_COLOR` is defined by a standard this
37
+ * library does not own and cannot revise. A truthiness test is correct here
38
+ * and a bug ninety lines further down.
39
+ * @returns True when `NO_COLOR` is set to a non-empty value
40
+ */
41
+ export declare function noColorRequested(): boolean;
13
42
  export declare function getDefaultConfig(): LoggerConfig;
14
43
  /**
15
44
  * Reset the warn-once state, config file warnings included. Exposed for tests;
@@ -31,7 +60,61 @@ export declare function tryParseLogLevel(level: string): LogLevel | undefined;
31
60
  *
32
61
  * In a browser these values exist only if the bundler inlined them at build
33
62
  * time; otherwise this returns an empty object.
63
+ *
64
+ * **This reads the environment; it does not apply SPEC §6.4.1's `NO_COLOR`
65
+ * veto.** Called on its own under `NO_COLOR=1 LOG_COLOR=true` it emits the
66
+ * disagreement diagnostic and still returns `{ colorize: true }`: the message
67
+ * describes what the *resolved* configuration will do, not what this function
68
+ * did. `applyNoColorOverride` is what actually forces `colorize` off, and
69
+ * `createTransports` enforces it per transport. A caller assembling a
70
+ * configuration by hand from this function must apply the former, or let
71
+ * `createLogger()` do both.
34
72
  * @returns The subset of configuration the environment specifies
35
73
  */
36
74
  export declare function loadConfigFromEnvironment(): Partial<LoggerConfig>;
37
75
  export declare function mergeConfigs(...configs: Partial<LoggerConfig>[]): LoggerConfig;
76
+ /**
77
+ * Apply SPEC §6.4.1's `NO_COLOR` override to an already-merged configuration.
78
+ *
79
+ * `NO_COLOR` is not another tier of §6.2's chain — it outranks all of it — so
80
+ * it cannot be expressed as one more argument to `mergeConfigs`, where anything
81
+ * later would beat it. That is precisely the bug this replaces: `shouldColorize()`
82
+ * consulted `NO_COLOR` at the defaults layer, so `LOG_COLOR=true` and an explicit
83
+ * `colorize: true` both silently defeated it.
84
+ *
85
+ * Precedence is the whole reason it is a separate step, and the only one. It is
86
+ * *not* that `mergeConfigs` must be kept clear of the environment: that function
87
+ * seeds itself with `getDefaultConfig()`, which calls `shouldColorize()`, which
88
+ * reads `FORCE_COLOR` and `process.stdout.isTTY`. It has always read the
89
+ * environment, and a purity argument here would be false.
90
+ *
91
+ * **`ignoreEnvironment` does not suppress this**, deliberately. That flag is
92
+ * documented as opting out of `LOG_LEVEL`, `LOG_FORMAT`, `LOG_TIMESTAMP` and
93
+ * `LOG_COLOR` by name, and it exists so a library is not hijacked by the *host
94
+ * application's* operational settings. `NO_COLOR` is a different kind of thing:
95
+ * the *end user's* preference, expressed to every program in their session. A
96
+ * library author who sets the flag for the documented reason must not silently
97
+ * also acquire "and ignore the user's `NO_COLOR`" — least of all because
98
+ * `ignoreEnvironment` is settable from a config file, which would let a file
99
+ * checked into a repository defeat every one of that project's users.
100
+ *
101
+ * **This is not the only place the veto is applied, and it must not be.** It
102
+ * rewrites the top-level `colorize` and nothing else, so a per-transport
103
+ * `options: { colorize: true }` sails straight past it — the console transport
104
+ * factory resolves `options.colorize ?? context.colorize`. `createTransports`
105
+ * therefore resolves `NO_COLOR` again when it builds each transport, which also
106
+ * covers a directly constructed `new NodeLogger(...)` that never reached this
107
+ * function. Between the two, no configuration source defeats `NO_COLOR`:
108
+ * neither `LOG_COLOR`, nor an explicit `colorize: true`, nor a config file, nor
109
+ * a per-transport option, nor `ignoreEnvironment`.
110
+ *
111
+ * What still does: bypassing configuration resolution entirely. `new
112
+ * BrowserLogger({ colorize: true })` reads `config.colorize` and consults no
113
+ * environment at all, so it honors neither `NO_COLOR` nor `LOG_COLOR` nor a
114
+ * config file. That is a property of constructing an adapter by hand rather
115
+ * than a hole in the veto, and `docs/configuration.md` says so where a reader
116
+ * meets `NO_COLOR`.
117
+ * @param config - The configuration after the whole precedence chain
118
+ * @returns The configuration with color forced off when `NO_COLOR` applies
119
+ */
120
+ export declare function applyNoColorOverride(config: LoggerConfig): LoggerConfig;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "logan-logger",
3
- "version": "2.5.1",
3
+ "version": "2.5.2",
4
4
  "packageManager": "pnpm@11.9.0",
5
5
  "description": "Universal TypeScript logging library for all JavaScript runtimes",
6
6
  "main": "dist/index.cjs",
@@ -1 +0,0 @@
1
- const e=require("./config-BoVokyIX.cjs");var t=new Map;function n(e,n){t.set(e,n)}function r(e){return t.get(e)}var i=class{constructor(e={}){this.type=`console`,this.level=e.level,this.format=e.format===`json`?`json`:`text`,this.formatOptions={timestamp:e.timestamp!==!1,colorize:e.colorize===!0}}write(t){let n=e.l(t,this.format,this.formatOptions);switch(t.level){case e._.DEBUG:console.debug(n);break;case e._.WARN:console.warn(n);break;case e._.ERROR:console.error(n);break;default:console.info(n)}}};n(`console`,(e,t)=>{let n=e.options??{};return new i({format:n.format??t.format,timestamp:n.timestamp??t.timestamp,colorize:n.colorize??t.colorize,level:e.level})}),n(`custom`,(e,t)=>{let n=e.options?.transport,r=typeof n==`function`?n(t):n;if(!r||typeof r.write!=`function`)throw Error(`custom transport requires options.transport to be an object with a write(entry) method, or a function returning one; ${typeof n==`function`?`the function it was given returned no write(entry) method`:`it was given no object with a write(entry) method`}`);return e.level===void 0?r:{type:r.type??`custom`,level:e.level,write:e=>r.write(e),close:r.close?()=>r.close?.():void 0}});function a(e){let t={format:e.format??`text`,timestamp:e.timestamp??!0,colorize:e.colorize??!1};if(e.transports===void 0)return[new i({format:t.format,timestamp:t.timestamp,colorize:t.colorize})];let n=[];for(let i of e.transports){let e=r(i.type);if(!e){console.warn(`[logan-logger] ${o(i.type)}`);continue}try{n.push(e(i,t))}catch(e){console.warn(`[logan-logger] transport '${i.type}' failed to initialize:`,e)}}return n}function o(e){return e===`file`?`the 'file' transport is not registered; import from 'logan-logger/node' (or 'logan-logger/bun') rather than the main entry point to use file logging`:e===`http`?`the 'http' transport is not built in; supply one with { type: 'custom', options: { transport } }`:`unknown transport type '${e}'; skipping`}var s=class t extends e.g{constructor(e={},t){super(e),this.transports=t??a(e)}getTransports(){return this.transports}close(){for(let e of this.transports)e.close?.()}writeLog(e){for(let t of this.transports)if(!(t.level!==void 0&&e.level<t.level))try{t.write(e)}catch(e){console.warn(`[logan-logger] transport '${t.type}' failed to write:`,e)}}createChild(){return new t(this.config,this.transports)}};function c(e){return{write:t=>{e.info(t.trim())}}}var l=class t{static create(n={}){let r=e.v(),i=t.mergeConfig(n);switch(r.name){case`node`:return new s(i);case`deno`:return new e.u(i);case`bun`:return new s(i);case`browser`:case`webworker`:return new e.u(i);default:return new e.u(i)}}static createChild(e,t){return e.child(t)}static mergeConfig(t){let n=t.ignoreEnvironment?{}:e.n();return e.r(t,n)}};function u(e){return l.create(e)}function d(){let t=f();return u({level:p(t),colorize:t!==`production`&&e.a(),timestamp:!0,format:t===`production`?`json`:`text`})}function f(){return typeof process<`u`&&process.env?process.env.NODE_ENV||process.env.NEXT_PUBLIC_APP_ENV||process.env.ENVIRONMENT||`development`:typeof window<`u`&&globalThis.__ENV__||`development`}function p(t){switch(t){case`production`:return e._.ERROR;case`staging`:case`test`:return e._.WARN;case`development`:case`dev`:return e._.DEBUG;default:return e._.INFO}}function m(t){return e.o(t)??e._.INFO}function h(t){switch(t){case e._.DEBUG:return`debug`;case e._.INFO:return`info`;case e._.WARN:return`warn`;case e._.ERROR:return`error`;case e._.SILENT:return`silent`;default:return`info`}}var g=[{path:`logan.config.json`},{path:`.loganrc`},{path:`package.json`,key:`logan`}],_=new Set([`level`,`format`,`timestamp`,`colorize`,`ignoreEnvironment`,`metadata`,`transports`,`$schema`]),v=new Set([`EACCES`,`EPERM`,`NotCapable`,`PermissionDenied`]);function y(e){return typeof e==`object`&&!!e&&!Array.isArray(e)}function b(e){return e instanceof Error?e.message:String(e)}function x(e){let t=/at position \d+(?: \(line \d+ column \d+\))?/.exec(b(e));return t?`not valid JSON (${t[0]})`:`not valid JSON`}function S(e){return e.startsWith(`/`)||e.startsWith(`\\`)||/^[A-Za-z]:/.test(e)}function C(e,t){return t===void 0||t===``||S(e)?e:`${t.replace(/[\\/]+$/,``)}/${e}`}async function w(e,t){if(e===`deno`)return e=>C(e,t);let{resolve:n}=await import(`node:path`);return t===void 0?e=>n(e):e=>n(t,e)}function T(e){let t=e.split(/[\\/]/);return t[t.length-1]||e}function E(e){if(e===`deno`)try{return globalThis.Deno?.cwd?.()}catch{return}return typeof process<`u`&&typeof process.cwd==`function`?process.cwd():void 0}function D(e){let t=e?.code??``,n=e?.name??``;return t===`ENOENT`||n===`NotFound`?{kind:`absent`}:v.has(t)||v.has(n)?{kind:`denied`,reason:b(e)}:{kind:`unreadable`,reason:b(e)}}async function O(e,t){let n=t===`deno`?()=>globalThis.Deno.readTextFile(e):async()=>(await import(`node:fs/promises`)).readFile(e,`utf-8`);try{return{kind:`content`,content:await n()}}catch(e){return D(e)}}function k(t){if(typeof t==`string`)return e.o(t);if(typeof t==`number`&&e._[t]!==void 0)return t}function A(t,n,r){let i=t=>{e.s(`[logan-logger] ${r}: ignoring transports[${n}]: ${t}.`)};if(!y(t)){i(`expected an object, got ${JSON.stringify(t)??typeof t}`);return}if(typeof t.type!=`string`){i(`"type" is required and must be a string`);return}if(t.options!==void 0&&!y(t.options)){i(`"options" must be an object`);return}let a={type:t.type,options:t.options??{}};if(t.level!==void 0){let i=k(t.level);i===void 0?e.s(`[logan-logger] ${r}: ignoring transports[${n}].level=${JSON.stringify(t.level)}. Accepted: debug, info, warn, error, silent.`):a.level=i}return a}function j(t,n){let r={},i=(t,r,i)=>{e.s(`[logan-logger] ${n}: ignoring ${t}=${JSON.stringify(r)}. Accepted: ${i}.`)};for(let r of Object.keys(t))_.has(r)||e.s(`[logan-logger] ${n}: ignoring unknown field "${r}". Known fields: level, format, timestamp, colorize, ignoreEnvironment, metadata, transports.`);if(t.level!==void 0){let e=k(t.level);e===void 0?i(`level`,t.level,`debug, info, warn, error, silent`):r.level=e}t.format!==void 0&&(t.format===`json`||t.format===`text`||t.format===`custom`?r.format=t.format:i(`format`,t.format,`json, text, custom`));for(let e of[`timestamp`,`colorize`,`ignoreEnvironment`])t[e]!==void 0&&(typeof t[e]==`boolean`?r[e]=t[e]:i(e,t[e],`true, false`));if(t.metadata!==void 0&&(y(t.metadata)?r.metadata=t.metadata:i(`metadata`,t.metadata,`an object`)),Array.isArray(t.transports)){let e=t.transports.map((e,t)=>A(e,t,n)).filter(e=>e!==void 0);(e.length>0||t.transports.length===0)&&(r.transports=e)}else t.transports!==void 0&&i(`transports`,t.transports,`an array of transport configs`);return r}async function M(e,t){let n=await O(e.path,t);if(n.kind===`absent`)return n;if(n.kind!==`content`)return{kind:n.kind,path:e.path,reason:n.reason};let r;try{r=JSON.parse(n.content)}catch(t){return{kind:`invalid`,path:e.path,reason:x(t)}}let i=r;if(e.key){if(!y(r))return{kind:`invalid`,path:e.path,reason:`expected a JSON object`};if(!Object.hasOwn(r,e.key))return{kind:`keyless`,path:e.path,key:e.key};i=r[e.key]}if(!y(i)){let t=e.key?`the "${e.key}" key`:`the file`;return{kind:`invalid`,path:e.path,reason:`expected ${t} to be an object`}}return{kind:`found`,config:j(i,e.path)}}function N(e){let t=g.find(t=>t.path===T(e));return t?{...t,path:e}:{path:e}}function P(t,n){e.s(`[logan-logger] cannot read config at ${t} — no permission; using defaults instead: ${n}`)}async function F(t,n={}){let r=e.v();if(!r.capabilities.fileSystem)return{};let i=n.cwd||E(r.name),a=await w(r.name,i);if(t!==void 0){let e=a(t),n=await M(N(e),r.name);if(n.kind===`found`)return n.config;if(n.kind===`absent`)throw Error(`[logan-logger] config file not found: ${e}`);if(n.kind===`keyless`)throw Error(`[logan-logger] no "${n.key}" key in ${n.path}`);if(n.kind===`denied`)return P(n.path,n.reason),{};throw n.kind===`unreadable`?Error(`[logan-logger] config at ${n.path} is not a readable file: ${n.reason}`):Error(`[logan-logger] config at ${n.path} is invalid: ${n.reason}`)}for(let t of g){let n=await M({...t,path:a(t.path)},r.name);if(n.kind===`found`)return n.config;if(n.kind===`invalid`)return e.s(`[logan-logger] config at ${n.path} is invalid: ${n.reason}`),{};if(n.kind===`denied`)return P(n.path,n.reason),{};n.kind===`unreadable`&&e.s(`[logan-logger] skipping ${n.path}: not a readable file (${n.reason})`)}return{}}Object.defineProperty(exports,"a",{enumerable:!0,get:function(){return h}}),Object.defineProperty(exports,"c",{enumerable:!0,get:function(){return c}}),Object.defineProperty(exports,"d",{enumerable:!0,get:function(){return r}}),Object.defineProperty(exports,"f",{enumerable:!0,get:function(){return n}}),Object.defineProperty(exports,"i",{enumerable:!0,get:function(){return d}}),Object.defineProperty(exports,"l",{enumerable:!0,get:function(){return i}}),Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return l}}),Object.defineProperty(exports,"o",{enumerable:!0,get:function(){return m}}),Object.defineProperty(exports,"r",{enumerable:!0,get:function(){return u}}),Object.defineProperty(exports,"s",{enumerable:!0,get:function(){return s}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return F}}),Object.defineProperty(exports,"u",{enumerable:!0,get:function(){return a}});
@@ -1 +0,0 @@
1
- const e=require("./config-BoVokyIX.cjs"),t=require("./config-file-C_hoT1V-.cjs");let n=require("node:fs"),r=require("node:path");var i=5242880,a=5,o=class{constructor(e){if(this.type=`file`,this.size=0,this.warned=!1,!e?.filename)throw Error(`file transport requires options.filename`);this.path=(0,r.resolve)(e.filename),this.maxsize=e.maxsize??i,this.maxFiles=e.maxFiles??a,this.format=e.format===`text`?`text`:`json`,this.timestamp=e.timestamp!==!1,this.level=e.level}get filename(){return this.path}write(t){let r=`${e.l(t,this.format,{timestamp:this.timestamp,colorize:!1})}\n`;try{this.open(),this.maxsize>0&&this.size>=this.maxsize&&(this.rotate(),this.open()),this.size+=(0,n.writeSync)(this.fd,r)}catch(e){this.warnOnce(e)}}close(){if(this.fd!==void 0)try{(0,n.closeSync)(this.fd)}finally{this.fd=void 0}}open(){if(this.fd!==void 0)return;let e=(0,r.dirname)(this.path);try{(0,n.mkdirSync)(e,{recursive:!0})}catch(t){throw s(`create log directory`,e,t)}try{this.fd=(0,n.openSync)(this.path,`a`)}catch(e){throw s(`open log file`,this.path,e)}this.size=(0,n.fstatSync)(this.fd).size}rotate(){if(this.close(),this.maxFiles<=0){(0,n.existsSync)(this.path)&&(0,n.unlinkSync)(this.path),this.size=0;return}let e=`${this.path}.${this.maxFiles}`;(0,n.existsSync)(e)&&(0,n.unlinkSync)(e);for(let e=this.maxFiles-1;e>=1;e--){let t=`${this.path}.${e}`;(0,n.existsSync)(t)&&(0,n.renameSync)(t,`${this.path}.${e+1}`)}(0,n.existsSync)(this.path)&&(0,n.renameSync)(this.path,`${this.path}.1`),this.size=0}warnOnce(e){this.close(),!this.warned&&(this.warned=!0,console.warn(`[logan-logger] file transport for '${this.path}' failed and will keep retrying quietly:`,e instanceof Error?e.message:e))}};function s(e,t,n){let r=n,i=r?.code?` [${r.code}]`:``,a=r?.syscall?` during ${r.syscall}`:``,o=Error(`could not ${e} '${t}'${i}${a}: ${r?.message??n}`);return o.cause=n,o}t.f(`file`,(e,t)=>{let n=e.options??{};return new o({filename:n.filename,maxsize:n.maxsize,maxFiles:n.maxFiles,format:n.format,timestamp:n.timestamp??t.timestamp,level:e.level})}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return o}});