@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 +147 -59
- package/dist/color.d.ts +10 -0
- package/dist/color.js +39 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +219 -0
- package/package.json +22 -8
- package/src/color.js +0 -50
- package/src/index.js +0 -172
package/README.md
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
|
+
[](https://github.com/abofs/stonyx-logs/actions/workflows/ci.yml)
|
|
2
|
+
[](https://www.npmjs.com/package/@stonyx/logs)
|
|
3
|
+
[](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/
|
|
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
|
-

|
|
13
17
|
|
|
14
18
|
<br>
|
|
15
19
|
|
|
@@ -27,10 +31,10 @@
|
|
|
27
31
|
|
|
28
32
|
---
|
|
29
33
|
|
|
30
|
-
**
|
|
31
|
-
This project is not directly associated with chalk other than chalk being a core dependency of **
|
|
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 **
|
|
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
|
|
48
|
+
npm install @stonyx/logs
|
|
45
49
|
```
|
|
46
50
|
|
|
47
51
|
## Usage
|
|
48
52
|
|
|
49
53
|
```js
|
|
50
|
-
import
|
|
54
|
+
import Log from '@stonyx/logs';
|
|
51
55
|
|
|
52
|
-
const
|
|
56
|
+
const log = new Log();
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
|
66
|
+
import Log from '@stonyx/logs';
|
|
63
67
|
|
|
64
|
-
const
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
|
84
|
+
import Log from '@stonyx/logs';
|
|
81
85
|
|
|
82
|
-
const
|
|
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
|
-
|
|
94
|
+
log.info('Info: sample application has started');
|
|
91
95
|
```
|
|
92
|
-

|
|
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
|
|
102
|
+
import Log from '@stonyx/logs';
|
|
99
103
|
|
|
100
|
-
const
|
|
104
|
+
const log = new Log({ additionalLogs: { question: 'green' } });
|
|
101
105
|
|
|
102
106
|
// create additional log with direct chalk configuration
|
|
103
|
-
|
|
107
|
+
log.defineType('query', log.chalk().black.bgGreen);
|
|
104
108
|
|
|
105
|
-
|
|
106
|
-
await
|
|
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
|
-

|
|
109
113
|
|
|
110
114
|
## API
|
|
111
115
|
|
|
112
116
|
### Defining Logs & Colors
|
|
113
117
|
|
|
114
|
-
By default, **
|
|
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 **
|
|
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
|
|
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
|
-
**
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
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,
|
|
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
|
-
|
|
160
|
+
try {
|
|
161
|
+
await log.error('error message', true);
|
|
157
162
|
|
|
158
|
-
|
|
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
|
-
**
|
|
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 **
|
|
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
|
-
|
|
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 **
|
|
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
|
|
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 **
|
|
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()`. **
|
|
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
|
|
281
|
+
const log = new Log();
|
|
227
282
|
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
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
|
|
306
|
+
const log = new Log();
|
|
252
307
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
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 **
|
|
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 **
|
|
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
|
|
package/dist/color.d.ts
ADDED
|
@@ -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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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.
|
|
3
|
+
"version": "1.0.1-beta.21",
|
|
4
4
|
"description": "Simplified logging for node applications",
|
|
5
5
|
"type": "module",
|
|
6
|
-
"main": "
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
7
8
|
"exports": {
|
|
8
|
-
".":
|
|
9
|
-
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
}
|