@brianbuie/node-kit 0.0.4 → 0.1.1
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/.dist/Jwt.d.ts +16 -0
- package/.dist/Jwt.js +35 -0
- package/.dist/Log.d.ts +43 -0
- package/.dist/Log.js +81 -0
- package/.dist/_index.d.ts +3 -0
- package/.dist/_index.js +3 -0
- package/.dist/snapshot.d.ts +5 -0
- package/.dist/snapshot.js +33 -0
- package/README.md +91 -8
- package/package.json +5 -2
- package/src/Jwt.test.ts +22 -0
- package/src/Jwt.ts +56 -0
- package/src/Log.test.ts +28 -0
- package/src/Log.ts +102 -0
- package/src/_index.ts +3 -0
- package/src/snapshot.test.ts +35 -0
- package/src/snapshot.ts +36 -0
package/.dist/Jwt.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type JwtPayload, type SignOptions } from 'jsonwebtoken';
|
|
2
|
+
export declare class Jwt {
|
|
3
|
+
#private;
|
|
4
|
+
payload: JwtPayload;
|
|
5
|
+
options: SignOptions;
|
|
6
|
+
seconds: number;
|
|
7
|
+
key: string;
|
|
8
|
+
constructor({ payload, options, seconds, key, }: {
|
|
9
|
+
payload: JwtPayload;
|
|
10
|
+
options: SignOptions;
|
|
11
|
+
seconds: number;
|
|
12
|
+
key: string;
|
|
13
|
+
});
|
|
14
|
+
get now(): number;
|
|
15
|
+
get token(): string;
|
|
16
|
+
}
|
package/.dist/Jwt.js
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { default as jsonwebtoken } from 'jsonwebtoken';
|
|
2
|
+
import { merge } from 'lodash-es';
|
|
3
|
+
export class Jwt {
|
|
4
|
+
payload;
|
|
5
|
+
options;
|
|
6
|
+
seconds;
|
|
7
|
+
key;
|
|
8
|
+
#saved;
|
|
9
|
+
constructor({ payload, options, seconds, key, }) {
|
|
10
|
+
this.payload = payload;
|
|
11
|
+
this.options = options;
|
|
12
|
+
this.seconds = seconds;
|
|
13
|
+
this.key = key;
|
|
14
|
+
this.#createToken();
|
|
15
|
+
}
|
|
16
|
+
get now() {
|
|
17
|
+
return Math.floor(Date.now() / 1000);
|
|
18
|
+
}
|
|
19
|
+
get token() {
|
|
20
|
+
if (this.#saved && this.#saved.exp > this.now) {
|
|
21
|
+
return this.#saved.token;
|
|
22
|
+
}
|
|
23
|
+
return this.#createToken();
|
|
24
|
+
}
|
|
25
|
+
#createToken() {
|
|
26
|
+
const exp = this.now + this.seconds;
|
|
27
|
+
const payload = merge({
|
|
28
|
+
iat: this.now,
|
|
29
|
+
exp,
|
|
30
|
+
}, this.payload);
|
|
31
|
+
const token = jsonwebtoken.sign(payload, this.key, this.options);
|
|
32
|
+
this.#saved = { token, exp };
|
|
33
|
+
return token;
|
|
34
|
+
}
|
|
35
|
+
}
|
package/.dist/Log.d.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { type ChalkInstance } from 'chalk';
|
|
2
|
+
type Severity = 'DEFAULT' | 'DEBUG' | 'INFO' | 'NOTICE' | 'WARNING' | 'ERROR' | 'CRITICAL' | 'ALERT' | 'EMERGENCY';
|
|
3
|
+
type Options = {
|
|
4
|
+
severity: Severity;
|
|
5
|
+
color: ChalkInstance;
|
|
6
|
+
};
|
|
7
|
+
export declare class Log {
|
|
8
|
+
#private;
|
|
9
|
+
static isTest: boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Handle first argument being a string or an object with a 'message' prop
|
|
12
|
+
* Also snapshots special objects (eg Error, Response) to keep props in later JSON.stringify output
|
|
13
|
+
*/
|
|
14
|
+
static prepare(...input: unknown[]): {
|
|
15
|
+
message?: string;
|
|
16
|
+
details: unknown[];
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Logs error details before throwing
|
|
20
|
+
*/
|
|
21
|
+
static error(...input: unknown[]): void;
|
|
22
|
+
static warn(...input: unknown[]): {
|
|
23
|
+
message: string | undefined;
|
|
24
|
+
details: unknown[];
|
|
25
|
+
options: Options;
|
|
26
|
+
};
|
|
27
|
+
static notice(...input: unknown[]): {
|
|
28
|
+
message: string | undefined;
|
|
29
|
+
details: unknown[];
|
|
30
|
+
options: Options;
|
|
31
|
+
};
|
|
32
|
+
static info(...input: unknown[]): {
|
|
33
|
+
message: string | undefined;
|
|
34
|
+
details: unknown[];
|
|
35
|
+
options: Options;
|
|
36
|
+
};
|
|
37
|
+
static debug(...input: unknown[]): {
|
|
38
|
+
message: string | undefined;
|
|
39
|
+
details: unknown[];
|
|
40
|
+
options: Options;
|
|
41
|
+
} | undefined;
|
|
42
|
+
}
|
|
43
|
+
export {};
|
package/.dist/Log.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { inspect } from 'util';
|
|
3
|
+
import { isObjectLike } from 'lodash-es';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { snapshot } from './snapshot.js';
|
|
6
|
+
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
7
|
+
export class Log {
|
|
8
|
+
static isTest = process.env.npm_package_name === packageJson.name && process.env.npm_lifecycle_event === 'test';
|
|
9
|
+
/**
|
|
10
|
+
* Gcloud parses JSON in stdout
|
|
11
|
+
*/
|
|
12
|
+
static #toGcloud(entry) {
|
|
13
|
+
if (entry.details?.length === 1) {
|
|
14
|
+
console.log(JSON.stringify({ ...entry, details: entry.details[0] }));
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
console.log(JSON.stringify(entry));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Includes colors and better inspection for logging during dev
|
|
22
|
+
*/
|
|
23
|
+
static #toConsole(entry, color) {
|
|
24
|
+
if (entry.message)
|
|
25
|
+
console.log(color(`[${entry.severity}] ${entry.message}`));
|
|
26
|
+
entry.details?.forEach(detail => {
|
|
27
|
+
console.log(inspect(detail, { depth: 10, breakLength: 100, compact: true, colors: true }));
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
static #log(options, ...input) {
|
|
31
|
+
const { message, details } = this.prepare(...input);
|
|
32
|
+
// https://cloud.google.com/run/docs/container-contract#env-vars
|
|
33
|
+
const isGcloud = process.env.K_SERVICE !== undefined || process.env.CLOUD_RUN_JOB !== undefined;
|
|
34
|
+
if (isGcloud) {
|
|
35
|
+
this.#toGcloud({ message, severity: options.severity, details });
|
|
36
|
+
return { message, details, options };
|
|
37
|
+
}
|
|
38
|
+
// Hide output while testing this package
|
|
39
|
+
if (!this.isTest) {
|
|
40
|
+
this.#toConsole({ message, severity: options.severity, details }, options.color);
|
|
41
|
+
}
|
|
42
|
+
return { message, details, options };
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Handle first argument being a string or an object with a 'message' prop
|
|
46
|
+
* Also snapshots special objects (eg Error, Response) to keep props in later JSON.stringify output
|
|
47
|
+
*/
|
|
48
|
+
static prepare(...input) {
|
|
49
|
+
let [first, ...rest] = input.map(snapshot);
|
|
50
|
+
if (typeof first === 'string')
|
|
51
|
+
return { message: first, details: rest };
|
|
52
|
+
// @ts-ignore
|
|
53
|
+
if (isObjectLike(first) && typeof first['message'] === 'string') {
|
|
54
|
+
const { message, ...firstDetails } = first;
|
|
55
|
+
return { message, details: [firstDetails, ...rest] };
|
|
56
|
+
}
|
|
57
|
+
return { details: input };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Logs error details before throwing
|
|
61
|
+
*/
|
|
62
|
+
static error(...input) {
|
|
63
|
+
const { message } = this.#log({ severity: 'ERROR', color: chalk.red }, ...input);
|
|
64
|
+
throw new Error(message);
|
|
65
|
+
}
|
|
66
|
+
static warn(...input) {
|
|
67
|
+
return this.#log({ severity: 'WARNING', color: chalk.yellow }, ...input);
|
|
68
|
+
}
|
|
69
|
+
static notice(...input) {
|
|
70
|
+
return this.#log({ severity: 'NOTICE', color: chalk.cyan }, ...input);
|
|
71
|
+
}
|
|
72
|
+
static info(...input) {
|
|
73
|
+
return this.#log({ severity: 'INFO', color: chalk.white }, ...input);
|
|
74
|
+
}
|
|
75
|
+
static debug(...input) {
|
|
76
|
+
const debugging = process.argv.some(arg => arg.includes('--debug')) || process.env.DEBUG !== undefined;
|
|
77
|
+
if (debugging || process.env.NODE_ENV !== 'production') {
|
|
78
|
+
return this.#log({ severity: 'DEBUG', color: chalk.gray }, ...input);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
package/.dist/_index.d.ts
CHANGED
package/.dist/_index.js
CHANGED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { isObjectLike } from 'lodash-es';
|
|
2
|
+
/**
|
|
3
|
+
* Allows special objects (Error, Headers, Set) to be included in JSON.stringify output
|
|
4
|
+
* functions are removed
|
|
5
|
+
*/
|
|
6
|
+
export function snapshot(i) {
|
|
7
|
+
if (Array.isArray(i))
|
|
8
|
+
return i.map(snapshot);
|
|
9
|
+
if (typeof i === 'function')
|
|
10
|
+
return undefined;
|
|
11
|
+
if (!isObjectLike(i))
|
|
12
|
+
return i;
|
|
13
|
+
let output = {};
|
|
14
|
+
// @ts-ignore If it has an 'entries' function, use that for looping (eg. Set, Map, Headers)
|
|
15
|
+
if (typeof i.entries === 'function') {
|
|
16
|
+
// @ts-ignore
|
|
17
|
+
for (let [k, v] of i.entries()) {
|
|
18
|
+
output[k] = snapshot(v);
|
|
19
|
+
}
|
|
20
|
+
return output;
|
|
21
|
+
}
|
|
22
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Enumerability_and_ownership_of_properties
|
|
23
|
+
// Get Enumerable, inherited properties
|
|
24
|
+
const obj = i;
|
|
25
|
+
for (let key in obj) {
|
|
26
|
+
output[key] = snapshot(obj[key]);
|
|
27
|
+
}
|
|
28
|
+
// Get Non-enumberable, own properties
|
|
29
|
+
Object.getOwnPropertyNames(obj).forEach(key => {
|
|
30
|
+
output[key] = snapshot(obj[key]);
|
|
31
|
+
});
|
|
32
|
+
return output;
|
|
33
|
+
}
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Node Kit
|
|
1
|
+
# Node Kit • 
|
|
2
2
|
|
|
3
3
|
Basic tools for quick node.js projects
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Installing
|
|
6
6
|
|
|
7
7
|
```
|
|
8
8
|
npm add @brianbuie/node-kit
|
|
@@ -12,17 +12,100 @@ npm add @brianbuie/node-kit
|
|
|
12
12
|
import { thing } from '@brianbuie/node-kit';
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
### Fetcher
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { Fetcher } from '@brianbuie/node-kit';
|
|
21
|
+
|
|
22
|
+
// All requests will include Authorization header
|
|
23
|
+
const api = new Fetcher({
|
|
24
|
+
base: 'https://www.example.com',
|
|
25
|
+
headers: {
|
|
26
|
+
Authorization: `Bearer ${process.env.EXAMPLE_SECRET}`,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// GET https://www.example.com/route
|
|
31
|
+
// returns [Response, Request]
|
|
32
|
+
const [res] = await api.fetch('/route');
|
|
33
|
+
|
|
34
|
+
// GET https://www.example.com/other-route
|
|
35
|
+
// returns [string, Response, Request]
|
|
36
|
+
const [text] = await api.fetchText('/other-route');
|
|
37
|
+
|
|
38
|
+
// GET https://www.example.com/thing?page=1
|
|
39
|
+
// returns [Thing, Response, Request]
|
|
40
|
+
const [data] = await api.fetchJson<Thing>('/thing', { query: { page: 1 } });
|
|
41
|
+
|
|
42
|
+
// POST https://www.example.com/thing (data is sent as JSON in body)
|
|
43
|
+
// returns [Thing, Response, Request]
|
|
44
|
+
const [result] = await api.fetchJson<Thing>('/thing', { data: { example: 1 } });
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### Jwt
|
|
48
|
+
|
|
49
|
+
Save a JSON Web Token in memory and reuse it throughout the process.
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
import { Jwt, Fetcher } from '@brianbuie/node-kit';
|
|
53
|
+
|
|
54
|
+
const apiJwt = new Jwt({
|
|
55
|
+
payload: {
|
|
56
|
+
example: 'value',
|
|
57
|
+
},
|
|
58
|
+
options: {
|
|
59
|
+
algorithm: 'HS256',
|
|
60
|
+
},
|
|
61
|
+
seconds: 60,
|
|
62
|
+
key: process.env.JWT_KEY,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const api = new Fetcher({
|
|
66
|
+
base: 'https://example.com',
|
|
67
|
+
headers: {
|
|
68
|
+
Authorization: `Bearer ${apiJwt.token}`,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
> TODO: expiration is not checked again when provided in a header
|
|
74
|
+
|
|
75
|
+
### Log
|
|
76
|
+
|
|
77
|
+
Chalk output in development, structured JSON when running in gcloud
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
import { Log } from '@brianbuie/node-kit';
|
|
81
|
+
|
|
82
|
+
Log.info('message', { other: 'details' });
|
|
83
|
+
|
|
84
|
+
// Print in development, or if process.env.DEBUG or --debug argument is present
|
|
85
|
+
Log.debug('message', Response);
|
|
86
|
+
|
|
87
|
+
// Log details and throw
|
|
88
|
+
Log.error('Something happened', details, moreDetails);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### snapshot
|
|
92
|
+
|
|
93
|
+
Gets all enumerable and non-enumerable properties, so they can be included in JSON.stringify. Helpful for built-in objects, like Error, Request, Response, Headers, Map, etc.
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
fs.writeFileSync('result.json', JSON.stringify(snapshot(response), null, 2));
|
|
97
|
+
```
|
|
98
|
+
|
|
15
99
|
## Publishing changes to this package
|
|
16
100
|
|
|
17
|
-
Commit all changes, then run
|
|
101
|
+
Commit all changes, then run:
|
|
18
102
|
|
|
19
103
|
```
|
|
20
104
|
npm version [patch|minor|major] [-m "custom commit message"]
|
|
21
105
|
```
|
|
22
106
|
|
|
23
107
|
- Bumps version in `package.json`
|
|
24
|
-
- Runs tests
|
|
25
|
-
-
|
|
26
|
-
-
|
|
27
|
-
-
|
|
28
|
-
- The new tag will trigger github action to publish to npm
|
|
108
|
+
- Runs tests (`"preversion"` script in `package.json`)
|
|
109
|
+
- Creates new commit, tagged with version
|
|
110
|
+
- Pushes commit and tags to github (`"postversion"` script in `package.json`)
|
|
111
|
+
- The new tag will trigger github action to publish to npm (`.github/actions/publish.yml`)
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brianbuie/node-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
7
7
|
"url": "git+https://github.com/brianbuie/node-kit.git"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "tsc && node --test \".dist/**/*.test.js\"",
|
|
10
|
+
"test": "tsc && node --test \".dist/**/*.test.js\" --quiet",
|
|
11
11
|
"preversion": "npm test",
|
|
12
12
|
"postversion": "git push --follow-tags"
|
|
13
13
|
},
|
|
@@ -25,8 +25,11 @@
|
|
|
25
25
|
"node": ">=24"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
28
29
|
"@types/lodash-es": "^4.17.12",
|
|
29
30
|
"@types/node": "^24.9.1",
|
|
31
|
+
"chalk": "^5.6.2",
|
|
32
|
+
"jsonwebtoken": "^9.0.2",
|
|
30
33
|
"lodash-es": "^4.17.21"
|
|
31
34
|
},
|
|
32
35
|
"devDependencies": {
|
package/src/Jwt.test.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import jsonwebtoken from 'jsonwebtoken';
|
|
4
|
+
import { Jwt } from './Jwt.js';
|
|
5
|
+
|
|
6
|
+
describe('Jwt', () => {
|
|
7
|
+
it('should create a valid JWT', () => {
|
|
8
|
+
const key = 'test';
|
|
9
|
+
const jwt = new Jwt({
|
|
10
|
+
payload: {
|
|
11
|
+
example: 'value',
|
|
12
|
+
},
|
|
13
|
+
options: {
|
|
14
|
+
algorithm: 'HS256',
|
|
15
|
+
},
|
|
16
|
+
seconds: 60,
|
|
17
|
+
key,
|
|
18
|
+
});
|
|
19
|
+
const result = jsonwebtoken.verify(jwt.token, key);
|
|
20
|
+
assert(typeof result !== 'string' && result.example === 'value');
|
|
21
|
+
});
|
|
22
|
+
});
|
package/src/Jwt.ts
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { default as jsonwebtoken, type JwtPayload, type SignOptions } from 'jsonwebtoken';
|
|
2
|
+
import { merge } from 'lodash-es';
|
|
3
|
+
|
|
4
|
+
export class Jwt {
|
|
5
|
+
payload: JwtPayload;
|
|
6
|
+
options: SignOptions;
|
|
7
|
+
seconds: number;
|
|
8
|
+
key: string;
|
|
9
|
+
#saved?: {
|
|
10
|
+
exp: number;
|
|
11
|
+
token: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
constructor({
|
|
15
|
+
payload,
|
|
16
|
+
options,
|
|
17
|
+
seconds,
|
|
18
|
+
key,
|
|
19
|
+
}: {
|
|
20
|
+
payload: JwtPayload;
|
|
21
|
+
options: SignOptions;
|
|
22
|
+
seconds: number;
|
|
23
|
+
key: string;
|
|
24
|
+
}) {
|
|
25
|
+
this.payload = payload;
|
|
26
|
+
this.options = options;
|
|
27
|
+
this.seconds = seconds;
|
|
28
|
+
this.key = key;
|
|
29
|
+
this.#createToken();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get now() {
|
|
33
|
+
return Math.floor(Date.now() / 1000);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get token() {
|
|
37
|
+
if (this.#saved && this.#saved.exp > this.now) {
|
|
38
|
+
return this.#saved.token;
|
|
39
|
+
}
|
|
40
|
+
return this.#createToken();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
#createToken() {
|
|
44
|
+
const exp = this.now + this.seconds;
|
|
45
|
+
const payload: JwtPayload = merge(
|
|
46
|
+
{
|
|
47
|
+
iat: this.now,
|
|
48
|
+
exp,
|
|
49
|
+
},
|
|
50
|
+
this.payload
|
|
51
|
+
);
|
|
52
|
+
const token = jsonwebtoken.sign(payload, this.key, this.options);
|
|
53
|
+
this.#saved = { token, exp };
|
|
54
|
+
return token;
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/Log.test.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { Log } from './Log.js';
|
|
4
|
+
|
|
5
|
+
describe('Log', () => {
|
|
6
|
+
it('should throw error', () => {
|
|
7
|
+
try {
|
|
8
|
+
Log.error('Test error');
|
|
9
|
+
} catch (e) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
assert(false, 'Did not throw error');
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('should recognize this is a test', () => {
|
|
16
|
+
assert(Log.isTest);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should use first argument as message when string', () => {
|
|
20
|
+
const result = Log.prepare('test', { something: 'else' });
|
|
21
|
+
assert(result.message === 'test');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should use message prop when provided', () => {
|
|
25
|
+
const result = Log.prepare({ message: 'test', something: 'else' });
|
|
26
|
+
assert(result.message === 'test');
|
|
27
|
+
});
|
|
28
|
+
});
|
package/src/Log.ts
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import { inspect } from 'util';
|
|
3
|
+
import { isObjectLike } from 'lodash-es';
|
|
4
|
+
import chalk, { type ChalkInstance } from 'chalk';
|
|
5
|
+
import { snapshot } from './snapshot.js';
|
|
6
|
+
|
|
7
|
+
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
|
|
8
|
+
|
|
9
|
+
type Severity = 'DEFAULT' | 'DEBUG' | 'INFO' | 'NOTICE' | 'WARNING' | 'ERROR' | 'CRITICAL' | 'ALERT' | 'EMERGENCY';
|
|
10
|
+
|
|
11
|
+
type Options = {
|
|
12
|
+
severity: Severity;
|
|
13
|
+
color: ChalkInstance;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type Entry = {
|
|
17
|
+
message?: string;
|
|
18
|
+
severity: Severity;
|
|
19
|
+
details?: unknown[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export class Log {
|
|
23
|
+
static isTest = process.env.npm_package_name === packageJson.name && process.env.npm_lifecycle_event === 'test';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Gcloud parses JSON in stdout
|
|
27
|
+
*/
|
|
28
|
+
static #toGcloud(entry: Entry) {
|
|
29
|
+
if (entry.details?.length === 1) {
|
|
30
|
+
console.log(JSON.stringify({ ...entry, details: entry.details[0] }));
|
|
31
|
+
} else {
|
|
32
|
+
console.log(JSON.stringify(entry));
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Includes colors and better inspection for logging during dev
|
|
38
|
+
*/
|
|
39
|
+
static #toConsole(entry: Entry, color: ChalkInstance) {
|
|
40
|
+
if (entry.message) console.log(color(`[${entry.severity}] ${entry.message}`));
|
|
41
|
+
entry.details?.forEach(detail => {
|
|
42
|
+
console.log(inspect(detail, { depth: 10, breakLength: 100, compact: true, colors: true }));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static #log(options: Options, ...input: unknown[]) {
|
|
47
|
+
const { message, details } = this.prepare(...input);
|
|
48
|
+
// https://cloud.google.com/run/docs/container-contract#env-vars
|
|
49
|
+
const isGcloud = process.env.K_SERVICE !== undefined || process.env.CLOUD_RUN_JOB !== undefined;
|
|
50
|
+
if (isGcloud) {
|
|
51
|
+
this.#toGcloud({ message, severity: options.severity, details });
|
|
52
|
+
return { message, details, options };
|
|
53
|
+
}
|
|
54
|
+
// Hide output while testing this package
|
|
55
|
+
if (!this.isTest) {
|
|
56
|
+
this.#toConsole({ message, severity: options.severity, details }, options.color);
|
|
57
|
+
}
|
|
58
|
+
return { message, details, options };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Handle first argument being a string or an object with a 'message' prop
|
|
63
|
+
* Also snapshots special objects (eg Error, Response) to keep props in later JSON.stringify output
|
|
64
|
+
*/
|
|
65
|
+
static prepare(...input: unknown[]): { message?: string; details: unknown[] } {
|
|
66
|
+
let [first, ...rest] = input.map(snapshot);
|
|
67
|
+
if (typeof first === 'string') return { message: first, details: rest };
|
|
68
|
+
// @ts-ignore
|
|
69
|
+
if (isObjectLike(first) && typeof first['message'] === 'string') {
|
|
70
|
+
const { message, ...firstDetails } = first as { message: string };
|
|
71
|
+
return { message, details: [firstDetails, ...rest] };
|
|
72
|
+
}
|
|
73
|
+
return { details: input };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Logs error details before throwing
|
|
78
|
+
*/
|
|
79
|
+
static error(...input: unknown[]) {
|
|
80
|
+
const { message } = this.#log({ severity: 'ERROR', color: chalk.red }, ...input);
|
|
81
|
+
throw new Error(message);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
static warn(...input: unknown[]) {
|
|
85
|
+
return this.#log({ severity: 'WARNING', color: chalk.yellow }, ...input);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static notice(...input: unknown[]) {
|
|
89
|
+
return this.#log({ severity: 'NOTICE', color: chalk.cyan }, ...input);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static info(...input: unknown[]) {
|
|
93
|
+
return this.#log({ severity: 'INFO', color: chalk.white }, ...input);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
static debug(...input: unknown[]) {
|
|
97
|
+
const debugging = process.argv.some(arg => arg.includes('--debug')) || process.env.DEBUG !== undefined;
|
|
98
|
+
if (debugging || process.env.NODE_ENV !== 'production') {
|
|
99
|
+
return this.#log({ severity: 'DEBUG', color: chalk.gray }, ...input);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
package/src/_index.ts
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { snapshot } from './snapshot.js';
|
|
4
|
+
|
|
5
|
+
describe('snapshot', () => {
|
|
6
|
+
it('should capture Error details', () => {
|
|
7
|
+
try {
|
|
8
|
+
throw new Error('Test Error');
|
|
9
|
+
} catch (e) {
|
|
10
|
+
const result = snapshot(e) as Error;
|
|
11
|
+
assert(result.message !== undefined);
|
|
12
|
+
assert(result.stack !== undefined);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
it('should capture Map values', () => {
|
|
17
|
+
const test = new Map<string, string>();
|
|
18
|
+
test.set('key', 'value');
|
|
19
|
+
const shot = JSON.parse(JSON.stringify(snapshot(test))) as Record<string, string>;
|
|
20
|
+
assert(shot.key === 'value');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('should capture Request values', () => {
|
|
24
|
+
const test = new Request('https://www.google.com', { headers: { example: 'value' } });
|
|
25
|
+
const shot = JSON.parse(JSON.stringify(snapshot(test))) as Record<string, any>;
|
|
26
|
+
assert(shot.url !== undefined);
|
|
27
|
+
assert(shot.headers.example === 'value');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should ignore functions', () => {
|
|
31
|
+
const test = { func: () => null };
|
|
32
|
+
const shot = snapshot(test) as Record<string, any>;
|
|
33
|
+
assert(shot.func === undefined);
|
|
34
|
+
});
|
|
35
|
+
});
|
package/src/snapshot.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { isObjectLike } from 'lodash-es';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Allows special objects (Error, Headers, Set) to be included in JSON.stringify output
|
|
5
|
+
* functions are removed
|
|
6
|
+
*/
|
|
7
|
+
export function snapshot(i: unknown): any {
|
|
8
|
+
if (Array.isArray(i)) return i.map(snapshot);
|
|
9
|
+
if (typeof i === 'function') return undefined;
|
|
10
|
+
if (!isObjectLike(i)) return i;
|
|
11
|
+
|
|
12
|
+
let output: Record<string, any> = {};
|
|
13
|
+
// @ts-ignore If it has an 'entries' function, use that for looping (eg. Set, Map, Headers)
|
|
14
|
+
if (typeof i.entries === 'function') {
|
|
15
|
+
// @ts-ignore
|
|
16
|
+
for (let [k, v] of i.entries()) {
|
|
17
|
+
output[k] = snapshot(v);
|
|
18
|
+
}
|
|
19
|
+
return output;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Enumerability_and_ownership_of_properties
|
|
23
|
+
|
|
24
|
+
// Get Enumerable, inherited properties
|
|
25
|
+
const obj: Record<string, any> = i!;
|
|
26
|
+
for (let key in obj) {
|
|
27
|
+
output[key] = snapshot(obj[key]);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Get Non-enumberable, own properties
|
|
31
|
+
Object.getOwnPropertyNames(obj).forEach(key => {
|
|
32
|
+
output[key] = snapshot(obj[key]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return output;
|
|
36
|
+
}
|