@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 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
@@ -1 +1,4 @@
1
1
  export { Fetcher, type Route, Query, FetchOptions } from './Fetcher.js';
2
+ export { Jwt } from './Jwt.js';
3
+ export { snapshot } from './snapshot.js';
4
+ export { Log } from './Log.js';
package/.dist/_index.js CHANGED
@@ -1 +1,4 @@
1
1
  export { Fetcher } from './Fetcher.js';
2
+ export { Jwt } from './Jwt.js';
3
+ export { snapshot } from './snapshot.js';
4
+ export { Log } from './Log.js';
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Allows special objects (Error, Headers, Set) to be included in JSON.stringify output
3
+ * functions are removed
4
+ */
5
+ export declare function snapshot(i: unknown): any;
@@ -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 • ![NPM Version](https://img.shields.io/npm/v/%40brianbuie%2Fnode-kit)
2
2
 
3
3
  Basic tools for quick node.js projects
4
4
 
5
- ## Using in other projects
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
- - Commits changes
26
- - Tags commit with new version
27
- - Pushes commit and tags to github
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.0.4",
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": {
@@ -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
+ }
@@ -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
@@ -1 +1,4 @@
1
1
  export { Fetcher, type Route, Query, FetchOptions } from './Fetcher.js';
2
+ export { Jwt } from './Jwt.js';
3
+ export { snapshot } from './snapshot.js';
4
+ export { Log } from './Log.js';
@@ -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
+ });
@@ -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
+ }