@brianbuie/node-kit 0.0.4 → 0.1.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/.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/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/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brianbuie/node-kit",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
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
|
+
}
|