@stonyx/logs 1.0.1-beta.2 → 1.0.1-beta.21

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.
package/README.md CHANGED
@@ -1,7 +1,11 @@
1
+ [![CI](https://github.com/abofs/stonyx-logs/actions/workflows/ci.yml/badge.svg)](https://github.com/abofs/stonyx-logs/actions/workflows/ci.yml)
2
+ [![npm version](https://img.shields.io/npm/v/@stonyx/logs.svg)](https://www.npmjs.com/package/@stonyx/logs)
3
+ [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)
4
+
1
5
  <h1 align="center">
2
6
  <br>
3
7
  <br>
4
- <img width="560" src="https://github.com/abofs/stonyx-logs/raw/master/media/logo.png" alt="Chronicle">
8
+ <img width="560" src="https://github.com/abofs/stonyx-logs/raw/main/media/logo.png" alt="Stonyx Logs">
5
9
  <br>
6
10
  <br>
7
11
  <br>
@@ -9,7 +13,7 @@
9
13
 
10
14
  > Simplified logging for node applications
11
15
 
12
- ![](https://github.com/abofs/chronicle/raw/master/media/screenshot.jpg)
16
+ ![](https://github.com/abofs/stonyx-logs/raw/main/media/screenshot.jpg)
13
17
 
14
18
  <br>
15
19
 
@@ -27,10 +31,10 @@
27
31
 
28
32
  ---
29
33
 
30
- **Chronicle** is built on top of all the great work done by "Sindre Sorhus" and other collaborators of the [chalk](https://www.npmjs.com/package/chalk) project.
31
- This project is not directly associated with chalk other than chalk being a core dependency of **Chronicle**.
34
+ **Log** is built on top of all the great work done by "Sindre Sorhus" and other collaborators of the [chalk](https://www.npmjs.com/package/chalk) project.
35
+ This project is not directly associated with chalk other than chalk being a core dependency of **Log**.
32
36
 
33
- **IMPORTANT**: Please note that although **Chronicle** can be configured to any color through chalk, your output is subject to your terminal's color limitations.
37
+ **IMPORTANT**: Please note that although **Log** can be configured to any color through chalk, your output is subject to your terminal's color limitations.
34
38
 
35
39
  ## Highlights
36
40
 
@@ -41,27 +45,27 @@ This project is not directly associated with chalk other than chalk being a core
41
45
  ## Install
42
46
 
43
47
  ```sh
44
- npm install node-chronicle
48
+ npm install @stonyx/logs
45
49
  ```
46
50
 
47
51
  ## Usage
48
52
 
49
53
  ```js
50
- import Chronicle from 'node-chronicle';
54
+ import Log from '@stonyx/logs';
51
55
 
52
- const chronicle = new Chronicle();
56
+ const log = new Log();
53
57
 
54
- chronicle.info('Info: sample application has started');
55
- chronicle.warn('Warning: this is just a sample');
56
- chronicle.error('Error: no application logic detected', true); // logs to logs/error.log file
58
+ log.info('Info: sample application has started');
59
+ log.warn('Warning: this is just a sample');
60
+ log.error('Error: no application logic detected', true); // logs to logs/error.log file
57
61
  ```
58
62
 
59
63
  Easily define your own logging mechanism and color-coding preference:
60
64
 
61
65
  ```js
62
- import Chronicle from 'node-chronicle';
66
+ import Log from '@stonyx/logs';
63
67
 
64
- const chronicle = new Chronicle({
68
+ const log = new Log({
65
69
  systemLogs: {
66
70
  blue: '#007cae', // indigo blue
67
71
  yellow: '#ae8f00', // bright orange
@@ -69,17 +73,17 @@ const chronicle = new Chronicle({
69
73
  },
70
74
  });
71
75
 
72
- chronicle.blue('Info: using custom method blue, sample application has started');
73
- chronicle.yellow('Warning: using custom method yellow, this is just a sample');
74
- chronicle.red('Error: using custom method red, no application logic detected', false);
76
+ log.blue('Info: using custom method blue, sample application has started');
77
+ log.yellow('Warning: using custom method yellow, this is just a sample');
78
+ log.red('Error: using custom method red, no application logic detected', false);
75
79
  ```
76
80
 
77
81
  Customize logging options to best suit your project
78
82
 
79
83
  ```js
80
- import Chronicle from 'node-chronicle';
84
+ import Log from '@stonyx/logs';
81
85
 
82
- const chronicle = new Chronicle({
86
+ const log = new Log({
83
87
  logToFileByDefault: true,
84
88
  logTimestamp: true,
85
89
  path: 'custom-logs', // <project root>/custom-logs/*.log
@@ -87,31 +91,31 @@ const chronicle = new Chronicle({
87
91
  suffix: '\n=============================================================== \n',
88
92
  });
89
93
 
90
- chronicle.info('Info: sample application has started');
94
+ log.info('Info: sample application has started');
91
95
  ```
92
- ![](https://github.com/abofs/chronicle/raw/master/media/examples/custom-options.jpg)
96
+ ![](https://github.com/abofs/stonyx-logs/raw/main/media/examples/custom-options.jpg)
93
97
 
94
98
 
95
99
  Add additional log types extending the default options of "info", "warn", "error" and "debug"
96
100
 
97
101
  ```js
98
- import Chronicle from '../source/index.js';
102
+ import Log from '@stonyx/logs';
99
103
 
100
- const chronicle = new Chronicle({ additionalLogs: { question: 'green' } });
104
+ const log = new Log({ additionalLogs: { question: 'green' } });
101
105
 
102
106
  // create additional log with direct chalk configuration
103
- chronicle.defineType('query', chronicle.chalk().black.bgGreen);
107
+ log.defineType('query', log.chalk().black.bgGreen);
104
108
 
105
- chronicle.question('What will a fully custom chalk color function look like?');
106
- await chronicle.query('This is what a custom chalk color setting looks like', true);
109
+ log.question('What will a fully custom chalk color function look like?');
110
+ await log.query('This is what a custom chalk color setting looks like', true);
107
111
  ```
108
- ![](https://github.com/abofs/chronicle/raw/master/media/examples/additional-logs.jpg)
112
+ ![](https://github.com/abofs/stonyx-logs/raw/main/media/examples/additional-logs.jpg)
109
113
 
110
114
  ## API
111
115
 
112
116
  ### Defining Logs & Colors
113
117
 
114
- By default, **Chronicle** is instantiated with the following options:
118
+ By default, **Log** is instantiated with the following options:
115
119
 
116
120
  ```js
117
121
  additionalLogs: {},
@@ -122,10 +126,10 @@ By default, **Chronicle** is instantiated with the following options:
122
126
  },
123
127
  ```
124
128
 
125
- You can add to a new log/color setting by passing the `additionalLogs` option to the **Chronicle** constructor. Any setting that already exists in `systemLogs` will be replaced, otherwise they will be added.
129
+ You can add to a new log/color setting by passing the `additionalLogs` option to the **Log** constructor. Any setting that already exists in `systemLogs` will be replaced, otherwise they will be added.
126
130
 
127
131
  ```js
128
- const chronicle = new Chronicle({ additionalLogs: { info: 'green', custom: 'cyan' } });
132
+ const log = new Log({ additionalLogs: { info: 'green', custom: 'cyan' } });
129
133
 
130
134
  // output configuration:
131
135
  {
@@ -136,32 +140,81 @@ You can add to a new log/color setting by passing the `additionalLogs` option to
136
140
  }
137
141
  ```
138
142
 
139
- **Chronicle** will generate convenience methods for all keys provided, with the corresponding color settings. The example above would create the following convenience methods, for logging:
143
+ **Log** will generate convenience methods for all keys provided, with the corresponding color settings. The example above would create the following convenience methods, for logging:
140
144
 
141
145
  ```js
142
- chronicle.info() // green output
143
- chronicle.warn() // yellow output
144
- chronicle.error() // red output
145
- chronicle.custom() // cyan output
146
+ log.info() // green output
147
+ log.warn() // yellow output
148
+ log.error() // red output
149
+ log.custom() // cyan output
146
150
  ```
147
151
 
148
152
  These methods can then be called in your application with [logging parameters](#logging-parameters).
149
153
 
150
154
  Color settings are handled by determining whether your input is a color name or a hex value (prefixed with **#**). For example, passing `red` as a color setting will utilize `chalk.red`, while passing `#ff0000` would use `chalk.hex('#ff0000')` instead. A [list of available colors](https://github.com/chalk/chalk#colors) can be found in chalks' documentation.
151
155
 
152
- Additionally, these methods return a promise when `logToFile` is true, allowing you use them with `await` in an async method, or append `then(), catch(), or finally()` for more advanced callback usage.
156
+ Additionally, these methods return a promise when `logToFile` is true. That promise rejecting is the only signal that a write failed, so you must either `await` the call or attach a `.catch()` — see [File Write Failures](#file-write-failures). `then()` and `finally()` are also available.
153
157
 
154
158
  ```js
155
159
  async method() {
156
- await chronicle.error('error message', true);
160
+ try {
161
+ await log.error('error message', true);
157
162
 
158
- // do something after logs/error.log (default) is created
163
+ // do something after logs/error.log (default) is created
164
+ } catch (err) {
165
+ // the write failed; the rejection is the only notice you get
166
+ process.stderr.write(`log write failed: ${err.code}\n`);
167
+ }
159
168
  }
160
169
  ```
161
170
 
171
+ #### File Write Failures
172
+
173
+ When `logToFile` is true, **the returned promise rejecting is the only failure signal.** The log
174
+ line itself is still written to the console as usual, but **no error notice is printed** and no
175
+ fallback log is written when the log directory or file cannot be written: the underlying `fs` error
176
+ is propagated to the caller with its `code` intact (`ENOENT`, `ENOTDIR`, `EACCES`, `EPERM`,
177
+ `EROFS`, ...).
178
+
179
+ A fire-and-forget call therefore produces an **unhandled promise rejection** on a failed write.
180
+ Always `await` the call (or attach a `.catch()`) anywhere file logging is enabled:
181
+
182
+ ```js
183
+ // unhandled rejection if the log directory is not writable
184
+ log.error('error message', true);
185
+
186
+ // handled
187
+ log.error('error message', true).catch(err => process.stderr.write(`log write failed: ${err.code}\n`));
188
+ ```
189
+
190
+ A failed write is retried exactly once, and only for the two codes that recreating the log directory
191
+ can repair: `ENOENT` (the directory was removed at runtime) and `ENOTDIR` (a path component was
192
+ replaced by a non-directory). The directory cache entry is dropped, the directory is recreated, and
193
+ the write is reattempted once before the rejection surfaces. If the recreate itself fails, that
194
+ error is what surfaces.
195
+
196
+ Every other code — including `EACCES`, `EPERM` and `EROFS` — rejects immediately with no retry,
197
+ because `mkdir` on an existing directory is a successful no-op: it cannot change a permission bit or
198
+ a read-only mount, so a retry could only ever repeat the same failure at twice the syscall cost.
199
+
200
+ ##### The directory cache
201
+
202
+ To keep the write path free of repeated `mkdir` calls, each `Log` instance exposes a
203
+ `directoryCache` field: a `Map` keyed on the resolved directory (not on the target filename), whose
204
+ values are the in-flight or settled `mkdir` promises. It is **instance-scoped, not module-scoped** —
205
+ two `Log` instances pointing at the same directory each call `mkdir` once. Entries are **never
206
+ evicted on success**, so a directory is created at most once per instance for the process lifetime;
207
+ entries are dropped only when the `mkdir` rejects, or when a write fails with a retryable code and
208
+ the directory is recreated.
209
+
210
+ It is public only because the class carries an index signature, and it is not part of the supported
211
+ API: treat it as read-only, since mutating it corrupts the write path. Note also that the name is
212
+ reserved — a log type called `directoryCache` is silently skipped rather than overwriting the cache,
213
+ so no convenience method is generated for it.
214
+
162
215
  ### The Debug Method
163
216
 
164
- **Chronicle** allows for the `chronicle.debug()` method to be overridden by a color setting. However, by default we do not define a color for debug and debug is handled differently. For console logging, all **debug** does is output the following:
217
+ **Log** allows for the `log.debug()` method to be overridden by a color setting. However, by default we do not define a color for debug and debug is handled differently. For console logging, all **debug** does is output the following:
165
218
 
166
219
  ```js
167
220
  // For logging to console:
@@ -171,12 +224,12 @@ console.dir(content);
171
224
  JSON.stringify(content, null, 2);
172
225
  ```
173
226
 
174
- We believe that when wanting to output complicated objects or debug **typescript** applications, there are better methods than utilizing this **Chronicle** package. But for anyone who's fully incorporated **Chronicle** into their project, this function offers some convenience.
227
+ We believe that when wanting to output complicated objects or debug **typescript** applications, there are better methods than utilizing this **Log** package. But for anyone who's fully incorporated **Log** into their project, this function offers some convenience.
175
228
 
176
229
  ### Logging Parameters
177
230
 
178
231
  ```js
179
- chronicle.error('error message', true, false); // content, logToFile, overwrite
232
+ log.error('error message', true, false); // content, logToFile, overwrite
180
233
  ```
181
234
 
182
235
  | Parameter | Type | Default | Description |
@@ -188,15 +241,16 @@ chronicle.error('error message', true, false); // content, logToFile, overwrite
188
241
  **logToFile** will log to *<project-root>/logs* unless [configured](#configuration) differently during instantiation. <br>
189
242
  ### Configuration
190
243
 
191
- When instantiating **Chronicle**, you can pass an object to customize your settings. Below is the default configuration:
244
+ When instantiating **Log**, you can pass an object to customize your settings. Below is the default configuration:
192
245
 
193
246
  ```js
194
- const chronicle = new Chronicle({
247
+ const log = new Log({
195
248
  logToFileByDefault: false,
196
249
  logTimestamp: false,
197
250
  path: 'logs/',
198
251
  prefix: '',
199
252
  suffix: '',
253
+ filename: '',
200
254
  additionalLogs: {},
201
255
  systemLogs: {
202
256
  info: 'cyan',
@@ -213,27 +267,28 @@ const chronicle = new Chronicle({
213
267
  | `path` | **String** | *'logs/'* | Path in which to store log files. This setting is relative to your project's root directory. |
214
268
  | `prefix` | **String** | *''* | Prefix string to prepend all log messages for all log types with the exception of *debug*. |
215
269
  | `suffix` | **String** | *''* | Suffix string to tack on to all log messages for all log types with the exception of *debug*. |
270
+ | `filename` | **String** | *''* | Template for log file names with variable support. Defaults to `{type}.log` when empty. See [dynamic file names](#dynamic-file-names). |
216
271
  | `additionalLogs` | **Object** | | Key value pair object containing log type to color setting for logs that will be merged with `systemLogs` |
217
- | `systemLogs` | **Object** | | Key value pair object containing log type to color setting for main **Chronicle** logs available in application |
272
+ | `systemLogs` | **Object** | | Key value pair object containing log type to color setting for main **Log** logs available in application |
218
273
 
219
274
  `additionalLogs` and `systemLogs` are explained with more detail in the [defining logs and colors](#defining-logs) section.
220
275
 
221
276
  ### Advanced Configuration
222
277
 
223
- You may want to do more than just pick a basic color for your output. **chalk** offers a variety of different options, and can be configured via `defineType()`. **Chronicle** exposes the chalk instance via `chalk()` so that you don't have to import **chalk** directly into your project. Here is an example of how you can use this method to fully customize your log color setting:
278
+ You may want to do more than just pick a basic color for your output. **chalk** offers a variety of different options, and can be configured via `defineType()`. **Log** exposes the chalk instance via `chalk()` so that you don't have to import **chalk** directly into your project. Here is an example of how you can use this method to fully customize your log color setting:
224
279
 
225
280
  ```js
226
- const chronicle = new Chronicle();
281
+ const log = new Log();
227
282
 
228
- chronicle.defineType('critical', chronicle.chalk().bold.red);
229
- chronicle.critical('This is a critical error');
283
+ log.defineType('critical', log.chalk().bold.red);
284
+ log.critical('This is a critical error');
230
285
  ```
231
286
 
232
287
  Additionally, any [configuration](#configuration) that can be set during instantiation, can also be applied exclusively to any given type by passing in a third **options** parameter.
233
288
 
234
289
  ```js
235
290
  // params: type, setting, options
236
- chronicle.definetype('notice', '#c0c0c0', {
291
+ log.definetype('notice', '#c0c0c0', {
237
292
  prefix: '--------------------------------------------------------------- \n',
238
293
  suffix: '\n=============================================================== \n'
239
294
  });
@@ -248,29 +303,62 @@ chronicle.definetype('notice', '#c0c0c0', {
248
303
 
249
304
 
250
305
  ```js
251
- const chronicle = new Chronicle();
306
+ const log = new Log();
252
307
 
253
- chronicle.defineType('info', chronicle.chalk().black.bgCyan);
254
- chronicle.defineType('critical', chronicle.chalk().bold.red);
255
- chronicle.defineType('dialog', 'magentaBright');
256
- chronicle.definetype('notice', '#c0c0c0', {
308
+ log.defineType('info', log.chalk().black.bgCyan);
309
+ log.defineType('critical', log.chalk().bold.red);
310
+ log.defineType('dialog', 'magentaBright');
311
+ log.definetype('notice', '#c0c0c0', {
257
312
  prefix: '--------------------------------------------------------------- \n',
258
313
  suffix: '\n=============================================================== \n'
259
314
  });
260
315
 
261
- chronicle.info('This pre-existing log now has a cyan background and black foreground');
262
- chronicle.critical('This new log is bold and red');
263
- chronicle.dialog('This new dialog is bright magenta');
264
- chronicle.notice('This new log is the hex "#c0c0c0" share of gray');
316
+ log.info('This pre-existing log now has a cyan background and black foreground');
317
+ log.critical('This new log is bold and red');
318
+ log.dialog('This new dialog is bright magenta');
319
+ log.notice('This new log is the hex "#c0c0c0" share of gray');
265
320
  ```
266
321
 
267
322
  `defineType()` can also be used as an alternative to populating the `additionalLogs` setting in the constructor, as if the setting doesn't already exist, it will then be created.
268
323
 
324
+ ### Dynamic File Names
325
+
326
+ The `filename` option supports template variables that are resolved at write-time, allowing each log type to produce uniquely named files.
327
+
328
+ #### Supported Variables
329
+
330
+ | Variable | Resolves To | Example Output |
331
+ | :---: | :--- | :--- |
332
+ | `{date}` | Current date in YYYY-MM-DD format | `2026-04-04` |
333
+ | `{type}` | Log type name | `error` |
334
+ | `{pid}` | Current process ID | `12345` |
335
+ | `{hostname}` | Machine hostname | `my-server` |
336
+
337
+ #### Examples
338
+
339
+ ```js
340
+ // Per-type filename via defineType
341
+ log.defineType('error', 'red', { filename: 'error-{date}.log' });
342
+ // writes to: logs/error-2026-04-04.log
343
+
344
+ // Per-type filename with multiple variables
345
+ log.defineType('info', 'cyan', { filename: '{type}-{hostname}-{date}.log' });
346
+ // writes to: logs/info-my-server-2026-04-04.log
347
+
348
+ // Global filename template via constructor
349
+ const log = new Log({ filename: '{type}-{date}.log' });
350
+ // all types write to: logs/<type>-2026-04-04.log
351
+ ```
352
+
353
+ When no `filename` is configured, the default behavior of `{type}.log` is preserved for full backward compatibility.
354
+
355
+ Path traversal characters (`..`, `/`, `\`) are automatically stripped from resolved file names for security.
356
+
269
357
  ## Origin
270
358
 
271
- As a team of developers who are constantly working on side projects, we often litter our codebase with TODOs to refactor convenience utils such as **chronicle** into classes of their own, or projects of their own. This usually turns into internal tech debt that never gets addressed. Furthermore, we also often find ourselves going the *copy -> paste -> modify* route of previously written useful logic, which saves us time in new projects, but not as much as it would if all we had to do was run an `npm install` instead.
359
+ As a team of developers who are constantly working on side projects, we often litter our codebase with TODOs to refactor convenience utils such as **@stonyx/logs** into classes of their own, or projects of their own. This usually turns into internal tech debt that never gets addressed. Furthermore, we also often find ourselves going the *copy -> paste -> modify* route of previously written useful logic, which saves us time in new projects, but not as much as it would if all we had to do was run an `npm install` instead.
272
360
 
273
- With that in mind, we are proud to release **chronicle** as an open source package, in hopes others will find this just as useful as we do in their own projects.
361
+ With that in mind, we are proud to release **@stonyx/logs** as an open source package, in hopes others will find this just as useful as we do in their own projects.
274
362
 
275
363
  ## Maintainers
276
364
 
@@ -0,0 +1,10 @@
1
+ import chalk from 'chalk';
2
+ export type ChalkColorFn = (text: string) => string;
3
+ export type ColorSetting = string | ChalkColorFn;
4
+ export default class Color {
5
+ types: Record<string, ChalkColorFn>;
6
+ getLogColor(type: string): ChalkColorFn;
7
+ getChalkInstance(): typeof chalk;
8
+ setLogColor(type: string, setting: ColorSetting): void;
9
+ settingToChalkColorFunction(setting: ColorSetting): ChalkColorFn;
10
+ }
package/dist/color.js ADDED
@@ -0,0 +1,39 @@
1
+ import chalk from 'chalk';
2
+ export default class Color {
3
+ types = {};
4
+ getLogColor(type) {
5
+ return this.types[type];
6
+ }
7
+ getChalkInstance() {
8
+ return chalk;
9
+ }
10
+ setLogColor(type, setting) {
11
+ const chalkColorFunction = this.settingToChalkColorFunction(setting);
12
+ this.types[type] = chalkColorFunction;
13
+ }
14
+ // retrieves chalk color function, and fully validates output
15
+ settingToChalkColorFunction(setting) {
16
+ const errorMessage = 'Invalid chalk color function. '
17
+ + 'For help with color settings, see https://github.com/abofs/stonyx-logs#defining-logs--colors';
18
+ switch (typeof setting) {
19
+ case 'string':
20
+ const chalkColorFunction = (setting[0] === '#')
21
+ ? chalk.hex(setting)
22
+ : chalk[setting];
23
+ if (!chalkColorFunction
24
+ || typeof chalkColorFunction !== 'function'
25
+ || typeof chalkColorFunction('') !== 'string') {
26
+ throw new Error(errorMessage);
27
+ }
28
+ return chalkColorFunction;
29
+ case 'function':
30
+ // validate that given function returns a string
31
+ if (typeof setting('') !== 'string') {
32
+ throw new Error(errorMessage);
33
+ }
34
+ return setting;
35
+ default:
36
+ throw new Error(errorMessage);
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,33 @@
1
+ import Color, { type ColorSetting } from './color.js';
2
+ export interface LogOptions {
3
+ logToFileByDefault: boolean;
4
+ logTimestamp: boolean;
5
+ path: string;
6
+ prefix: string;
7
+ suffix: string;
8
+ filename: string;
9
+ additionalLogs: Record<string, ColorSetting>;
10
+ systemLogs: Record<string, ColorSetting>;
11
+ }
12
+ export default class Log {
13
+ options: LogOptions;
14
+ color: Color;
15
+ typeOptions: Record<string, Partial<LogOptions>>;
16
+ directoryCache: Map<string, Promise<void>>;
17
+ [key: string]: unknown;
18
+ info: (content: string, logToFile?: boolean, overwrite?: boolean) => Promise<void>;
19
+ warn: (content: string, logToFile?: boolean, overwrite?: boolean) => Promise<void>;
20
+ error: (content: string, logToFile?: boolean, overwrite?: boolean) => Promise<void>;
21
+ constructor(options?: Partial<LogOptions>);
22
+ defineType(type: string, setting: ColorSetting, options?: Partial<LogOptions> | null): void;
23
+ createConvenienceMethod(type: string): void;
24
+ logAction(type: string, content: string, logToFile?: boolean, overwrite?: boolean): Promise<void>;
25
+ getOptionForType(type: string, option: keyof LogOptions): LogOptions[keyof LogOptions];
26
+ chalk(): ReturnType<Color['getChalkInstance']>;
27
+ log(content: string, type: string, logToFile: boolean, overwrite: boolean): Promise<void>;
28
+ debug(content: unknown, logToFile?: boolean, overwrite?: boolean): Promise<void>;
29
+ writeToFile(type: string, content: string, overwrite: boolean): Promise<void>;
30
+ resolveFilename(template: string, type: string): string;
31
+ validateFileAndDirectory(path: string, targetLog: string): Promise<void>;
32
+ sanitizePath(path: string): string;
33
+ }
package/dist/index.js ADDED
@@ -0,0 +1,219 @@
1
+ import { promises as fsp } from 'fs';
2
+ import { fileURLToPath } from 'url';
3
+ import { hostname } from 'os';
4
+ import projectPath from 'path';
5
+ import Color from './color.js';
6
+ const defaultOptions = {
7
+ logToFileByDefault: false,
8
+ logTimestamp: false,
9
+ path: 'logs/',
10
+ prefix: '',
11
+ suffix: '',
12
+ filename: '',
13
+ additionalLogs: {},
14
+ systemLogs: {
15
+ info: 'cyan',
16
+ warn: 'yellow',
17
+ error: 'red',
18
+ },
19
+ };
20
+ // used to sanitize defineType() options input
21
+ const optionKeys = Object.keys(defaultOptions);
22
+ /*
23
+ * Write failures that a recursive mkdir of the log directory can actually repair:
24
+ * ENOENT (the cached directory was removed at runtime) and ENOTDIR (a path component
25
+ * was replaced by a non-directory). These invalidate the directory cache and are
26
+ * retried once.
27
+ *
28
+ * Permission and mount faults (EACCES, EPERM, EROFS) are deliberately excluded: the
29
+ * retry's only remediation is mkdir(recursive), which is a successful no-op on an
30
+ * existing directory and can change neither a mode nor a mount flag. Retrying them
31
+ * doubled the syscalls on a permanently failing write and defeated the
32
+ * one-mkdir-per-directory invariant this cache exists to establish.
33
+ */
34
+ const recoverableWriteCodes = new Set(['ENOENT', 'ENOTDIR']);
35
+ export default class Log {
36
+ options;
37
+ color;
38
+ typeOptions = {};
39
+ /*
40
+ * Instance-level cache of directory creation, keyed on the resolved directory path.
41
+ * resolveFilename() strips directory separators, so a filename template can never
42
+ * introduce a new directory and date rollover cannot invalidate an entry.
43
+ */
44
+ directoryCache = new Map();
45
+ constructor(options = {}) {
46
+ const merged = {
47
+ ...defaultOptions,
48
+ ...options,
49
+ };
50
+ this.options = merged;
51
+ this.options.path = this.sanitizePath(this.options.path);
52
+ const { additionalLogs, systemLogs } = merged;
53
+ const logs = {
54
+ ...systemLogs,
55
+ ...additionalLogs,
56
+ };
57
+ this.color = new Color();
58
+ this.typeOptions = {};
59
+ // create direct convenience methods for logging
60
+ for (const type of Object.keys(logs)) {
61
+ this.defineType(type, logs[type]);
62
+ }
63
+ }
64
+ // records setting and options for log type, and creates convenience method ie: log.info()
65
+ defineType(type, setting, options = null) {
66
+ this.color.setLogColor(type, setting);
67
+ // create convenience method if it doesn't exist
68
+ if (!this[type])
69
+ this.createConvenienceMethod(type);
70
+ if (!options)
71
+ return;
72
+ if (typeof options !== 'object')
73
+ throw new Error('The options param must be an object.');
74
+ for (const option of Object.keys(options)) {
75
+ if (!optionKeys.includes(option)) {
76
+ throw new Error(`${option} is not a valid configuration object.`
77
+ + '\n For a list of available options, see https://github.com/abofs/stonyx-logs#configuration');
78
+ }
79
+ // sanitize path input
80
+ if (option === 'path') {
81
+ options[option] = this.sanitizePath(options[option]);
82
+ }
83
+ }
84
+ this.typeOptions[type] = options;
85
+ }
86
+ // proxy through `logAction` method in order to set defaults based on argument presence
87
+ createConvenienceMethod(type) {
88
+ this[type] = (content, logToFile, overwrite = false) => this.logAction(type, content, logToFile, overwrite);
89
+ }
90
+ // validates params and sets configuration-based defaults for logging
91
+ logAction(type, content, logToFile, overwrite) {
92
+ // set logToFile default based on class options when not set
93
+ if (logToFile === undefined)
94
+ logToFile = this.getOptionForType(type, 'logToFileByDefault');
95
+ // treat overwrite default as true for log type "debug"
96
+ if (type === 'debug' && overwrite === undefined)
97
+ overwrite = true;
98
+ return this.log(content, type, logToFile, overwrite ?? false);
99
+ }
100
+ // retrieves option setting for given type, default to global
101
+ getOptionForType(type, option) {
102
+ const options = this.typeOptions[type];
103
+ if (!options || !options[option])
104
+ return this.options[option];
105
+ return options[option];
106
+ }
107
+ // exposes chalk for custom color options via defineType
108
+ chalk() {
109
+ return this.color.getChalkInstance();
110
+ }
111
+ // logs to console, and conditionally to file
112
+ async log(content, type, logToFile, overwrite) {
113
+ const logTimestamp = this.getOptionForType(type, 'logTimestamp');
114
+ const timestamp = `[${new Date().toLocaleString('en-US')}]`;
115
+ const chalkColorFunction = this.color.getLogColor(type);
116
+ let prefix = this.getOptionForType(type, 'prefix');
117
+ let suffix = this.getOptionForType(type, 'suffix');
118
+ if (logTimestamp)
119
+ prefix += `${timestamp} `;
120
+ if (prefix)
121
+ prefix = chalkColorFunction(prefix);
122
+ if (suffix)
123
+ suffix = chalkColorFunction(suffix);
124
+ const coloredLog = chalkColorFunction(content);
125
+ console.log(`${prefix}${coloredLog}${suffix}`); // eslint-disable-line no-console
126
+ if (!logToFile)
127
+ return;
128
+ await this.writeToFile(type, `${timestamp} ${content}\n`, overwrite);
129
+ }
130
+ // direct hardcoded debug method (log to file functionality is limited)
131
+ async debug(content, logToFile = false, overwrite = true) {
132
+ console.dir(content, { depth: 6 }); // eslint-disable-line no-console
133
+ if (!logToFile)
134
+ return;
135
+ await this.writeToFile('debug', JSON.stringify(content, null, 2), overwrite);
136
+ }
137
+ async writeToFile(type, content, overwrite) {
138
+ const path = this.getOptionForType(type, 'path');
139
+ const filenameTemplate = this.getOptionForType(type, 'filename');
140
+ const resolvedName = this.resolveFilename(filenameTemplate, type);
141
+ const targetLog = `${path}${resolvedName}`;
142
+ const fileAction = overwrite ? fsp.writeFile : fsp.appendFile;
143
+ await this.validateFileAndDirectory(path, targetLog);
144
+ try {
145
+ await fileAction(targetLog, content);
146
+ }
147
+ catch (error) {
148
+ const { code } = error;
149
+ if (!recoverableWriteCodes.has(code))
150
+ throw error;
151
+ /*
152
+ * The cached directory may have been removed underneath a warm cache. Invalidate
153
+ * the entry and retry exactly once so a cache hit can never become a permanent
154
+ * silent write failure. The rejection is the caller's only failure signal.
155
+ */
156
+ this.directoryCache.delete(path);
157
+ await this.validateFileAndDirectory(path, targetLog);
158
+ await fileAction(targetLog, content);
159
+ }
160
+ }
161
+ // resolves template variables in a filename string
162
+ resolveFilename(template, type) {
163
+ // default to '{type}.log' when no template is configured
164
+ if (!template)
165
+ return `${type}.log`;
166
+ const now = new Date();
167
+ const yyyy = now.getFullYear();
168
+ const mm = String(now.getMonth() + 1).padStart(2, '0');
169
+ const dd = String(now.getDate()).padStart(2, '0');
170
+ const variables = {
171
+ date: `${yyyy}-${mm}-${dd}`,
172
+ type,
173
+ pid: process.pid,
174
+ hostname: hostname(),
175
+ };
176
+ const resolved = template.replace(/\{(\w+)\}/g, (match, key) => {
177
+ return variables[key] !== undefined ? String(variables[key]) : match;
178
+ });
179
+ // sanitize: prevent path traversal and disallow directory separators
180
+ return resolved.replace(/\.\./g, '').replace(/[/\\]/g, '');
181
+ }
182
+ /*
183
+ * Ensures the log directory exists, deduping concurrent and repeat calls onto a single
184
+ * mkdir per directory. No file bootstrap happens here: both write paths already create
185
+ * the file (appendFile opens 'a', writeFile opens 'w'), so a bootstrap write's only
186
+ * reachable effect was truncating a concurrent caller's content.
187
+ *
188
+ * targetLog is unused but retained for signature compatibility.
189
+ */
190
+ async validateFileAndDirectory(path, targetLog) {
191
+ const cached = this.directoryCache.get(path);
192
+ if (cached)
193
+ return cached;
194
+ // cache the promise before awaiting so concurrent writers dedupe and none run early
195
+ const pending = fsp.mkdir(path, { recursive: true }).then(() => undefined);
196
+ this.directoryCache.set(path, pending);
197
+ // never cache a poisoned promise: drop the entry so the next write retries
198
+ pending.catch(() => {
199
+ if (this.directoryCache.get(path) === pending)
200
+ this.directoryCache.delete(path);
201
+ });
202
+ return pending;
203
+ }
204
+ // method to conditionally sanitize user configuration input
205
+ sanitizePath(path) {
206
+ const moduleDir = projectPath.dirname(fileURLToPath(import.meta.url));
207
+ const delim = moduleDir.includes('node_modules') ? 'node_modules' : 'src';
208
+ const splitDir = moduleDir.split(delim);
209
+ if (splitDir.length < 2)
210
+ throw new Error('Failed to locate your project\'s root directory.');
211
+ // use project root directory behind path
212
+ path = projectPath.resolve(splitDir[0], path);
213
+ // force path property to contain a trailing "/"
214
+ if (path[path.length - 1] !== '/') {
215
+ path += '/';
216
+ }
217
+ return path;
218
+ }
219
+ }
package/package.json CHANGED
@@ -1,15 +1,22 @@
1
1
  {
2
2
  "name": "@stonyx/logs",
3
- "version": "1.0.1-beta.2",
3
+ "version": "1.0.1-beta.21",
4
4
  "description": "Simplified logging for node applications",
5
5
  "type": "module",
6
- "main": "src/index.js",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
7
8
  "exports": {
8
- ".": "./src/index.js",
9
- "./color": "./src/color.js"
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./color": {
14
+ "types": "./dist/color.d.ts",
15
+ "default": "./dist/color.js"
16
+ }
10
17
  },
11
18
  "files": [
12
- "src",
19
+ "dist",
13
20
  "README.md"
14
21
  ],
15
22
  "publishConfig": {
@@ -24,7 +31,7 @@
24
31
  "log",
25
32
  "logging",
26
33
  "color-coding",
27
- "chronicle",
34
+ "stonyx",
28
35
  "history",
29
36
  "documentation",
30
37
  "document",
@@ -43,13 +50,20 @@
43
50
  "chalk": "^5.3.0"
44
51
  },
45
52
  "devDependencies": {
53
+ "@types/node": "^25.5.2",
54
+ "@types/qunit": "^2.19.13",
55
+ "@types/sinon": "^21.0.1",
46
56
  "eslint": "^8.27.0",
47
57
  "eslint-plugin-node": "^11.1.0",
48
58
  "qunit": "^2.19.3",
49
- "sinon": "^17.0.0"
59
+ "sinon": "^17.0.0",
60
+ "tsx": "^4.21.0",
61
+ "typescript": "^5.8.3"
50
62
  },
51
63
  "scripts": {
52
- "test": "qunit 'test/unit/**/*-test.js'",
64
+ "build": "tsc",
65
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
66
+ "test": "node --import tsx node_modules/qunit/bin/qunit.js 'test/**/*-test.ts'",
53
67
  "lint": "eslint . --fix"
54
68
  }
55
69
  }
package/src/color.js DELETED
@@ -1,50 +0,0 @@
1
- import chalk from 'chalk';
2
-
3
- export default class Color {
4
- constructor() {
5
- this.types = [];
6
- }
7
-
8
- // retrieves configured color function for log type
9
- getLogColor(type) {
10
- return this.types[type];
11
- }
12
-
13
- getChalkInstance() {
14
- return chalk;
15
- }
16
-
17
- setLogColor(type, setting) {
18
- const chalkColorFunction = this.settingToChalkColorFunction(setting);
19
- this.types[type] = chalkColorFunction;
20
- }
21
-
22
- // retrieves chalk color function, and fully validates output
23
- settingToChalkColorFunction(setting) {
24
- const errorMessage = 'Invalid chalk color function.'
25
- + 'For help with color settings, see https://github.com/abofs/chronicle#defining-logs--colors';
26
-
27
- switch (typeof setting) {
28
- case 'string':
29
- const chalkColorFunction = (setting[0] === '#') ? chalk.hex(setting) : chalk[setting];
30
- if (!chalkColorFunction
31
- || typeof chalkColorFunction !== 'function'
32
- || typeof chalkColorFunction('') !== 'string') {
33
- throw errorMessage;
34
- }
35
-
36
- return chalkColorFunction;
37
-
38
- case 'function':
39
- // validate that given function returns a string
40
- if (typeof setting('') !== 'string') {
41
- throw errorMessage;
42
- }
43
-
44
- return setting;
45
-
46
- default:
47
- throw errorMessage;
48
- }
49
- }
50
- }
package/src/index.js DELETED
@@ -1,172 +0,0 @@
1
- import { mkdirSync, promises as fsp } from 'fs';
2
- import { fileURLToPath } from 'url';
3
- import projectPath from 'path';
4
- import Color from './color.js';
5
-
6
- const defaultOptions = {
7
- logToFileByDefault: false, // default setting (overridable by logToFile param)
8
- logTimestamp: false, // option to include timestamp in console logs
9
- path: 'logs/', // default log directory (relative to main project root directory)
10
- prefix: '',
11
- suffix: '',
12
-
13
- // log types with corresponding color settings
14
- additionalLogs: {},
15
- systemLogs: {
16
- info: 'cyan',
17
- warn: 'yellow',
18
- error: 'red',
19
- },
20
- };
21
-
22
- // used to sanitize defineType() options input
23
- const optionKeys = Object.keys(defaultOptions);
24
-
25
- export default class Chronicle {
26
- constructor(options = defaultOptions) {
27
- options = {
28
- ...defaultOptions,
29
- ...options,
30
- };
31
- this.options = options;
32
- this.options.path = this.sanitizePath(this.options.path);
33
-
34
- const { additionalLogs, systemLogs } = options;
35
- const logs = {
36
- ...systemLogs,
37
- ...additionalLogs,
38
- };
39
-
40
- this.color = new Color();
41
- this.typeOptions = [];
42
-
43
- // create direct convenience methods for logging
44
- for (const type of Object.keys(logs)) {
45
- this.defineType(type, logs[type]);
46
- }
47
- }
48
-
49
- // records setting and options for log type, and crates convenience method ie: chronicle.info()
50
- defineType(type, setting, options = null) {
51
- this.color.setLogColor(type, setting);
52
-
53
- // create convenience method if it doesn't exist
54
- if (!this[type]) this.createConvenienceMethod(type);
55
-
56
- if (!options) return;
57
- if (typeof options !== 'object') throw 'The options param must be an object.';
58
-
59
- for (let option of Object.keys(options)) {
60
- if (!optionKeys.includes(option)) {
61
- throw `${option} is not a valid configuration object.`
62
- + '\n For a list of available options, see https://github.com/abofs/chronicle#configuration';
63
- }
64
-
65
- // sanitize path input
66
- if (option === 'path') options[option] = this.sanitizePath(options[option]);
67
- }
68
-
69
- this.typeOptions[type] = options;
70
- }
71
-
72
- // proxy through `logAction` method in order to set defaults based on argument presence
73
- createConvenienceMethod(type) {
74
- this[type] = (content, logToFile, overwrite = false) =>
75
- this.logAction(type, content, logToFile, overwrite);
76
- }
77
-
78
- // validates params and sets configuration-based defaults for logging
79
- logAction(type, content, logToFile, overwrite) {
80
- // set logToFile default based on class options when not set
81
- if (arguments[2] === undefined) logToFile = this.getOptionForType(type, 'logToFileByDefault');
82
-
83
- // treat overwrite default as true for log type "debug"
84
- if (type === 'debug' && arguments[3] === undefined) overwrite = true;
85
-
86
- return this.log(content, type, logToFile, overwrite);
87
- }
88
-
89
- // retrieves option setting for given type, default to global
90
- getOptionForType(type, option) {
91
- const options = this.typeOptions[type];
92
- if (!options || !options[option]) return this.options[option];
93
-
94
- return options[option];
95
- }
96
-
97
- // exposes chalk for custom color options via defineType
98
- chalk() {
99
- return this.color.getChalkInstance();
100
- }
101
-
102
- // logs to console, and conditionally to file
103
- async log(content, type, logToFile, overwrite) {
104
- const logTimestamp = this.getOptionForType(type, 'logTimestamp');
105
- const timestamp = `[${new Date().toLocaleString('en-US')}]`;
106
- const chalkColorFunction = this.color.getLogColor(type);
107
- let prefix = this.getOptionForType(type, 'prefix');
108
- let suffix = this.getOptionForType(type, 'suffix');
109
- if (logTimestamp) prefix += `${timestamp} `;
110
- if (prefix) prefix = chalkColorFunction(prefix);
111
- if (suffix) suffix = chalkColorFunction(suffix);
112
- const coloredLog = chalkColorFunction(content);
113
-
114
- console.log(`${prefix}${coloredLog}${suffix}`); // eslint-disable-line no-console
115
-
116
- if (!logToFile) return;
117
-
118
- return this.writeToFile(type, `${timestamp} ${content}\n`, overwrite);
119
- }
120
-
121
- // direct hardcoded debug method (log to file functionality is limited)
122
- async debug(content, logToFile = false, overwrite = true) {
123
- console.dir(content, { depth: 6 }); // eslint-disable-line no-console
124
-
125
- if (!logToFile) return;
126
-
127
- return this.writeToFile('debug', JSON.stringify(content, null, 2), overwrite);
128
- }
129
-
130
- async writeToFile(type, content, overwrite) {
131
- const path = this.getOptionForType(type, 'path');
132
- const targetLog = `${path}${type}.log`;
133
- await this.validateFileAndDirectory(path, targetLog);
134
-
135
- const fileAction = overwrite ? fsp.writeFile : fsp.appendFile;
136
-
137
- return fileAction(targetLog, content);
138
- }
139
-
140
- // attempts to create file and/or directory if they don't already exist
141
- async validateFileAndDirectory(path, targetLog) {
142
- const errorMethod = this.error || console.error; // prefer native method unless removed by user
143
-
144
- mkdirSync(path, { recursive: true });
145
-
146
- await fsp.access(targetLog).catch(() => {
147
- fsp.writeFile(targetLog, '').catch(() => {
148
- errorMethod(`Failed to create log file: ${targetLog}.`
149
- + '\n Verify that the application runner has write permissions');
150
- });
151
- });
152
- }
153
-
154
- // method to conditionally sanitize user configuration input
155
- sanitizePath(path) {
156
- const moduleDir = projectPath.dirname(fileURLToPath(import.meta.url));
157
- const delim = moduleDir.includes('node_modules') ? 'node_modules' : 'src';
158
- const splitDir = moduleDir.split(delim);
159
-
160
- if (splitDir.length < 2) throw ('Failed to locate your project\'s root directory.');
161
-
162
- // use project root directory behind path
163
- path = projectPath.resolve(splitDir[0], path);
164
-
165
- // force path property to contain a trailing "/"
166
- if (path[path.length - 1] !== '/') {
167
- path += '/';
168
- }
169
-
170
- return path;
171
- }
172
- }