@brianbuie/node-kit 0.5.0 → 0.5.2

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/Cache.d.ts CHANGED
@@ -10,6 +10,7 @@ export declare class Cache<T> {
10
10
  }): void;
11
11
  file: import("./File.js").File;
12
12
  get exists(): boolean;
13
+ delete(): void;
13
14
  get path(): string;
14
15
  };
15
16
  ttl: number;
package/dist/Dir.js CHANGED
@@ -1,5 +1,5 @@
1
- import fs from 'fs';
2
- import path from 'path';
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
3
  import sanitizeFilename from 'sanitize-filename';
4
4
  import { File } from './File.js';
5
5
  /**
package/dist/Fetcher.d.ts CHANGED
@@ -17,17 +17,26 @@ export type FetchOptions = RequestInit & {
17
17
  */
18
18
  export declare class Fetcher {
19
19
  defaultOptions: {
20
- timeout: number;
21
- retries: number;
22
- retryDelay: number;
23
- } & RequestInit & {
20
+ body?: BodyInit | null;
21
+ cache?: RequestCache;
22
+ credentials?: RequestCredentials;
23
+ headers?: (HeadersInit & Record<string, string>) | undefined;
24
+ integrity?: string;
25
+ keepalive?: boolean;
26
+ method?: string;
27
+ mode?: RequestMode;
28
+ priority?: RequestPriority;
29
+ redirect?: RequestRedirect;
30
+ referrer?: string;
31
+ referrerPolicy?: ReferrerPolicy;
32
+ signal?: AbortSignal | null;
33
+ window?: null;
24
34
  base?: string;
25
35
  query?: Query;
26
- headers?: Record<string, string>;
27
36
  data?: any;
28
- timeout?: number;
29
- retries?: number;
30
- retryDelay?: number;
37
+ timeout: number;
38
+ retries: number;
39
+ retryDelay: number;
31
40
  };
32
41
  constructor(opts?: FetchOptions);
33
42
  /**
@@ -35,6 +44,10 @@ export declare class Fetcher {
35
44
  * Also returns domain, to help with cookies
36
45
  */
37
46
  buildUrl(route: Route, opts?: FetchOptions): [URL, string];
47
+ /**
48
+ * Merges options to get headers. Useful when extending the Fetcher class to add custom auth.
49
+ */
50
+ buildHeaders(route: Route, opts?: FetchOptions): HeadersInit & Record<string, string>;
38
51
  /**
39
52
  * Builds request, merging defaultOptions and provided options
40
53
  * Includes Abort signal for timeout
package/dist/Fetcher.js CHANGED
@@ -8,12 +8,12 @@ import extractDomain from 'extract-domain';
8
8
  export class Fetcher {
9
9
  defaultOptions;
10
10
  constructor(opts = {}) {
11
- const defaultOptions = {
11
+ this.defaultOptions = {
12
12
  timeout: 60000,
13
13
  retries: 0,
14
14
  retryDelay: 3000,
15
+ ...opts,
15
16
  };
16
- this.defaultOptions = merge(defaultOptions, opts);
17
17
  }
18
18
  /**
19
19
  * Build URL with URLSearchParams if query is provided
@@ -39,6 +39,13 @@ export class Fetcher {
39
39
  const domain = extractDomain(url.href);
40
40
  return [url, domain];
41
41
  }
42
+ /**
43
+ * Merges options to get headers. Useful when extending the Fetcher class to add custom auth.
44
+ */
45
+ buildHeaders(route, opts = {}) {
46
+ const { headers } = merge({}, this.defaultOptions, opts);
47
+ return headers || {};
48
+ }
42
49
  /**
43
50
  * Builds request, merging defaultOptions and provided options
44
51
  * Includes Abort signal for timeout
@@ -46,8 +53,8 @@ export class Fetcher {
46
53
  buildRequest(route, opts = {}) {
47
54
  const mergedOptions = merge({}, this.defaultOptions, opts);
48
55
  const { query, data, timeout, retries, ...init } = mergedOptions;
56
+ init.headers = this.buildHeaders(route, mergedOptions);
49
57
  if (data) {
50
- init.headers = init.headers || {};
51
58
  init.headers['content-type'] = init.headers['content-type'] || 'application/json';
52
59
  init.method = init.method || 'POST';
53
60
  init.body = JSON.stringify(data);
@@ -70,7 +77,6 @@ export class Fetcher {
70
77
  let attempt = 0;
71
78
  while (attempt < maxAttempts) {
72
79
  attempt++;
73
- // Rebuild request on every attempt to reset AbortSignal.timeout
74
80
  const [req] = this.buildRequest(route, opts);
75
81
  const res = await fetch(req)
76
82
  .then((r) => {
package/dist/File.d.ts CHANGED
@@ -1,7 +1,13 @@
1
+ import * as fs from 'node:fs';
2
+ /**
3
+ * WARNING: API will change!
4
+ */
1
5
  export declare class File {
2
6
  path: string;
3
7
  constructor(filepath: string);
4
8
  get exists(): boolean;
9
+ createWriteStream(options?: Parameters<typeof fs.createWriteStream>[1]): fs.WriteStream;
10
+ delete(): void;
5
11
  read(): string | undefined;
6
12
  write(contents: string): void;
7
13
  /**
@@ -13,26 +19,35 @@ export declare class File {
13
19
  * @returns lines as strings, removes trailing '\n'
14
20
  */
15
21
  lines(): string[];
16
- static get Adaptor(): typeof FileAdaptor;
22
+ static get Adaptor(): typeof Adaptor;
17
23
  json<T>(contents?: T): JsonFile<T>;
18
24
  static get json(): typeof JsonFile;
19
- ndjson<T>(contents?: T): NdjsonFile<T>;
25
+ ndjson<T extends object>(lines?: T | T[]): NdjsonFile<T>;
20
26
  static get ndjson(): typeof NdjsonFile;
27
+ csv<T extends object>(rows?: T[], keys?: (keyof T)[]): Promise<CsvFile<T>>;
28
+ static get csv(): typeof CsvFile;
21
29
  }
22
- declare class FileAdaptor<T = string> {
30
+ declare class Adaptor<T = string> {
23
31
  file: File;
24
32
  constructor(filepath: string, contents?: T);
25
33
  get exists(): boolean;
34
+ delete(): void;
26
35
  get path(): string;
27
36
  }
28
- declare class JsonFile<T> extends FileAdaptor {
37
+ declare class JsonFile<T> extends Adaptor {
29
38
  constructor(filepath: string, contents?: T);
30
39
  read(): T | undefined;
31
40
  write(contents: T): void;
32
41
  }
33
- declare class NdjsonFile<T> extends FileAdaptor {
34
- constructor(filepath: string, contents?: T);
42
+ declare class NdjsonFile<T extends object> extends Adaptor {
43
+ constructor(filepath: string, lines?: T | T[]);
35
44
  append(lines: T | T[]): void;
36
45
  lines(): T[];
37
46
  }
47
+ type Key<T extends object> = keyof T;
48
+ declare class CsvFile<Row extends object> extends Adaptor {
49
+ constructor(filepath: string);
50
+ write(rows: Row[], keys?: Key<Row>[]): Promise<void>;
51
+ read(): Promise<Row[]>;
52
+ }
38
53
  export {};
package/dist/File.js CHANGED
@@ -1,6 +1,10 @@
1
- import fs from 'fs';
2
- import path from 'path';
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { writeToString, parseString } from 'fast-csv';
3
4
  import { snapshot } from './snapshot.js';
5
+ /**
6
+ * WARNING: API will change!
7
+ */
4
8
  export class File {
5
9
  path;
6
10
  constructor(filepath) {
@@ -9,6 +13,12 @@ export class File {
9
13
  get exists() {
10
14
  return fs.existsSync(this.path);
11
15
  }
16
+ createWriteStream(options = {}) {
17
+ return fs.createWriteStream(this.path, options);
18
+ }
19
+ delete() {
20
+ fs.rmSync(this.path, { force: true });
21
+ }
12
22
  read() {
13
23
  return this.exists ? fs.readFileSync(this.path, 'utf8') : undefined;
14
24
  }
@@ -34,7 +44,7 @@ export class File {
34
44
  return contents.slice(0, contents.length - 1);
35
45
  }
36
46
  static get Adaptor() {
37
- return FileAdaptor;
47
+ return Adaptor;
38
48
  }
39
49
  json(contents) {
40
50
  return new JsonFile(this.path, contents);
@@ -42,14 +52,23 @@ export class File {
42
52
  static get json() {
43
53
  return JsonFile;
44
54
  }
45
- ndjson(contents) {
46
- return new NdjsonFile(this.path, contents);
55
+ ndjson(lines) {
56
+ return new NdjsonFile(this.path, lines);
47
57
  }
48
58
  static get ndjson() {
49
59
  return NdjsonFile;
50
60
  }
61
+ async csv(rows, keys) {
62
+ const csvFile = new CsvFile(this.path);
63
+ if (rows)
64
+ await csvFile.write(rows, keys);
65
+ return csvFile;
66
+ }
67
+ static get csv() {
68
+ return CsvFile;
69
+ }
51
70
  }
52
- class FileAdaptor {
71
+ class Adaptor {
53
72
  file;
54
73
  constructor(filepath, contents) {
55
74
  this.file = new File(filepath);
@@ -63,11 +82,14 @@ class FileAdaptor {
63
82
  get exists() {
64
83
  return this.file.exists;
65
84
  }
85
+ delete() {
86
+ this.file.delete();
87
+ }
66
88
  get path() {
67
89
  return this.file.path;
68
90
  }
69
91
  }
70
- class JsonFile extends FileAdaptor {
92
+ class JsonFile extends Adaptor {
71
93
  constructor(filepath, contents) {
72
94
  super(filepath.endsWith('.json') ? filepath : filepath + '.json');
73
95
  if (contents)
@@ -81,11 +103,11 @@ class JsonFile extends FileAdaptor {
81
103
  this.file.write(JSON.stringify(snapshot(contents), null, 2));
82
104
  }
83
105
  }
84
- class NdjsonFile extends FileAdaptor {
85
- constructor(filepath, contents) {
106
+ class NdjsonFile extends Adaptor {
107
+ constructor(filepath, lines) {
86
108
  super(filepath.endsWith('.ndjson') ? filepath : filepath + '.ndjson');
87
- if (contents)
88
- this.append(contents);
109
+ if (lines)
110
+ this.append(lines);
89
111
  }
90
112
  append(lines) {
91
113
  this.file.append(Array.isArray(lines) ? lines.map((l) => JSON.stringify(snapshot(l))) : JSON.stringify(snapshot(lines)));
@@ -94,3 +116,53 @@ class NdjsonFile extends FileAdaptor {
94
116
  return this.file.lines().map((l) => JSON.parse(l));
95
117
  }
96
118
  }
119
+ class CsvFile extends Adaptor {
120
+ constructor(filepath) {
121
+ super(filepath.endsWith('.csv') ? filepath : filepath + '.csv');
122
+ }
123
+ async write(rows, keys) {
124
+ const headerSet = new Set();
125
+ if (keys) {
126
+ for (const key of keys)
127
+ headerSet.add(key);
128
+ }
129
+ else {
130
+ for (const row of rows) {
131
+ for (const key in row)
132
+ headerSet.add(key);
133
+ }
134
+ }
135
+ const headers = Array.from(headerSet);
136
+ const outRows = rows.map((row) => headers.map((key) => row[key]));
137
+ const contents = await writeToString([headers, ...outRows]);
138
+ this.file.write(contents);
139
+ }
140
+ async read() {
141
+ return new Promise((resolve, reject) => {
142
+ const parsed = [];
143
+ const content = this.file.read();
144
+ if (!content)
145
+ return resolve(parsed);
146
+ function parseVal(val) {
147
+ if (val.toLowerCase() === 'false')
148
+ return false;
149
+ if (val.toLowerCase() === 'true')
150
+ return true;
151
+ if (val.length === 0)
152
+ return null;
153
+ if (/^[\.0-9]+$/.test(val))
154
+ return Number(val);
155
+ return val;
156
+ }
157
+ parseString(content, { headers: true })
158
+ .on('error', (e) => reject(e))
159
+ .on('end', () => resolve(parsed))
160
+ .on('data', (raw) => {
161
+ parsed.push(Object.entries(raw).reduce((all, [key, val]) => ({
162
+ ...all,
163
+ [key]: parseVal(val),
164
+ }), {}));
165
+ });
166
+ });
167
+ }
168
+ }
package/dist/Log.js CHANGED
@@ -1,11 +1,10 @@
1
- import fs from 'fs';
2
- import { inspect } from 'util';
1
+ import { inspect } from 'node:util';
3
2
  import { isObjectLike } from 'lodash-es';
4
3
  import chalk from 'chalk';
5
4
  import { snapshot } from './snapshot.js';
6
- const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
7
5
  export class Log {
8
- static isTest = process.env.npm_package_name === packageJson.name && process.env.npm_lifecycle_event === 'test';
6
+ // Only silence logs when THIS package is running its own tests
7
+ static isTest = process.env.npm_package_name === '@brianbuie/node-kit' && process.env.npm_lifecycle_event === 'test';
9
8
  /**
10
9
  * Gcloud parses JSON in stdout
11
10
  */
@@ -1,4 +1,4 @@
1
- import fs from 'fs';
1
+ import * as fs from 'node:fs';
2
2
  import { merge } from 'lodash-es';
3
3
  import * as qt from 'quicktype-core';
4
4
  export class TypeWriter {
package/dist/_index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { Dir, TempDir, temp } from './Dir.js';
2
2
  export { Cache } from './Cache.js';
3
- export { Fetcher, type Route, Query, FetchOptions } from './Fetcher.js';
3
+ export { Fetcher, type Route, type Query, type FetchOptions } from './Fetcher.js';
4
4
  export { File } from './File.js';
5
5
  export { Jwt } from './Jwt.js';
6
6
  export { Log } from './Log.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brianbuie/node-kit",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "license": "ISC",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,7 +13,10 @@
13
13
  },
14
14
  "type": "module",
15
15
  "exports": {
16
- ".": "./dist/_index.js"
16
+ ".": {
17
+ "types": "./dist/_index.d.ts",
18
+ "default": "./dist/_index.js"
19
+ }
17
20
  },
18
21
  "files": [
19
22
  "src",
@@ -27,6 +30,7 @@
27
30
  "dependencies": {
28
31
  "chalk": "^5.6.2",
29
32
  "extract-domain": "^5.0.2",
33
+ "fast-csv": "^5.0.5",
30
34
  "jsonwebtoken": "^9.0.2",
31
35
  "lodash-es": "^4.17.21",
32
36
  "quicktype-core": "^23.2.6",
package/src/Dir.ts CHANGED
@@ -1,5 +1,5 @@
1
- import fs from 'fs';
2
- import path from 'path';
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
3
  import sanitizeFilename from 'sanitize-filename';
4
4
  import { File } from './File.js';
5
5
  import { snapshot } from './snapshot.js';
package/src/Fetcher.ts CHANGED
@@ -25,12 +25,12 @@ export class Fetcher {
25
25
  defaultOptions;
26
26
 
27
27
  constructor(opts: FetchOptions = {}) {
28
- const defaultOptions = {
28
+ this.defaultOptions = {
29
29
  timeout: 60000,
30
30
  retries: 0,
31
31
  retryDelay: 3000,
32
+ ...opts,
32
33
  };
33
- this.defaultOptions = merge(defaultOptions, opts);
34
34
  }
35
35
 
36
36
  /**
@@ -56,6 +56,14 @@ export class Fetcher {
56
56
  return [url, domain];
57
57
  }
58
58
 
59
+ /**
60
+ * Merges options to get headers. Useful when extending the Fetcher class to add custom auth.
61
+ */
62
+ buildHeaders(route: Route, opts: FetchOptions = {}) {
63
+ const { headers } = merge({}, this.defaultOptions, opts);
64
+ return headers || {};
65
+ }
66
+
59
67
  /**
60
68
  * Builds request, merging defaultOptions and provided options
61
69
  * Includes Abort signal for timeout
@@ -63,8 +71,8 @@ export class Fetcher {
63
71
  buildRequest(route: Route, opts: FetchOptions = {}): [Request, FetchOptions, string] {
64
72
  const mergedOptions = merge({}, this.defaultOptions, opts);
65
73
  const { query, data, timeout, retries, ...init } = mergedOptions;
74
+ init.headers = this.buildHeaders(route, mergedOptions);
66
75
  if (data) {
67
- init.headers = init.headers || {};
68
76
  init.headers['content-type'] = init.headers['content-type'] || 'application/json';
69
77
  init.method = init.method || 'POST';
70
78
  init.body = JSON.stringify(data);
@@ -88,7 +96,6 @@ export class Fetcher {
88
96
  let attempt = 0;
89
97
  while (attempt < maxAttempts) {
90
98
  attempt++;
91
- // Rebuild request on every attempt to reset AbortSignal.timeout
92
99
  const [req] = this.buildRequest(route, opts);
93
100
  const res = await fetch(req)
94
101
  .then((r) => {
package/src/File.test.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { describe, it } from 'node:test';
2
2
  import assert from 'node:assert';
3
- import { isEqual } from 'lodash-es';
4
3
  import { temp } from './Dir.js';
5
4
  import { File } from './File.js';
6
5
 
@@ -11,14 +10,14 @@ const thing = {
11
10
  a: 'string',
12
11
  b: 2,
13
12
  c: true,
14
- d: null,
13
+ d: false,
14
+ e: null,
15
15
  };
16
16
 
17
17
  describe('FileAdaptor', () => {
18
- it('Instances can be created', () => {
18
+ it('Creates instances', () => {
19
19
  const test1 = new File.Adaptor(testDir.filepath('test1.txt'));
20
20
  assert(test1.file.path.includes('test1.txt'));
21
-
22
21
  const base = 'test2';
23
22
  const eg1 = new File.json(testDir.filepath(base));
24
23
  const eg2 = testDir.file(base).json();
@@ -26,17 +25,26 @@ describe('FileAdaptor', () => {
26
25
  const eg3 = eg3File.json();
27
26
  assert(eg1.path === eg2.path && eg2.path === eg3.path);
28
27
  });
28
+
29
+ it('Deletes files', () => {
30
+ const test = testDir.file('delete-test.txt');
31
+ test.write('test');
32
+ assert.equal(test.read(), 'test');
33
+ test.delete();
34
+ assert.equal(test.exists, false);
35
+ });
29
36
  });
30
37
 
31
38
  describe('File.ndjson', () => {
32
39
  it('Appends new lines correctly', () => {
33
- const file = testDir.file('empty-lines').ndjson();
40
+ const file = testDir.file('appends-lines').ndjson();
41
+ file.delete();
34
42
  file.append([thing, thing]);
35
43
  assert(file.lines().length === 2);
36
44
  file.append(thing);
37
45
  assert(file.lines().length === 3);
38
46
  file.lines().forEach((line) => {
39
- assert(isEqual(line, thing));
47
+ assert.deepStrictEqual(line, thing);
40
48
  });
41
49
  });
42
50
 
@@ -52,9 +60,9 @@ describe('File.ndjson', () => {
52
60
  describe('File.json', () => {
53
61
  it('Saves data as json', () => {
54
62
  const file = testDir.file('jsonfile-data').json(thing);
55
- assert(isEqual(file.read(), thing));
63
+ assert.deepStrictEqual(file.read(), thing);
56
64
  file.write(thing);
57
- assert(isEqual(file.read(), thing));
65
+ assert.deepStrictEqual(file.read(), thing);
58
66
  });
59
67
 
60
68
  it('Does not create file when reading', () => {
@@ -64,3 +72,14 @@ describe('File.json', () => {
64
72
  assert(!file.exists);
65
73
  });
66
74
  });
75
+
76
+ describe('File.csv', () => {
77
+ it('Saves data as csv', async () => {
78
+ const things = [thing, thing, thing];
79
+ const file = await testDir.file('csv-data').csv(things);
80
+ const parsed = await file.read();
81
+ parsed.forEach((row) => {
82
+ assert.deepEqual(row, thing);
83
+ });
84
+ });
85
+ });
package/src/File.ts CHANGED
@@ -1,7 +1,11 @@
1
- import fs from 'fs';
2
- import path from 'path';
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { writeToString, parseString } from 'fast-csv';
3
4
  import { snapshot } from './snapshot.js';
4
5
 
6
+ /**
7
+ * WARNING: API will change!
8
+ */
5
9
  export class File {
6
10
  path;
7
11
 
@@ -13,6 +17,14 @@ export class File {
13
17
  return fs.existsSync(this.path);
14
18
  }
15
19
 
20
+ createWriteStream(options: Parameters<typeof fs.createWriteStream>[1] = {}) {
21
+ return fs.createWriteStream(this.path, options);
22
+ }
23
+
24
+ delete() {
25
+ fs.rmSync(this.path, { force: true });
26
+ }
27
+
16
28
  read() {
17
29
  return this.exists ? fs.readFileSync(this.path, 'utf8') : undefined;
18
30
  }
@@ -41,7 +53,7 @@ export class File {
41
53
  }
42
54
 
43
55
  static get Adaptor() {
44
- return FileAdaptor;
56
+ return Adaptor;
45
57
  }
46
58
 
47
59
  json<T>(contents?: T) {
@@ -52,16 +64,26 @@ export class File {
52
64
  return JsonFile;
53
65
  }
54
66
 
55
- ndjson<T>(contents?: T) {
56
- return new NdjsonFile<T>(this.path, contents);
67
+ ndjson<T extends object>(lines?: T | T[]) {
68
+ return new NdjsonFile<T>(this.path, lines);
57
69
  }
58
70
 
59
71
  static get ndjson() {
60
72
  return NdjsonFile;
61
73
  }
74
+
75
+ async csv<T extends object>(rows?: T[], keys?: (keyof T)[]) {
76
+ const csvFile = new CsvFile<T>(this.path);
77
+ if (rows) await csvFile.write(rows, keys);
78
+ return csvFile;
79
+ }
80
+
81
+ static get csv() {
82
+ return CsvFile;
83
+ }
62
84
  }
63
85
 
64
- class FileAdaptor<T = string> {
86
+ class Adaptor<T = string> {
65
87
  file;
66
88
 
67
89
  constructor(filepath: string, contents?: T) {
@@ -78,12 +100,16 @@ class FileAdaptor<T = string> {
78
100
  return this.file.exists;
79
101
  }
80
102
 
103
+ delete() {
104
+ this.file.delete();
105
+ }
106
+
81
107
  get path() {
82
108
  return this.file.path;
83
109
  }
84
110
  }
85
111
 
86
- class JsonFile<T> extends FileAdaptor {
112
+ class JsonFile<T> extends Adaptor {
87
113
  constructor(filepath: string, contents?: T) {
88
114
  super(filepath.endsWith('.json') ? filepath : filepath + '.json');
89
115
  if (contents) this.write(contents);
@@ -99,10 +125,10 @@ class JsonFile<T> extends FileAdaptor {
99
125
  }
100
126
  }
101
127
 
102
- class NdjsonFile<T> extends FileAdaptor {
103
- constructor(filepath: string, contents?: T) {
128
+ class NdjsonFile<T extends object> extends Adaptor {
129
+ constructor(filepath: string, lines?: T | T[]) {
104
130
  super(filepath.endsWith('.ndjson') ? filepath : filepath + '.ndjson');
105
- if (contents) this.append(contents);
131
+ if (lines) this.append(lines);
106
132
  }
107
133
 
108
134
  append(lines: T | T[]) {
@@ -115,3 +141,55 @@ class NdjsonFile<T> extends FileAdaptor {
115
141
  return this.file.lines().map((l) => JSON.parse(l) as T);
116
142
  }
117
143
  }
144
+
145
+ type Key<T extends object> = keyof T;
146
+
147
+ class CsvFile<Row extends object> extends Adaptor {
148
+ constructor(filepath: string) {
149
+ super(filepath.endsWith('.csv') ? filepath : filepath + '.csv');
150
+ }
151
+
152
+ async write(rows: Row[], keys?: Key<Row>[]) {
153
+ const headerSet = new Set<Key<Row>>();
154
+ if (keys) {
155
+ for (const key of keys) headerSet.add(key);
156
+ } else {
157
+ for (const row of rows) {
158
+ for (const key in row) headerSet.add(key);
159
+ }
160
+ }
161
+ const headers = Array.from(headerSet);
162
+ const outRows = rows.map((row) => headers.map((key) => row[key]));
163
+ const contents = await writeToString([headers, ...outRows]);
164
+ this.file.write(contents);
165
+ }
166
+
167
+ async read() {
168
+ return new Promise<Row[]>((resolve, reject) => {
169
+ const parsed: Row[] = [];
170
+ const content = this.file.read();
171
+ if (!content) return resolve(parsed);
172
+ function parseVal(val: string) {
173
+ if (val.toLowerCase() === 'false') return false;
174
+ if (val.toLowerCase() === 'true') return true;
175
+ if (val.length === 0) return null;
176
+ if (/^[\.0-9]+$/.test(val)) return Number(val);
177
+ return val;
178
+ }
179
+ parseString(content, { headers: true })
180
+ .on('error', (e) => reject(e))
181
+ .on('end', () => resolve(parsed))
182
+ .on('data', (raw: Record<Key<Row>, string>) => {
183
+ parsed.push(
184
+ Object.entries(raw).reduce(
185
+ (all, [key, val]) => ({
186
+ ...all,
187
+ [key]: parseVal(val as string),
188
+ }),
189
+ {} as Row
190
+ )
191
+ );
192
+ });
193
+ });
194
+ }
195
+ }
package/src/Log.ts CHANGED
@@ -1,11 +1,8 @@
1
- import fs from 'fs';
2
- import { inspect } from 'util';
1
+ import { inspect } from 'node:util';
3
2
  import { isObjectLike } from 'lodash-es';
4
3
  import chalk, { type ChalkInstance } from 'chalk';
5
4
  import { snapshot } from './snapshot.js';
6
5
 
7
- const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8'));
8
-
9
6
  type Severity = 'DEFAULT' | 'DEBUG' | 'INFO' | 'NOTICE' | 'WARNING' | 'ERROR' | 'CRITICAL' | 'ALERT' | 'EMERGENCY';
10
7
 
11
8
  type Options = {
@@ -20,7 +17,8 @@ type Entry = {
20
17
  };
21
18
 
22
19
  export class Log {
23
- static isTest = process.env.npm_package_name === packageJson.name && process.env.npm_lifecycle_event === 'test';
20
+ // Only silence logs when THIS package is running its own tests
21
+ static isTest = process.env.npm_package_name === '@brianbuie/node-kit' && process.env.npm_lifecycle_event === 'test';
24
22
 
25
23
  /**
26
24
  * Gcloud parses JSON in stdout
package/src/TypeWriter.ts CHANGED
@@ -1,4 +1,4 @@
1
- import fs from 'fs';
1
+ import * as fs from 'node:fs';
2
2
  import { merge } from 'lodash-es';
3
3
  import * as qt from 'quicktype-core';
4
4
 
package/src/_index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { Dir, TempDir, temp } from './Dir.js';
2
2
  export { Cache } from './Cache.js';
3
- export { Fetcher, type Route, Query, FetchOptions } from './Fetcher.js';
3
+ export { Fetcher, type Route, type Query, type FetchOptions } from './Fetcher.js';
4
4
  export { File } from './File.js';
5
5
  export { Jwt } from './Jwt.js';
6
6
  export { Log } from './Log.js';