@brianbuie/node-kit 0.12.5 → 0.14.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/src/Dir.ts CHANGED
@@ -3,52 +3,82 @@ import * as path from 'node:path';
3
3
  import sanitizeFilename from 'sanitize-filename';
4
4
  import { File } from './File.ts';
5
5
 
6
+ export type DirOptions = {
7
+ temp?: boolean;
8
+ };
9
+
6
10
  /**
7
11
  * Reference to a specific directory with methods to create and list files.
8
- * Default path: '.'
9
- * > Created on file system the first time .path is read or any methods are used
12
+ * @param inputPath
13
+ * The path of the directory, created on file system the first time `.path` is read or any methods are used
14
+ * @param options
15
+ * include `{ temp: true }` to enable the `.clear()` method
10
16
  */
11
17
  export class Dir {
12
18
  #inputPath;
13
19
  #resolved?: string;
20
+ isTemp;
14
21
 
15
22
  /**
16
23
  * @param path can be relative to workspace or absolute
17
24
  */
18
- constructor(inputPath = '.') {
25
+ constructor(inputPath: string, options: DirOptions = {}) {
19
26
  this.#inputPath = inputPath;
27
+ this.isTemp = Boolean(options.temp);
28
+ }
29
+
30
+ /**
31
+ * The path of the directory, which might not exist yet.
32
+ */
33
+ get pathUnsafe() {
34
+ return this.#resolved || path.resolve(this.#inputPath);
20
35
  }
21
36
 
22
37
  /**
23
38
  * The path of this Dir instance. Created on file system the first time this property is read/used.
39
+ * Safe to use the directory immediately, without calling mkdir separately.
24
40
  */
25
41
  get path() {
42
+ // avoids calling mkdir every time path is read
26
43
  if (!this.#resolved) {
27
- this.#resolved = path.resolve(this.#inputPath);
44
+ this.#resolved = this.pathUnsafe;
28
45
  fs.mkdirSync(this.#resolved, { recursive: true });
29
46
  }
30
47
  return this.#resolved;
31
48
  }
32
49
 
50
+ /**
51
+ * The last segment in the path. Doesn't read this.path, to avoid creating directory on file system before it's needed.
52
+ * @example
53
+ * const example = new Dir('/path/to/folder');
54
+ * console.log(example.name); // "folder"
55
+ */
56
+ get name() {
57
+ return this.pathUnsafe.split(path.sep).at(-1)!;
58
+ }
59
+
33
60
  /**
34
61
  * Create a new Dir inside the current Dir
35
- * @param subPath joined with parent Dir's path to make new Dir
62
+ * @param subPath
63
+ * joined with parent Dir's path to make new Dir
64
+ * @param options
65
+ * include `{ temp: true }` to enable the `.clear()` method. If current Dir is temporary, child directories will also be temporary.
36
66
  * @example
37
67
  * const folder = new Dir('example');
38
68
  * // folder.path = '/path/to/cwd/example'
39
69
  * const child = folder.dir('path/to/dir');
40
70
  * // child.path = '/path/to/cwd/example/path/to/dir'
41
71
  */
42
- dir(subPath: string) {
43
- return new Dir(path.join(this.path, subPath));
72
+ dir(subPath: string, options: DirOptions = { temp: this.isTemp }) {
73
+ return new (this.constructor as typeof Dir)(path.join(this.path, subPath), options) as this;
44
74
  }
45
75
 
46
76
  /**
47
- * Creates a new TempDir inside current Dir
77
+ * Creates a new temp directory inside current Dir
48
78
  * @param subPath joined with parent Dir's path to make new TempDir
49
79
  */
50
80
  tempDir(subPath: string) {
51
- return new TempDir(path.join(this.path, subPath));
81
+ return this.dir(subPath, { temp: true });
52
82
  }
53
83
 
54
84
  sanitize(filename: string) {
@@ -67,28 +97,95 @@ export class Dir {
67
97
  return path.resolve(this.path, this.sanitize(base));
68
98
  }
69
99
 
100
+ /**
101
+ * Create a new file in this directory
102
+ */
70
103
  file(base: string) {
71
104
  return new File(this.filepath(base));
72
105
  }
73
106
 
107
+ /**
108
+ * All files and subdirectories in in this directory, returned as Dir and File instances
109
+ */
110
+ get contents(): (Dir | File)[] {
111
+ return fs
112
+ .readdirSync(this.path)
113
+ .map(name => (fs.statSync(path.join(this.path, name)).isDirectory() ? this.dir(name) : this.file(name)));
114
+ }
115
+
116
+ /**
117
+ * All subdirectories in this directory
118
+ */
119
+ get dirs() {
120
+ return this.contents.filter(f => f instanceof Dir);
121
+ }
122
+
123
+ /**
124
+ * All files in this directory
125
+ */
74
126
  get files() {
75
- return fs.readdirSync(this.path).map(filename => this.file(filename));
127
+ return this.contents.filter(f => f instanceof File);
76
128
  }
77
- }
78
129
 
79
- /**
80
- * Extends Dir class with method to `clear()` contents.
81
- * Default path: `./.temp`
82
- */
83
- export class TempDir extends Dir {
84
- constructor(inputPath = `./.temp`) {
85
- super(inputPath);
130
+ /**
131
+ * All files with MIME type that includes "video"
132
+ */
133
+ get videos() {
134
+ return this.files.filter(f => f.type?.includes('video'));
135
+ }
136
+
137
+ /**
138
+ * All files with MIME type that includes "image"
139
+ */
140
+ get images() {
141
+ return this.files.filter(f => f.type?.includes('image'));
142
+ }
143
+
144
+ /**
145
+ * All files with ext ".json"
146
+ * @example
147
+ * // Directory of json files with the same shape
148
+ * const dataFiles = dataDir.jsonFiles.map(f => f.json<ExampleType>());
149
+ * // dataFiles: FileTypeJson<ExampleType>[]
150
+ */
151
+ get jsonFiles() {
152
+ return this.files.filter(f => f.ext === '.json');
153
+ }
154
+
155
+ /**
156
+ * All files with ext ".ndjson"
157
+ * @example
158
+ * // Directory of ndjson files with the same shape
159
+ * const dataFiles = dataDir.ndjsonFiles.map(f => f.ndjson<ExampleType>());
160
+ * // dataFiles: FileTypeNdjson<ExampleType>[]
161
+ */
162
+ get ndjsonFiles() {
163
+ return this.files.filter(f => f.ext === '.ndjson');
164
+ }
165
+
166
+ /**
167
+ * All files with ext ".csv"
168
+ * @example
169
+ * // Directory of csv files with the same shape
170
+ * const dataFiles = dataDir.csvFile.map(f => f.csv<ExampleType>());
171
+ * // dataFiles: FileTypeCsv<ExampleType>[]
172
+ */
173
+ get csvFiles() {
174
+ return this.files.filter(f => f.ext === '.csv');
175
+ }
176
+
177
+ /**
178
+ * All files with ext ".txt"
179
+ */
180
+ get textFiles() {
181
+ return this.files.filter(f => f.ext === '.txt');
86
182
  }
87
183
 
88
184
  /**
89
- * > ⚠️ Warning! This deletes the directory!
185
+ * Deletes the contents of the directory. Only allowed if created with `temp` option set to `true` (or created with `dir.tempDir` method).
90
186
  */
91
187
  clear() {
188
+ if (!this.isTemp) throw new Error('Dir is not temporary');
92
189
  fs.rmSync(this.path, { recursive: true, force: true });
93
190
  fs.mkdirSync(this.path, { recursive: true });
94
191
  }
package/src/Fetcher.ts CHANGED
@@ -43,7 +43,7 @@ export class Fetcher {
43
43
  Object.entries(mergedOptions.query || {}).forEach(([key, val]) => {
44
44
  if (val === undefined) return;
45
45
  if (Array.isArray(val)) {
46
- val.forEach((v) => {
46
+ val.forEach(v => {
47
47
  params.push([key, `${v}`]);
48
48
  });
49
49
  } else {
@@ -98,15 +98,15 @@ export class Fetcher {
98
98
  attempt++;
99
99
  const [req] = this.buildRequest(route, opts);
100
100
  const res = await fetch(req)
101
- .then((r) => {
101
+ .then(r => {
102
102
  if (!r.ok) throw new Error(r.statusText);
103
103
  return r;
104
104
  })
105
- .catch(async (error) => {
105
+ .catch(async error => {
106
106
  if (attempt < maxAttempts) {
107
107
  const wait = attempt * 3000;
108
108
  console.warn(`${req.method} ${req.url} (attempt ${attempt} of ${maxAttempts})`, error);
109
- await new Promise((resolve) => setTimeout(resolve, wait));
109
+ await new Promise(resolve => setTimeout(resolve, wait));
110
110
  } else {
111
111
  throw new Error(error);
112
112
  }
package/src/File.test.ts CHANGED
@@ -17,7 +17,7 @@ const thing = {
17
17
  describe('File', () => {
18
18
  it('Handles request body as stream input', async () => {
19
19
  const img = testDir.file('image.jpg');
20
- await fetch('https://testingbot.com/free-online-tools/random-avatar/300').then((res) => {
20
+ await fetch('https://testingbot.com/free-online-tools/random-avatar/300').then(res => {
21
21
  if (!res.body) throw new Error('No response body');
22
22
  return img.write(res.body);
23
23
  });
@@ -74,7 +74,7 @@ describe('FileTypeNdjson', () => {
74
74
  assert(file.lines().length === 2);
75
75
  file.append(thing);
76
76
  assert(file.lines().length === 3);
77
- file.lines().forEach((line) => {
77
+ file.lines().forEach(line => {
78
78
  assert.deepStrictEqual(line, thing);
79
79
  });
80
80
  });
@@ -93,7 +93,7 @@ describe('FileTypeCsv', () => {
93
93
  const things = [thing, thing, thing];
94
94
  const file = await testDir.file('csv-data').csv(things);
95
95
  const parsed = await file.read();
96
- parsed.forEach((row) => {
96
+ parsed.forEach(row => {
97
97
  assert.deepEqual(row, thing);
98
98
  });
99
99
  });
package/src/Log.ts CHANGED
@@ -4,6 +4,7 @@ import chalk, { type ChalkInstance } from 'chalk';
4
4
  import { snapshot } from './snapshot.ts';
5
5
  import { Format } from './Format.ts';
6
6
 
7
+ // https://docs.cloud.google.com/logging/docs/reference/v2/rest/v2/LogEntry#logseverity
7
8
  type Severity = 'DEFAULT' | 'DEBUG' | 'INFO' | 'NOTICE' | 'WARNING' | 'ERROR' | 'CRITICAL' | 'ALERT' | 'EMERGENCY';
8
9
 
9
10
  type Options = {
@@ -82,25 +83,43 @@ export class Log {
82
83
  }
83
84
 
84
85
  /**
85
- * Logs error details before throwing
86
+ * Events that require action or attention immediately
87
+ */
88
+ static alert(...input: unknown[]) {
89
+ return this.#log({ severity: 'ALERT', color: chalk.bgRed }, ...input);
90
+ }
91
+
92
+ /**
93
+ * Events that cause problems
86
94
  */
87
95
  static error(...input: unknown[]) {
88
- const { message } = this.#log({ severity: 'ERROR', color: chalk.red }, ...input);
89
- throw new Error(message);
96
+ return this.#log({ severity: 'ERROR', color: chalk.red }, ...input);
90
97
  }
91
98
 
99
+ /**
100
+ * Events that might cause problems
101
+ */
92
102
  static warn(...input: unknown[]) {
93
103
  return this.#log({ severity: 'WARNING', color: chalk.yellow }, ...input);
94
104
  }
95
105
 
106
+ /**
107
+ * Normal but significant events, such as start up, shut down, or a configuration change
108
+ */
96
109
  static notice(...input: unknown[]) {
97
110
  return this.#log({ severity: 'NOTICE', color: chalk.cyan }, ...input);
98
111
  }
99
112
 
113
+ /**
114
+ * Routine information, such as ongoing status or performance
115
+ */
100
116
  static info(...input: unknown[]) {
101
117
  return this.#log({ severity: 'INFO', color: chalk.white }, ...input);
102
118
  }
103
119
 
120
+ /**
121
+ * Debug or trace information
122
+ */
104
123
  static debug(...input: unknown[]) {
105
124
  return this.#log({ severity: 'DEBUG', color: chalk.gray }, ...input);
106
125
  }
package/src/TypeWriter.ts CHANGED
@@ -25,7 +25,7 @@ export class TypeWriter {
25
25
  }
26
26
 
27
27
  async addMember(name: string, _samples: any[]) {
28
- const samples = _samples.map((s) => (typeof s === 'string' ? s : JSON.stringify(s)));
28
+ const samples = _samples.map(s => (typeof s === 'string' ? s : JSON.stringify(s)));
29
29
  await this.input.addSource({ name, samples });
30
30
  }
31
31
 
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { Dir, TempDir, temp } from './Dir.ts';
1
+ export { Dir, type DirOptions, temp, cwd } from './Dir.ts';
2
2
  export { Cache } from './Cache.ts';
3
3
  export { Fetcher, type Route, type Query, type FetchOptions } from './Fetcher.ts';
4
4
  export { File, FileType, FileTypeJson, FileTypeNdjson, FileTypeCsv } from './File.ts';
package/src/snapshot.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import { isObjectLike } from 'lodash-es';
2
2
 
3
3
  /**
4
- * Allows special objects (Error, Headers, Set) to be included in JSON.stringify output
5
- * functions are removed
4
+ * Allows special objects (Error, Headers, Set) to be included in JSON.stringify output.
5
+ * Functions are removed
6
6
  */
7
7
  export function snapshot(i: unknown, max = 50, depth = 0): any {
8
8
  if (Array.isArray(i)) {
9
9
  if (depth === max) return [];
10
- return i.map((c) => snapshot(c, max, depth + 1));
10
+ return i.map(c => snapshot(c, max, depth + 1));
11
11
  }
12
12
  if (typeof i === 'function') return undefined;
13
13
  if (!isObjectLike(i)) return i;
@@ -32,7 +32,7 @@ export function snapshot(i: unknown, max = 50, depth = 0): any {
32
32
  }
33
33
 
34
34
  // Get Non-enumberable, own properties
35
- Object.getOwnPropertyNames(obj).forEach((key) => {
35
+ Object.getOwnPropertyNames(obj).forEach(key => {
36
36
  output[key] = snapshot(obj[key], max, depth + 1);
37
37
  });
38
38
 
package/src/timeout.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  export async function timeout(ms: number) {
2
- return new Promise((resolve) => {
2
+ return new Promise(resolve => {
3
3
  setTimeout(resolve, ms);
4
4
  });
5
5
  }
package/tsconfig.json CHANGED
@@ -3,6 +3,7 @@
3
3
  "target": "esnext",
4
4
  "module": "nodenext",
5
5
  "moduleResolution": "nodenext",
6
+ "forceConsistentCasingInFileNames": true,
6
7
  "strict": true,
7
8
  "skipLibCheck": true,
8
9
  "allowImportingTsExtensions": true,
@@ -1,6 +0,0 @@
1
- export default {
2
- printWidth: 120,
3
- singleQuote: true,
4
- quoteProps: 'consistent',
5
- arrowParens: 'avoid',
6
- };