@fimbul-works/logger 1.0.0
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/LICENSE +21 -0
- package/README.md +146 -0
- package/biome.json +15 -0
- package/package.json +39 -0
- package/src/index.test.ts +87 -0
- package/src/index.ts +170 -0
- package/tsconfig.cjs.json +9 -0
- package/tsconfig.json +17 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FimbulWorks
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
# @fimbul-works/logger
|
|
2
|
+
|
|
3
|
+
A lightweight, TypeScript-friendly wrapper around [Pino](https://getpino.io/) that provides flexible logging patterns with automatic argument handling.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/@logger-works/logger)
|
|
6
|
+
[](https://github.com/microsoft/TypeScript)
|
|
7
|
+
[](https://bundlephobia.com/package/@logger-works/logger)
|
|
8
|
+
|
|
9
|
+
**Note:** This is a wrapper around [Pino](https://getpino.io/). For advanced configuration and features, refer to the [Pino documentation](https://getpino.io/#/docs/api).
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import Logger from 'your-package-name';
|
|
15
|
+
|
|
16
|
+
const logger = new Logger();
|
|
17
|
+
|
|
18
|
+
logger.info('Hello, world!');
|
|
19
|
+
logger.error('I AM ERROR');
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Installation
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npm install @logger-works/logger
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Features
|
|
29
|
+
|
|
30
|
+
- ๐ Multiple argument patterns supported (console-style, Pino-style, printf-style)
|
|
31
|
+
- ๐ฏ TypeScript support with full type definitions
|
|
32
|
+
- ๐งช Automatic test environment detection (disables output in `NODE_ENV=test`)
|
|
33
|
+
- ๐ Zero-config defaults with STDOUT/STDERR separation
|
|
34
|
+
- ๐ฆ Access to underlying Pino instance for advanced usage
|
|
35
|
+
|
|
36
|
+
## Usage Patterns
|
|
37
|
+
|
|
38
|
+
The logger intelligently handles different argument patterns to accommodate various logging styles:
|
|
39
|
+
|
|
40
|
+
### 1. Simple String Message
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
logger.info('Server started on port 3000');
|
|
44
|
+
// Output: {"level":30,"time":1234567890,"msg":"Server started on port 3000"}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. Pino-Style (Object First)
|
|
48
|
+
|
|
49
|
+
Standard Pino pattern with structured data first, message second:
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
logger.info({ userId: 123, action: 'login' }, 'User logged in');
|
|
53
|
+
// Output: {"level":30,"time":1234567890,"userId":123,"action":"login","msg":"User logged in"}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 3. Console-Style (Message First, Object Second)
|
|
57
|
+
|
|
58
|
+
Automatically swaps arguments to Pino format:
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
logger.info('User logged in', { userId: 123, action: 'login' });
|
|
62
|
+
// Output: {"level":30,"time":1234567890,"userId":123,"action":"login","msg":"User logged in"}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### 4. Printf-Style Formatting
|
|
66
|
+
|
|
67
|
+
Uses Pino's built-in printf formatting when `%` is detected:
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
logger.info('User %s logged in from %s', 'john', '192.168.1.1');
|
|
71
|
+
// Output: {"level":30,"time":1234567890,"msg":"User john logged in from 192.168.1.1"}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 5. Message with Multiple Arguments
|
|
75
|
+
|
|
76
|
+
Wraps additional arguments in a structured `args` field:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
logger.info('Processing items', 'item1', 'item2', 'item3');
|
|
80
|
+
// Output: {"level":30,"time":1234567890,"args":["item1","item2","item3"],"msg":"Processing items"}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### 6. Primitive Values
|
|
84
|
+
|
|
85
|
+
Non-string primitives are wrapped in an `args` array:
|
|
86
|
+
|
|
87
|
+
```typescript
|
|
88
|
+
logger.info(42, true, null);
|
|
89
|
+
// Output: {"level":30,"time":1234567890,"args":[42,true,null],"msg":"Log event"}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## API
|
|
93
|
+
|
|
94
|
+
All methods support the argument patterns described above:
|
|
95
|
+
|
|
96
|
+
- `logger.info(...args)` - Info level logs
|
|
97
|
+
- `logger.error(...args)` - Error level logs
|
|
98
|
+
- `logger.warn(...args)` - Warning level logs
|
|
99
|
+
- `logger.debug(...args)` - Debug level logs
|
|
100
|
+
- `logger.log(level, ...args)` - Log at specified level
|
|
101
|
+
|
|
102
|
+
### Access Raw Pino Instance
|
|
103
|
+
|
|
104
|
+
For advanced Pino features:
|
|
105
|
+
|
|
106
|
+
```typescript
|
|
107
|
+
const pinoInstance = logger.getRawLogger();
|
|
108
|
+
pinoInstance.child({ requestId: '123' });
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Default Behavior
|
|
112
|
+
|
|
113
|
+
### Output Destinations
|
|
114
|
+
|
|
115
|
+
By default, the logger sends:
|
|
116
|
+
- **Info, warn, debug** logs โ STDOUT (file descriptor 1)
|
|
117
|
+
- **Error** logs โ STDERR (file descriptor 2)
|
|
118
|
+
|
|
119
|
+
### Test Environment
|
|
120
|
+
|
|
121
|
+
When `NODE_ENV=test`, all output is automatically disabled to keep test output clean.
|
|
122
|
+
|
|
123
|
+
### Custom Targets
|
|
124
|
+
|
|
125
|
+
Override default targets with your own configuration:
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
const logger = new Logger({
|
|
129
|
+
transport: {
|
|
130
|
+
targets: [
|
|
131
|
+
{
|
|
132
|
+
target: 'pino-pretty',
|
|
133
|
+
options: { colorize: true }
|
|
134
|
+
}
|
|
135
|
+
]
|
|
136
|
+
}
|
|
137
|
+
});
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
MIT License - See [LICENSE](LICENSE) file for details.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
Built with โก by [loggerWorks](https://github.com/logger-works)
|
package/biome.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@fimbul-works/logger",
|
|
3
|
+
"description": "Simple wrapper for Pino",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"private": false,
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "FimbulWorks <contact@fimbul.works>",
|
|
9
|
+
"homepage": "https://github.com/fimbul-works/logger#readme",
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/fimbul-works/logger.git"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/fimbul-works/logger/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"pino",
|
|
19
|
+
"logger",
|
|
20
|
+
"typescript"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "rm -rf dist && pnpm build:esm && pnpm build:cjs",
|
|
24
|
+
"build:esm": "tsc -p tsconfig.json",
|
|
25
|
+
"build:cjs": "tsc -p tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
|
|
26
|
+
"format": "biome format --write",
|
|
27
|
+
"test": "NODE_ENV=test vitest run",
|
|
28
|
+
"prepublishOnly": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"pino": "^10.3.0"
|
|
32
|
+
},
|
|
33
|
+
"devDependencies": {
|
|
34
|
+
"@biomejs/biome": "^2.3.14",
|
|
35
|
+
"@types/node": "^25.2.1",
|
|
36
|
+
"typescript": "^5.9.3",
|
|
37
|
+
"vitest": "^4.0.18"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { Logger } from "./index";
|
|
3
|
+
|
|
4
|
+
// Mock Pino
|
|
5
|
+
vi.mock("pino", () => {
|
|
6
|
+
const info = vi.fn();
|
|
7
|
+
const error = vi.fn();
|
|
8
|
+
const warn = vi.fn();
|
|
9
|
+
const debug = vi.fn();
|
|
10
|
+
const logger = { info, error, warn, debug };
|
|
11
|
+
return {
|
|
12
|
+
default: vi.fn(() => logger),
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe("Logger", () => {
|
|
17
|
+
it("should log info messages", () => {
|
|
18
|
+
const logger = new Logger();
|
|
19
|
+
logger.info("Hello world");
|
|
20
|
+
|
|
21
|
+
const rawLogger = logger.getRawLogger();
|
|
22
|
+
expect(rawLogger.info).toHaveBeenCalledWith("Hello world");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("should log error messages", () => {
|
|
26
|
+
const logger = new Logger();
|
|
27
|
+
const err = new Error("Test error");
|
|
28
|
+
|
|
29
|
+
logger.error("Something went wrong", err);
|
|
30
|
+
|
|
31
|
+
const rawLogger = logger.getRawLogger();
|
|
32
|
+
expect(rawLogger.error).toHaveBeenCalledWith(err, "Something went wrong");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("Smart Argument Handling", () => {
|
|
36
|
+
it("should swap args when calling (msg, obj)", () => {
|
|
37
|
+
const logger = new Logger();
|
|
38
|
+
const rawLogger = logger.getRawLogger();
|
|
39
|
+
const obj = { id: 123 };
|
|
40
|
+
|
|
41
|
+
logger.info("User login", obj);
|
|
42
|
+
|
|
43
|
+
// Expect swap: (obj, msg)
|
|
44
|
+
expect(rawLogger.info).toHaveBeenCalledWith(obj, "User login");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should wrap multiple args in payload object", () => {
|
|
48
|
+
const logger = new Logger();
|
|
49
|
+
const rawLogger = logger.getRawLogger();
|
|
50
|
+
|
|
51
|
+
logger.info("Values", 1, true, "extra");
|
|
52
|
+
|
|
53
|
+
// Expect wrap: ({ args: [1, true, "extra"] }, msg)
|
|
54
|
+
expect(rawLogger.info).toHaveBeenCalledWith({ args: [1, true, "extra"] }, "Values");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("should respect printf style formatting", () => {
|
|
58
|
+
const logger = new Logger();
|
|
59
|
+
const rawLogger = logger.getRawLogger();
|
|
60
|
+
|
|
61
|
+
logger.info("User %s logged in", "Alice");
|
|
62
|
+
|
|
63
|
+
// Expect pass-through: (msg, ...args)
|
|
64
|
+
expect(rawLogger.info).toHaveBeenCalledWith("User %s logged in", "Alice");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should handle mixed printf and extra args (best effort)", () => {
|
|
68
|
+
const logger = new Logger();
|
|
69
|
+
const rawLogger = logger.getRawLogger();
|
|
70
|
+
|
|
71
|
+
// If user mixes printf with too many args, we just pass through and let Pino handle/ignore
|
|
72
|
+
logger.info("User %s", "Alice", "ExtraIgnored");
|
|
73
|
+
|
|
74
|
+
expect(rawLogger.info).toHaveBeenCalledWith("User %s", "Alice", "ExtraIgnored");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("should pass through (obj, msg) as is", () => {
|
|
78
|
+
const logger = new Logger();
|
|
79
|
+
const rawLogger = logger.getRawLogger();
|
|
80
|
+
const obj = { id: 1 };
|
|
81
|
+
|
|
82
|
+
logger.info(obj, "Message");
|
|
83
|
+
|
|
84
|
+
expect(rawLogger.info).toHaveBeenCalledWith(obj, "Message");
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import pino, { type Logger as Pino } from "pino";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Log level.
|
|
5
|
+
*/
|
|
6
|
+
export type LogLevel = "info" | "error" | "warn" | "debug";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Configuration options for the Logger.
|
|
10
|
+
*/
|
|
11
|
+
export interface LoggerOptions extends pino.LoggerOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Optional name label (e.g., "http")
|
|
14
|
+
*/
|
|
15
|
+
name?: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Optional log level (e.g., "info"), default: "info"
|
|
19
|
+
*/
|
|
20
|
+
level?: string;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Optional targets, default: STDOUT & STDERR, disabled when NODE_ENV="test"
|
|
24
|
+
*/
|
|
25
|
+
targets?: pino.TransportTargetOptions[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A wrapper around Pino to provide a consistent logging interface.
|
|
30
|
+
*/
|
|
31
|
+
export class Logger {
|
|
32
|
+
/**
|
|
33
|
+
* pino instance.
|
|
34
|
+
*
|
|
35
|
+
* @private
|
|
36
|
+
* @type {Pino}
|
|
37
|
+
*/
|
|
38
|
+
private logger: Pino;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Creates a new Logger instance.
|
|
42
|
+
*
|
|
43
|
+
* @param {LoggerOptions} options - Optional configuration options for the logger
|
|
44
|
+
*/
|
|
45
|
+
constructor(options: pino.LoggerOptions = {}) {
|
|
46
|
+
this.logger = pino({
|
|
47
|
+
level: "info",
|
|
48
|
+
...options,
|
|
49
|
+
transport: {
|
|
50
|
+
targets:
|
|
51
|
+
process.env.NODE_ENV !== "test"
|
|
52
|
+
? [
|
|
53
|
+
{
|
|
54
|
+
target: "pino/file",
|
|
55
|
+
options: { destination: 1 }, // STDOUT
|
|
56
|
+
level: options.level || "info",
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
target: "pino/file",
|
|
60
|
+
options: { destination: 2 }, // STDERR
|
|
61
|
+
level: "error",
|
|
62
|
+
},
|
|
63
|
+
]
|
|
64
|
+
: [],
|
|
65
|
+
...options.transport,
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Logs a message at the specified level.
|
|
72
|
+
*
|
|
73
|
+
* @param {LogLevel} level - The log level.
|
|
74
|
+
*/
|
|
75
|
+
public log(level: LogLevel, ...args: any[]): void {
|
|
76
|
+
this.handleLog(level, args);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Logs a message at the `info` level.
|
|
81
|
+
*/
|
|
82
|
+
public info(...args: any[]): void {
|
|
83
|
+
this.handleLog("info", args);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Logs a message at the `error` level.
|
|
88
|
+
*/
|
|
89
|
+
public error(...args: any[]): void {
|
|
90
|
+
this.handleLog("error", args);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Logs a message at the `warn` level.
|
|
95
|
+
*/
|
|
96
|
+
public warn(...args: any[]): void {
|
|
97
|
+
this.handleLog("warn", args);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Logs a message at the `debug` level.
|
|
102
|
+
*/
|
|
103
|
+
public debug(...args: any[]): void {
|
|
104
|
+
this.handleLog("debug", args);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Returns the underlying Pino logger instance.
|
|
109
|
+
*
|
|
110
|
+
* @returns {Pino} The Pino logger instance.
|
|
111
|
+
*/
|
|
112
|
+
public getRawLogger(): Pino {
|
|
113
|
+
return this.logger;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Handles logging by parsing arguments and calling the appropriate Pino method.
|
|
118
|
+
*
|
|
119
|
+
* @private
|
|
120
|
+
* @param {LogLevel} level
|
|
121
|
+
* @param {any[]} args
|
|
122
|
+
*/
|
|
123
|
+
private handleLog(level: LogLevel, args: any[]): void {
|
|
124
|
+
if (args.length === 0) {
|
|
125
|
+
this.logger[level]("");
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const [first, ...rest] = args;
|
|
130
|
+
|
|
131
|
+
// Case 1: Object first -> Pass through (Standard Pino)
|
|
132
|
+
// logger.info({ id: 1 }, "msg")
|
|
133
|
+
if (typeof first === "object" && first !== null) {
|
|
134
|
+
this.logger[level](first, ...rest);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Case 2: String first
|
|
139
|
+
if (typeof first === "string") {
|
|
140
|
+
// Check for printf-style formatting
|
|
141
|
+
// Simple heuristic: if string contains %, treat as printf and pass through
|
|
142
|
+
if (first.includes("%") && rest.length > 0) {
|
|
143
|
+
this.logger[level](first, ...rest);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Check for (msg, obj) pattern -> Swap to (obj, msg) (Console style)
|
|
148
|
+
if (rest.length === 1 && typeof rest[0] === "object" && rest[0] !== null) {
|
|
149
|
+
this.logger[level](rest[0], first);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Check for (msg, ...vars) pattern -> Wrap in payload
|
|
154
|
+
if (rest.length > 0) {
|
|
155
|
+
this.logger[level]({ args: rest }, first);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Just a single string message
|
|
160
|
+
this.logger[level](first);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Case 3: Primitive first (non-string output) -> Wrap in payload
|
|
165
|
+
// logger.info(1, true)
|
|
166
|
+
this.logger[level]({ args }, "Log event");
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export default Logger;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ES2020",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"allowJs": true,
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"outDir": "./dist/esm/"
|
|
14
|
+
},
|
|
15
|
+
"include": ["src"],
|
|
16
|
+
"exclude": ["src/**/*.test.ts", "node_modules"]
|
|
17
|
+
}
|