@brianbuie/node-kit 0.15.1 → 0.16.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/README.md +321 -210
- package/dist/index.d.mts +64 -520
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +202 -150
- package/dist/index.mjs.map +1 -1
- package/package.json +8 -6
- package/src/Cache.ts +4 -3
- package/src/Cmd.ts +65 -0
- package/src/Dir.ts +19 -19
- package/src/Fetcher.ts +2 -2
- package/src/File.ts +117 -57
- package/src/Format.ts +5 -5
- package/src/Log.test.ts +16 -6
- package/src/Log.ts +58 -98
- package/src/TypeWriter.ts +15 -7
- package/src/snapshot.ts +3 -2
- package/src/timeout.ts +1 -1
package/src/File.ts
CHANGED
|
@@ -4,19 +4,20 @@ import { Readable } from 'node:stream';
|
|
|
4
4
|
import { finished } from 'node:stream/promises';
|
|
5
5
|
import mime from 'mime-types';
|
|
6
6
|
import { writeToStream, parseStream } from 'fast-csv';
|
|
7
|
-
import {
|
|
7
|
+
import { default as probeImageSize, type ProbeResult } from 'probe-image-size';
|
|
8
|
+
import { Cmd } from './Cmd.ts';
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Represents a file on the file system. If the file doesn't exist, it is created the first time it is written to.
|
|
11
12
|
*/
|
|
12
13
|
export class File {
|
|
13
|
-
path;
|
|
14
|
-
root;
|
|
15
|
-
dir;
|
|
16
|
-
base;
|
|
17
|
-
name;
|
|
18
|
-
ext;
|
|
19
|
-
type;
|
|
14
|
+
path: string;
|
|
15
|
+
root: string;
|
|
16
|
+
dir: string;
|
|
17
|
+
base: string;
|
|
18
|
+
name: string;
|
|
19
|
+
ext: string;
|
|
20
|
+
type?: string;
|
|
20
21
|
|
|
21
22
|
constructor(filepath: string) {
|
|
22
23
|
this.path = this.#resolve(filepath);
|
|
@@ -29,7 +30,7 @@ export class File {
|
|
|
29
30
|
this.type = mime.lookup(ext) || undefined;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
#resolve(filepath: string) {
|
|
33
|
+
#resolve(filepath: string): string {
|
|
33
34
|
if (filepath[0] === '~') {
|
|
34
35
|
if (process.env.HOME) {
|
|
35
36
|
return path.join(process.env.HOME, filepath.slice(1));
|
|
@@ -40,7 +41,7 @@ export class File {
|
|
|
40
41
|
return path.resolve(filepath);
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
get exists() {
|
|
44
|
+
get exists(): boolean {
|
|
44
45
|
return fs.existsSync(this.path);
|
|
45
46
|
}
|
|
46
47
|
|
|
@@ -51,35 +52,35 @@ export class File {
|
|
|
51
52
|
/**
|
|
52
53
|
* Deletes the file if it exists
|
|
53
54
|
*/
|
|
54
|
-
delete() {
|
|
55
|
+
delete(): void {
|
|
55
56
|
fs.rmSync(this.path, { force: true });
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
/**
|
|
59
60
|
* @returns the contents of the file as a string, or undefined if the file doesn't exist
|
|
60
61
|
*/
|
|
61
|
-
read() {
|
|
62
|
+
read(): string | undefined {
|
|
62
63
|
return this.exists ? fs.readFileSync(this.path, 'utf8') : undefined;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
/**
|
|
66
67
|
* @returns lines as strings, removes trailing '\n'
|
|
67
68
|
*/
|
|
68
|
-
lines() {
|
|
69
|
+
lines(): string[] {
|
|
69
70
|
const contents = (this.read() || '').split('\n');
|
|
70
71
|
return contents.at(-1)?.length ? contents : contents.slice(0, contents.length - 1);
|
|
71
72
|
}
|
|
72
73
|
|
|
73
|
-
get readStream() {
|
|
74
|
+
get readStream(): fs.ReadStream | Readable {
|
|
74
75
|
return this.exists ? fs.createReadStream(this.path) : Readable.from([]);
|
|
75
76
|
}
|
|
76
77
|
|
|
77
|
-
get writeStream() {
|
|
78
|
+
get writeStream(): fs.WriteStream {
|
|
78
79
|
fs.mkdirSync(this.dir, { recursive: true });
|
|
79
80
|
return fs.createWriteStream(this.path);
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
write(contents: string | ReadableStream) {
|
|
83
|
+
write(contents: string | ReadableStream): void | Promise<void> {
|
|
83
84
|
fs.mkdirSync(this.dir, { recursive: true });
|
|
84
85
|
if (typeof contents === 'string') return fs.writeFileSync(this.path, contents);
|
|
85
86
|
if (contents instanceof ReadableStream) return finished(Readable.from(contents).pipe(this.writeStream));
|
|
@@ -90,7 +91,7 @@ export class File {
|
|
|
90
91
|
* creates file if it doesn't exist, appends string or array of strings as new lines.
|
|
91
92
|
* File always ends with '\n', so contents don't need to be read before appending
|
|
92
93
|
*/
|
|
93
|
-
append(lines: string | string[]) {
|
|
94
|
+
append(lines: string | string[]): void {
|
|
94
95
|
if (!this.exists) this.write('');
|
|
95
96
|
const contents = Array.isArray(lines) ? lines.join('\n') : lines;
|
|
96
97
|
fs.appendFileSync(this.path, contents + '\n');
|
|
@@ -106,7 +107,7 @@ export class File {
|
|
|
106
107
|
* const file = new File('./data').json<object>({ key: 'val' }); // FileTypeJson<object>
|
|
107
108
|
* file.write({ something: 'else' }) // ✅ data is typed as object
|
|
108
109
|
*/
|
|
109
|
-
json<T>(contents?: T) {
|
|
110
|
+
json<T>(contents?: T): FileTypeJson<T> {
|
|
110
111
|
return new FileTypeJson<T>(this.path, contents);
|
|
111
112
|
}
|
|
112
113
|
|
|
@@ -114,14 +115,14 @@ export class File {
|
|
|
114
115
|
* @example
|
|
115
116
|
* const file = new File.json('data.json', { key: 'val' }); // FileTypeJson<{ key: string; }>
|
|
116
117
|
*/
|
|
117
|
-
static get json() {
|
|
118
|
+
static get json(): typeof FileTypeJson {
|
|
118
119
|
return FileTypeJson;
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
/**
|
|
122
123
|
* @returns FileTypeNdjson adaptor for current File, adds '.ndjson' extension if not present.
|
|
123
124
|
*/
|
|
124
|
-
ndjson<T extends object>(lines?: T | T[]) {
|
|
125
|
+
ndjson<T extends object>(lines?: T | T[]): FileTypeNdjson<T> {
|
|
125
126
|
return new FileTypeNdjson<T>(this.path, lines);
|
|
126
127
|
}
|
|
127
128
|
/**
|
|
@@ -129,7 +130,7 @@ export class File {
|
|
|
129
130
|
* const file = new File.ndjson('log', { key: 'val' }); // FileTypeNdjson<{ key: string; }>
|
|
130
131
|
* console.log(file.path) // /path/to/cwd/log.ndjson
|
|
131
132
|
*/
|
|
132
|
-
static get ndjson() {
|
|
133
|
+
static get ndjson(): typeof FileTypeNdjson {
|
|
133
134
|
return FileTypeNdjson;
|
|
134
135
|
}
|
|
135
136
|
|
|
@@ -141,80 +142,100 @@ export class File {
|
|
|
141
142
|
* await file.write({ col: 'val' }); // ✅ Writes one row
|
|
142
143
|
* await file.write([{ col: 'val2' }, { col: 'val3' }]); // ✅ Writes multiple rows
|
|
143
144
|
*/
|
|
144
|
-
async csv<T extends object>(rows?: T[],
|
|
145
|
-
const csvFile = new FileTypeCsv<T>(this.path);
|
|
146
|
-
if (rows) await csvFile.write(rows
|
|
145
|
+
async csv<T extends object>(rows?: T[], options?: FileTypeCsvOptions<T>): Promise<FileTypeCsv<T>> {
|
|
146
|
+
const csvFile = new FileTypeCsv<T>(this.path, options);
|
|
147
|
+
if (rows) await csvFile.write(rows);
|
|
147
148
|
return csvFile;
|
|
148
149
|
}
|
|
149
150
|
|
|
150
|
-
static get csv() {
|
|
151
|
+
static get csv(): typeof FileTypeCsv {
|
|
151
152
|
return FileTypeCsv;
|
|
152
153
|
}
|
|
154
|
+
|
|
155
|
+
async image(content?: ReadableStream) {
|
|
156
|
+
const imageFile = new FileTypeImage(this.path);
|
|
157
|
+
if (content) await imageFile.file.write(content);
|
|
158
|
+
return imageFile;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
static get image(): typeof FileTypeImage {
|
|
162
|
+
return FileTypeImage;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async video(content?: ReadableStream) {
|
|
166
|
+
const videoFile = new FileTypeVideo(this.path);
|
|
167
|
+
if (content) await videoFile.file.write(content);
|
|
168
|
+
return videoFile;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
static get video(): typeof FileTypeVideo {
|
|
172
|
+
return FileTypeVideo;
|
|
173
|
+
}
|
|
153
174
|
}
|
|
154
175
|
|
|
155
176
|
/**
|
|
156
177
|
* A generic file adaptor, extended by specific file type implementations
|
|
157
178
|
*/
|
|
158
179
|
export class FileType {
|
|
159
|
-
file;
|
|
180
|
+
file: File;
|
|
160
181
|
|
|
161
182
|
constructor(filepath: string, contents?: string) {
|
|
162
183
|
this.file = new File(filepath);
|
|
163
184
|
if (contents) this.file.write(contents);
|
|
164
185
|
}
|
|
165
186
|
|
|
166
|
-
get path() {
|
|
187
|
+
get path(): string {
|
|
167
188
|
return this.file.path;
|
|
168
189
|
}
|
|
169
190
|
|
|
170
|
-
get root() {
|
|
191
|
+
get root(): string {
|
|
171
192
|
return this.file.root;
|
|
172
193
|
}
|
|
173
194
|
|
|
174
|
-
get dir() {
|
|
195
|
+
get dir(): string {
|
|
175
196
|
return this.file.dir;
|
|
176
197
|
}
|
|
177
198
|
|
|
178
|
-
get base() {
|
|
199
|
+
get base(): string {
|
|
179
200
|
return this.file.base;
|
|
180
201
|
}
|
|
181
202
|
|
|
182
|
-
get name() {
|
|
203
|
+
get name(): string {
|
|
183
204
|
return this.file.name;
|
|
184
205
|
}
|
|
185
206
|
|
|
186
|
-
get ext() {
|
|
207
|
+
get ext(): string {
|
|
187
208
|
return this.file.ext;
|
|
188
209
|
}
|
|
189
210
|
|
|
190
|
-
get type() {
|
|
211
|
+
get type(): string | undefined {
|
|
191
212
|
return this.file.type;
|
|
192
213
|
}
|
|
193
214
|
|
|
194
|
-
get exists() {
|
|
215
|
+
get exists(): boolean {
|
|
195
216
|
return this.file.exists;
|
|
196
217
|
}
|
|
197
218
|
|
|
198
|
-
get stats() {
|
|
219
|
+
get stats(): Partial<fs.Stats> {
|
|
199
220
|
return this.file.stats;
|
|
200
221
|
}
|
|
201
222
|
|
|
202
|
-
delete() {
|
|
223
|
+
delete(): void {
|
|
203
224
|
this.file.delete();
|
|
204
225
|
}
|
|
205
226
|
|
|
206
|
-
get readStream() {
|
|
227
|
+
get readStream(): fs.ReadStream | Readable {
|
|
207
228
|
return this.file.readStream;
|
|
208
229
|
}
|
|
209
230
|
|
|
210
|
-
get writeStream() {
|
|
231
|
+
get writeStream(): fs.WriteStream {
|
|
211
232
|
return this.file.writeStream;
|
|
212
233
|
}
|
|
213
234
|
}
|
|
214
235
|
|
|
215
236
|
/**
|
|
216
237
|
* A .json file that maintains data type when reading/writing.
|
|
217
|
-
* > ⚠️ This is mildly unsafe,
|
|
238
|
+
* > ⚠️ This is mildly unsafe, json files should be validated at runtime!
|
|
218
239
|
* @example
|
|
219
240
|
* const file = new FileTypeJson('./data', { key: 'val' }); // FileTypeJson<{ key: string; }>
|
|
220
241
|
* console.log(file.path) // '/path/to/cwd/data.json'
|
|
@@ -229,13 +250,13 @@ export class FileTypeJson<T> extends FileType {
|
|
|
229
250
|
if (contents) this.write(contents);
|
|
230
251
|
}
|
|
231
252
|
|
|
232
|
-
read() {
|
|
253
|
+
read(): T | undefined {
|
|
233
254
|
const contents = this.file.read();
|
|
234
255
|
return contents ? (JSON.parse(contents) as T) : undefined;
|
|
235
256
|
}
|
|
236
257
|
|
|
237
|
-
write(contents: T) {
|
|
238
|
-
this.file.write(JSON.stringify(
|
|
258
|
+
write(contents: T): void {
|
|
259
|
+
this.file.write(JSON.stringify(contents, null, 2));
|
|
239
260
|
}
|
|
240
261
|
}
|
|
241
262
|
|
|
@@ -249,32 +270,45 @@ export class FileTypeNdjson<T extends object> extends FileType {
|
|
|
249
270
|
if (lines) this.append(lines);
|
|
250
271
|
}
|
|
251
272
|
|
|
252
|
-
append(lines: T | T[]) {
|
|
253
|
-
this.file.append(
|
|
254
|
-
Array.isArray(lines) ? lines.map(l => JSON.stringify(snapshot(l))) : JSON.stringify(snapshot(lines)),
|
|
255
|
-
);
|
|
273
|
+
append(lines: T | T[]): void {
|
|
274
|
+
this.file.append(Array.isArray(lines) ? lines.map(l => JSON.stringify(l)) : JSON.stringify(lines));
|
|
256
275
|
}
|
|
257
276
|
|
|
258
|
-
lines() {
|
|
277
|
+
lines(): T[] {
|
|
259
278
|
return this.file.lines().map(l => JSON.parse(l) as T);
|
|
260
279
|
}
|
|
261
280
|
}
|
|
262
281
|
|
|
263
282
|
type Key<T extends object> = keyof T;
|
|
264
283
|
|
|
284
|
+
type FileTypeCsvOptions<Row extends object> = {
|
|
285
|
+
parseNumbers?: boolean;
|
|
286
|
+
parseBooleans?: boolean;
|
|
287
|
+
parseNulls?: boolean;
|
|
288
|
+
keys?: Key<Row>[];
|
|
289
|
+
};
|
|
290
|
+
|
|
265
291
|
/**
|
|
266
292
|
* Comma separated values (.csv).
|
|
267
293
|
* Input rows as objects, keys are used as column headers
|
|
268
294
|
*/
|
|
269
295
|
export class FileTypeCsv<Row extends object> extends FileType {
|
|
270
|
-
|
|
296
|
+
options: FileTypeCsvOptions<Row>;
|
|
297
|
+
|
|
298
|
+
constructor(filepath: string, options: FileTypeCsvOptions<Row> = {}) {
|
|
271
299
|
super(filepath.endsWith('.csv') ? filepath : filepath + '.csv');
|
|
300
|
+
this.options = {
|
|
301
|
+
parseNumbers: true,
|
|
302
|
+
parseBooleans: true,
|
|
303
|
+
parseNulls: true,
|
|
304
|
+
...options,
|
|
305
|
+
};
|
|
272
306
|
}
|
|
273
307
|
|
|
274
|
-
async write(rows: Row[]
|
|
308
|
+
async write(rows: Row[]): Promise<void> {
|
|
275
309
|
const headerSet = new Set<Key<Row>>();
|
|
276
|
-
if (keys) {
|
|
277
|
-
for (const key of keys) headerSet.add(key);
|
|
310
|
+
if (this.options.keys) {
|
|
311
|
+
for (const key of this.options.keys) headerSet.add(key);
|
|
278
312
|
} else {
|
|
279
313
|
for (const row of rows) {
|
|
280
314
|
for (const key in row) headerSet.add(key);
|
|
@@ -285,15 +319,16 @@ export class FileTypeCsv<Row extends object> extends FileType {
|
|
|
285
319
|
return finished(writeToStream(this.file.writeStream, [headers, ...outRows]));
|
|
286
320
|
}
|
|
287
321
|
|
|
288
|
-
#parseVal(val: string) {
|
|
289
|
-
|
|
290
|
-
if (val.toLowerCase() === '
|
|
291
|
-
if (val.
|
|
292
|
-
if (
|
|
322
|
+
#parseVal(val: string): string | number | boolean | null {
|
|
323
|
+
const { parseNumbers, parseBooleans, parseNulls } = this.options;
|
|
324
|
+
if (parseBooleans && val.toLowerCase() === 'false') return false;
|
|
325
|
+
if (parseBooleans && val.toLowerCase() === 'true') return true;
|
|
326
|
+
if (parseNulls && (val.length === 0 || val.toLowerCase() === 'null')) return null;
|
|
327
|
+
if (parseNumbers && /^[\.0-9]+$/.test(val)) return Number(val);
|
|
293
328
|
return val;
|
|
294
329
|
}
|
|
295
330
|
|
|
296
|
-
async read() {
|
|
331
|
+
async read(): Promise<Row[]> {
|
|
297
332
|
return new Promise<Row[]>((resolve, reject) => {
|
|
298
333
|
const parsed: Row[] = [];
|
|
299
334
|
parseStream(this.file.readStream, { headers: true })
|
|
@@ -313,3 +348,28 @@ export class FileTypeCsv<Row extends object> extends FileType {
|
|
|
313
348
|
});
|
|
314
349
|
}
|
|
315
350
|
}
|
|
351
|
+
|
|
352
|
+
export class FileTypeImage extends FileType {
|
|
353
|
+
async dimensions(): Promise<{
|
|
354
|
+
width: number;
|
|
355
|
+
height: number;
|
|
356
|
+
}> {
|
|
357
|
+
return probeImageSize(this.file.readStream);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export class FileTypeVideo extends FileType {
|
|
362
|
+
async dimensions(): Promise<{
|
|
363
|
+
width: number;
|
|
364
|
+
height: number;
|
|
365
|
+
duration: number;
|
|
366
|
+
}> {
|
|
367
|
+
return Cmd.ffprobe(`-select_streams v:0 -show_entries stream -of json "${this.file.path}"`).then(out => {
|
|
368
|
+
const { streams } = JSON.parse(out) as {
|
|
369
|
+
streams: { width: number; height: number; duration: string }[];
|
|
370
|
+
};
|
|
371
|
+
if (!streams[0]) throw new Error('Could not parse video stream');
|
|
372
|
+
return { ...streams[0], duration: Number(streams[0].duration) };
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
}
|
package/src/Format.ts
CHANGED
|
@@ -20,7 +20,7 @@ export class Format {
|
|
|
20
20
|
static date(
|
|
21
21
|
formatStr: 'iso' | 'ymd' | 'ymd-hm' | 'ymd-hms' | 'h:m:s' | string = 'iso',
|
|
22
22
|
d: DateArg<Date> = new Date(),
|
|
23
|
-
) {
|
|
23
|
+
): string {
|
|
24
24
|
if (formatStr === 'iso') return formatISO(d);
|
|
25
25
|
if (formatStr === 'ymd') return format(d, 'yyyyMMdd');
|
|
26
26
|
if (formatStr === 'ymd-hm') return format(d, 'yyyyMMdd-HHmm');
|
|
@@ -32,11 +32,11 @@ export class Format {
|
|
|
32
32
|
/**
|
|
33
33
|
* Round a number to a specific set of places
|
|
34
34
|
*/
|
|
35
|
-
static round(n: number, places = 0) {
|
|
35
|
+
static round(n: number, places = 0): string {
|
|
36
36
|
return new Intl.NumberFormat('en-US', { maximumFractionDigits: places }).format(n);
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
static plural(amount: number, singular: string, multiple?: string) {
|
|
39
|
+
static plural(amount: number, singular: string, multiple?: string): string {
|
|
40
40
|
return amount === 1 ? `${amount} ${singular}` : `${amount} ${multiple || singular + 's'}`;
|
|
41
41
|
}
|
|
42
42
|
|
|
@@ -47,7 +47,7 @@ export class Format {
|
|
|
47
47
|
* @see details on 'digital' format https://github.com/ungoldman/format-duration
|
|
48
48
|
* @see waiting on `Intl.DurationFormat({ style: 'digital' })` types https://github.com/microsoft/TypeScript/issues/60608
|
|
49
49
|
*/
|
|
50
|
-
static ms(ms: number, style?: 'digital') {
|
|
50
|
+
static ms(ms: number, style?: 'digital'): string {
|
|
51
51
|
if (style === 'digital') return formatDuration(ms, { leading: true });
|
|
52
52
|
if (ms < 1000) return `${this.round(ms)}ms`;
|
|
53
53
|
const s = ms / 1000;
|
|
@@ -60,7 +60,7 @@ export class Format {
|
|
|
60
60
|
return `${d}d ${h % 24}h`;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
static bytes(b: number) {
|
|
63
|
+
static bytes(b: number): string {
|
|
64
64
|
const labels = ['b', 'KB', 'MB', 'GB', 'TB'];
|
|
65
65
|
let factor = 0;
|
|
66
66
|
while (b >= 1024 && labels[factor + 1]) {
|
package/src/Log.test.ts
CHANGED
|
@@ -3,13 +3,23 @@ import assert from 'node:assert';
|
|
|
3
3
|
import { Log } from './Log.ts';
|
|
4
4
|
|
|
5
5
|
describe('Log', () => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
function allLevels() {
|
|
7
|
+
Log.trace('trace');
|
|
8
|
+
Log.debug('debug');
|
|
9
|
+
Log.info('info');
|
|
10
|
+
Log.warn('warn');
|
|
11
|
+
try {
|
|
12
|
+
// Log.error('error', { err: new Error('error') });
|
|
13
|
+
} catch (e) {}
|
|
14
|
+
Log.fatal('fatal');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
it('Logs for dev', () => {
|
|
18
|
+
allLevels();
|
|
9
19
|
});
|
|
10
20
|
|
|
11
|
-
it('
|
|
12
|
-
|
|
13
|
-
|
|
21
|
+
it('Logs for production', () => {
|
|
22
|
+
Log.configure({ environment: 'production' });
|
|
23
|
+
allLevels();
|
|
14
24
|
});
|
|
15
25
|
});
|
package/src/Log.ts
CHANGED
|
@@ -1,126 +1,86 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import chalk, { type ChalkInstance } from 'chalk';
|
|
4
|
-
import { snapshot } from './snapshot.ts';
|
|
5
|
-
import { Format } from './Format.ts';
|
|
1
|
+
import { merge } from 'lodash-es';
|
|
2
|
+
import { default as pino, type Logger } from 'pino';
|
|
6
3
|
|
|
7
|
-
|
|
8
|
-
type Severity = 'DEFAULT' | 'DEBUG' | 'INFO' | 'NOTICE' | 'WARNING' | 'ERROR' | 'CRITICAL' | 'ALERT' | 'EMERGENCY';
|
|
4
|
+
export type LogDetails = Record<string, unknown>;
|
|
9
5
|
|
|
10
|
-
type
|
|
11
|
-
severity: Severity;
|
|
12
|
-
color: ChalkInstance;
|
|
13
|
-
};
|
|
6
|
+
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
|
14
7
|
|
|
15
|
-
type
|
|
16
|
-
|
|
17
|
-
severity: Severity;
|
|
18
|
-
stack?: string;
|
|
19
|
-
details?: unknown[];
|
|
8
|
+
export type LogOptions = pino.LoggerOptions & {
|
|
9
|
+
environment?: string;
|
|
20
10
|
};
|
|
21
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Wrapper for [pino](https://github.com/pinojs/pino)
|
|
14
|
+
* Levels: fatal, error, warn, info, debug, trace
|
|
15
|
+
* Use `LOG_LEVL=info` to limit what's printed to console
|
|
16
|
+
* Use `Log.configure` to customize the pino instance
|
|
17
|
+
*/
|
|
22
18
|
export class Log {
|
|
23
|
-
static
|
|
24
|
-
|
|
25
|
-
// replaces details.stack with current stack trace, excluding this Log.getStack call
|
|
26
|
-
Error.captureStackTrace(details, Log.getStack);
|
|
27
|
-
// remove 'Error' on first line
|
|
28
|
-
return details.stack
|
|
29
|
-
.split('\n')
|
|
30
|
-
.map(l => l.trim())
|
|
31
|
-
.filter(l => l !== 'Error');
|
|
32
|
-
}
|
|
19
|
+
static #logger?: Logger;
|
|
20
|
+
static #options: LogOptions;
|
|
33
21
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
22
|
+
static createLogger(): Logger {
|
|
23
|
+
const isProduction = this.#options?.environment === 'production' || process.env.NODE_ENV === 'production';
|
|
24
|
+
const defaultOptions: LogOptions = {
|
|
25
|
+
level: process.env.LOG_LEVEL ?? (isProduction ? 'info' : 'debug'),
|
|
26
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
27
|
+
formatters: {
|
|
28
|
+
level(label: string) {
|
|
29
|
+
return { level: label };
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
transport: isProduction
|
|
33
|
+
? undefined
|
|
34
|
+
: {
|
|
35
|
+
target: 'pino-pretty',
|
|
36
|
+
options: {
|
|
37
|
+
colorize: true,
|
|
38
|
+
singleLine: true,
|
|
39
|
+
ignore: 'pid,hostname',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
return pino(merge(defaultOptions, this.#options));
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
static #toConsole(entry: Entry, color: ChalkInstance) {
|
|
47
|
-
if (entry.message) console.log(color(`${Format.date('h:m:s')} [${entry.severity}] ${entry.message}`));
|
|
48
|
-
entry.details?.forEach(detail => {
|
|
49
|
-
console.log(inspect(detail, { depth: 10, breakLength: 100, compact: true, colors: true }));
|
|
50
|
-
});
|
|
46
|
+
static configure(options: LogOptions = {}): void {
|
|
47
|
+
this.#options = options;
|
|
48
|
+
this.#logger = this.createLogger();
|
|
51
49
|
}
|
|
52
50
|
|
|
53
|
-
static #
|
|
54
|
-
|
|
55
|
-
const entry: Entry = { message, severity, details };
|
|
56
|
-
// https://cloud.google.com/run/docs/container-contract#env-vars
|
|
57
|
-
const isGcloud = process.env.K_SERVICE !== undefined || process.env.CLOUD_RUN_JOB !== undefined;
|
|
58
|
-
if (isGcloud) {
|
|
59
|
-
this.#toGcloud(entry);
|
|
60
|
-
} else {
|
|
61
|
-
this.#toConsole(entry, color);
|
|
62
|
-
}
|
|
63
|
-
return entry;
|
|
51
|
+
static #getLogger(): Logger {
|
|
52
|
+
return (this.#logger ??= this.createLogger());
|
|
64
53
|
}
|
|
65
54
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
// First argument is a string, use that as the message
|
|
72
|
-
if (typeof firstArg === 'string') {
|
|
73
|
-
return { message: firstArg, details: rest };
|
|
74
|
-
}
|
|
75
|
-
// First argument is an object with a `message` property
|
|
76
|
-
// @ts-ignore
|
|
77
|
-
if (isObjectLike(firstArg) && typeof firstArg['message'] === 'string') {
|
|
78
|
-
const { message, ...firstDetails } = firstArg as { message: string };
|
|
79
|
-
return { message, details: [firstDetails, ...rest] };
|
|
55
|
+
static #write(level: LogLevel, message: string, details?: LogDetails): void {
|
|
56
|
+
if (details === undefined) {
|
|
57
|
+
Log.#getLogger()[level](message);
|
|
58
|
+
} else {
|
|
59
|
+
Log.#getLogger()[level](details, message);
|
|
80
60
|
}
|
|
81
|
-
// No message found, log all args as details
|
|
82
|
-
return { details: input };
|
|
83
61
|
}
|
|
84
62
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
*/
|
|
88
|
-
static alert(...input: unknown[]) {
|
|
89
|
-
return this.#log({ severity: 'ALERT', color: chalk.bgRed }, ...input);
|
|
63
|
+
static trace(message: string, details?: LogDetails): void {
|
|
64
|
+
Log.#write('trace', message, details);
|
|
90
65
|
}
|
|
91
66
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
*/
|
|
95
|
-
static error(...input: unknown[]) {
|
|
96
|
-
return this.#log({ severity: 'ERROR', color: chalk.red }, ...input);
|
|
67
|
+
static debug(message: string, details?: LogDetails): void {
|
|
68
|
+
Log.#write('debug', message, details);
|
|
97
69
|
}
|
|
98
70
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
*/
|
|
102
|
-
static warn(...input: unknown[]) {
|
|
103
|
-
return this.#log({ severity: 'WARNING', color: chalk.yellow }, ...input);
|
|
71
|
+
static info(message: string, details?: LogDetails): void {
|
|
72
|
+
Log.#write('info', message, details);
|
|
104
73
|
}
|
|
105
74
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
*/
|
|
109
|
-
static notice(...input: unknown[]) {
|
|
110
|
-
return this.#log({ severity: 'NOTICE', color: chalk.cyan }, ...input);
|
|
75
|
+
static warn(message: string, details?: LogDetails): void {
|
|
76
|
+
Log.#write('warn', message, details);
|
|
111
77
|
}
|
|
112
78
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
*/
|
|
116
|
-
static info(...input: unknown[]) {
|
|
117
|
-
return this.#log({ severity: 'INFO', color: chalk.white }, ...input);
|
|
79
|
+
static error(message: string, details?: LogDetails): void {
|
|
80
|
+
Log.#write('error', message, details);
|
|
118
81
|
}
|
|
119
82
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
*/
|
|
123
|
-
static debug(...input: unknown[]) {
|
|
124
|
-
return this.#log({ severity: 'DEBUG', color: chalk.gray }, ...input);
|
|
83
|
+
static fatal(message: string, details?: LogDetails): void {
|
|
84
|
+
Log.#write('fatal', message, details);
|
|
125
85
|
}
|
|
126
86
|
}
|
package/src/TypeWriter.ts
CHANGED
|
@@ -2,12 +2,20 @@ import * as fs from 'node:fs';
|
|
|
2
2
|
import { merge } from 'lodash-es';
|
|
3
3
|
import * as qt from 'quicktype-core';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Wrapper for [quicktype-core](https://github.com/glideapps/quicktype)
|
|
7
|
+
* @example
|
|
8
|
+
* const group = new TypeWriter('Group');
|
|
9
|
+
* await types.addMember('Thing', [{ a: 1 }, { a: 2, b: 1 }]);
|
|
10
|
+
* await types.toFile();
|
|
11
|
+
* // type def for `Thing` saved in `types/Group.types.ts`
|
|
12
|
+
*/
|
|
5
13
|
export class TypeWriter {
|
|
6
|
-
moduleName;
|
|
14
|
+
moduleName: string;
|
|
7
15
|
input = qt.jsonInputForTargetLanguage('typescript');
|
|
8
|
-
outDir;
|
|
9
|
-
outFile;
|
|
10
|
-
qtSettings
|
|
16
|
+
outDir: string;
|
|
17
|
+
outFile: string;
|
|
18
|
+
qtSettings: Partial<qt.Options>;
|
|
11
19
|
|
|
12
20
|
constructor(moduleName: string, settings: { outDir?: string; outFile?: string } & Partial<qt.Options> = {}) {
|
|
13
21
|
this.moduleName = moduleName;
|
|
@@ -26,12 +34,12 @@ export class TypeWriter {
|
|
|
26
34
|
this.qtSettings = merge(defaultSettings, qtSettings);
|
|
27
35
|
}
|
|
28
36
|
|
|
29
|
-
async addMember(name: string, _samples: any[]) {
|
|
37
|
+
async addMember(name: string, _samples: any[]): Promise<void> {
|
|
30
38
|
const samples = _samples.map(s => (typeof s === 'string' ? s : JSON.stringify(s)));
|
|
31
39
|
await this.input.addSource({ name, samples });
|
|
32
40
|
}
|
|
33
41
|
|
|
34
|
-
async toString() {
|
|
42
|
+
async toString(): Promise<string> {
|
|
35
43
|
const inputData = new qt.InputData();
|
|
36
44
|
inputData.addInput(this.input);
|
|
37
45
|
const result = await qt.quicktype({
|
|
@@ -41,7 +49,7 @@ export class TypeWriter {
|
|
|
41
49
|
return result.lines.join('\n');
|
|
42
50
|
}
|
|
43
51
|
|
|
44
|
-
async toFile() {
|
|
52
|
+
async toFile(): Promise<void> {
|
|
45
53
|
const result = await this.toString();
|
|
46
54
|
fs.mkdirSync(this.outDir, { recursive: true });
|
|
47
55
|
fs.writeFileSync(`${this.outDir}/${this.outFile}`, result);
|