@brianbuie/node-kit 0.2.4 → 0.3.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/Cache.d.ts +21 -0
- package/.dist/Cache.js +23 -0
- package/.dist/Dir.d.ts +44 -0
- package/.dist/Dir.js +64 -0
- package/.dist/File.d.ts +39 -0
- package/.dist/File.js +103 -0
- package/.dist/Jwt.d.ts +7 -8
- package/.dist/Jwt.js +12 -18
- package/.dist/_index.d.ts +5 -1
- package/.dist/_index.js +5 -1
- package/.dist/timeout.d.ts +1 -0
- package/.dist/timeout.js +5 -0
- package/package.json +6 -5
- package/src/Cache.ts +27 -0
- package/src/Dir.test.ts +31 -0
- package/src/Dir.ts +75 -0
- package/src/File.test.ts +67 -0
- package/src/File.ts +123 -0
- package/src/Jwt.ts +17 -26
- package/src/_index.ts +5 -1
- package/src/timeout.test.ts +11 -0
- package/src/timeout.ts +5 -0
package/.dist/Cache.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare class Cache<T> {
|
|
2
|
+
file: {
|
|
3
|
+
addExt(filepath: string): string;
|
|
4
|
+
read(): {
|
|
5
|
+
createdAt: number;
|
|
6
|
+
value: T;
|
|
7
|
+
} | undefined;
|
|
8
|
+
write(contents: {
|
|
9
|
+
createdAt: number;
|
|
10
|
+
value: T;
|
|
11
|
+
}): void;
|
|
12
|
+
file: import("./File.js").File;
|
|
13
|
+
get exists(): boolean;
|
|
14
|
+
get path(): string;
|
|
15
|
+
};
|
|
16
|
+
ttl: number;
|
|
17
|
+
refresh: () => T | Promise<T>;
|
|
18
|
+
constructor(key: string, ttl: number, refresh: () => T | Promise<T>);
|
|
19
|
+
read(): Promise<T>;
|
|
20
|
+
write(): Promise<T>;
|
|
21
|
+
}
|
package/.dist/Cache.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { temp } from './Dir.js';
|
|
2
|
+
const cacheDir = temp.subDir('cache');
|
|
3
|
+
export class Cache {
|
|
4
|
+
file;
|
|
5
|
+
ttl;
|
|
6
|
+
refresh;
|
|
7
|
+
constructor(key, ttl, refresh) {
|
|
8
|
+
this.file = cacheDir.file(key).json();
|
|
9
|
+
this.ttl = ttl;
|
|
10
|
+
this.refresh = refresh;
|
|
11
|
+
}
|
|
12
|
+
async read() {
|
|
13
|
+
const { createdAt, value } = this.file.read() || {};
|
|
14
|
+
if (value && createdAt && createdAt + this.ttl > Date.now())
|
|
15
|
+
return value;
|
|
16
|
+
return this.write();
|
|
17
|
+
}
|
|
18
|
+
async write() {
|
|
19
|
+
const value = await this.refresh();
|
|
20
|
+
this.file.write({ createdAt: Date.now(), value });
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
}
|
package/.dist/Dir.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { File } from './File.js';
|
|
2
|
+
/**
|
|
3
|
+
* Reference to a specific directory with helpful methods for resolving filepaths,
|
|
4
|
+
* sanitizing filenames, and saving files
|
|
5
|
+
*/
|
|
6
|
+
export declare class Dir {
|
|
7
|
+
path: string;
|
|
8
|
+
/**
|
|
9
|
+
* @param path can be relative to workspace or absolute
|
|
10
|
+
*/
|
|
11
|
+
constructor(_path: string);
|
|
12
|
+
create(): void;
|
|
13
|
+
/**
|
|
14
|
+
* Create a new Dir inside the current Dir
|
|
15
|
+
* @param subPath to create in current Dir
|
|
16
|
+
* @example
|
|
17
|
+
* const folder = new Dir('example');
|
|
18
|
+
* // folder.path = './example'
|
|
19
|
+
* const child = folder.subDir('path/to/dir');
|
|
20
|
+
* // child.path = './example/path/to/dir'
|
|
21
|
+
*/
|
|
22
|
+
subDir(subPath: string): Dir;
|
|
23
|
+
sanitize(name: string): string;
|
|
24
|
+
/**
|
|
25
|
+
* @param base - The file name with extension
|
|
26
|
+
* @example
|
|
27
|
+
* const folder = new Dir('example');
|
|
28
|
+
* const filepath = folder.resolve('file.json');
|
|
29
|
+
* // 'example/file.json'
|
|
30
|
+
*/
|
|
31
|
+
filepath(base: string): string;
|
|
32
|
+
file(base: string): File;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Extends Dir class with method to `clear()` contents
|
|
36
|
+
*/
|
|
37
|
+
export declare class TempDir extends Dir {
|
|
38
|
+
subDir(subPath: string): TempDir;
|
|
39
|
+
clear(): void;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Common temp dir location
|
|
43
|
+
*/
|
|
44
|
+
export declare const temp: TempDir;
|
package/.dist/Dir.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import sanitizeFilename from 'sanitize-filename';
|
|
4
|
+
import { File } from './File.js';
|
|
5
|
+
/**
|
|
6
|
+
* Reference to a specific directory with helpful methods for resolving filepaths,
|
|
7
|
+
* sanitizing filenames, and saving files
|
|
8
|
+
*/
|
|
9
|
+
export class Dir {
|
|
10
|
+
path;
|
|
11
|
+
/**
|
|
12
|
+
* @param path can be relative to workspace or absolute
|
|
13
|
+
*/
|
|
14
|
+
constructor(_path) {
|
|
15
|
+
this.path = _path;
|
|
16
|
+
}
|
|
17
|
+
create() {
|
|
18
|
+
fs.mkdirSync(this.path, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Create a new Dir inside the current Dir
|
|
22
|
+
* @param subPath to create in current Dir
|
|
23
|
+
* @example
|
|
24
|
+
* const folder = new Dir('example');
|
|
25
|
+
* // folder.path = './example'
|
|
26
|
+
* const child = folder.subDir('path/to/dir');
|
|
27
|
+
* // child.path = './example/path/to/dir'
|
|
28
|
+
*/
|
|
29
|
+
subDir(subPath) {
|
|
30
|
+
return new Dir(path.resolve(this.path, subPath));
|
|
31
|
+
}
|
|
32
|
+
sanitize(name) {
|
|
33
|
+
return sanitizeFilename(name.replace('https://', '').replace('www.', ''), { replacement: '_' });
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* @param base - The file name with extension
|
|
37
|
+
* @example
|
|
38
|
+
* const folder = new Dir('example');
|
|
39
|
+
* const filepath = folder.resolve('file.json');
|
|
40
|
+
* // 'example/file.json'
|
|
41
|
+
*/
|
|
42
|
+
filepath(base) {
|
|
43
|
+
return path.resolve(this.path, this.sanitize(base));
|
|
44
|
+
}
|
|
45
|
+
file(base) {
|
|
46
|
+
return new File(this.filepath(base));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Extends Dir class with method to `clear()` contents
|
|
51
|
+
*/
|
|
52
|
+
export class TempDir extends Dir {
|
|
53
|
+
subDir(subPath) {
|
|
54
|
+
return new TempDir(path.resolve(this.path, subPath));
|
|
55
|
+
}
|
|
56
|
+
clear() {
|
|
57
|
+
fs.rmSync(this.path, { recursive: true, force: true });
|
|
58
|
+
this.create();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Common temp dir location
|
|
63
|
+
*/
|
|
64
|
+
export const temp = new TempDir('.temp');
|
package/.dist/File.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export declare class File {
|
|
2
|
+
path: string;
|
|
3
|
+
constructor(filepath: string);
|
|
4
|
+
get exists(): boolean;
|
|
5
|
+
read(): string | undefined;
|
|
6
|
+
write(contents: string): void;
|
|
7
|
+
/**
|
|
8
|
+
* creates file if it doesn't exist, appends string or array of strings as new lines.
|
|
9
|
+
* File always ends with '\n', so contents don't need to be read before appending
|
|
10
|
+
*/
|
|
11
|
+
append(lines: string | string[]): void;
|
|
12
|
+
/**
|
|
13
|
+
* @returns lines as strings, removes trailing '\n'
|
|
14
|
+
*/
|
|
15
|
+
lines(): string[];
|
|
16
|
+
static get Adaptor(): typeof FileAdaptor;
|
|
17
|
+
json<T>(): JsonFile<T>;
|
|
18
|
+
static get json(): typeof JsonFile;
|
|
19
|
+
ndjson<T>(): NdjsonFile<T>;
|
|
20
|
+
static get ndjson(): typeof NdjsonFile;
|
|
21
|
+
}
|
|
22
|
+
declare class FileAdaptor {
|
|
23
|
+
file: File;
|
|
24
|
+
constructor(file: string | File);
|
|
25
|
+
addExt(filepath: string): string;
|
|
26
|
+
get exists(): boolean;
|
|
27
|
+
get path(): string;
|
|
28
|
+
}
|
|
29
|
+
declare class JsonFile<T> extends FileAdaptor {
|
|
30
|
+
addExt(filepath: string): string;
|
|
31
|
+
read(): T | undefined;
|
|
32
|
+
write(contents: T): void;
|
|
33
|
+
}
|
|
34
|
+
declare class NdjsonFile<T> extends FileAdaptor {
|
|
35
|
+
addExt(filepath: string): string;
|
|
36
|
+
lines(): T[];
|
|
37
|
+
append(lines: T | T[]): void;
|
|
38
|
+
}
|
|
39
|
+
export {};
|
package/.dist/File.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { snapshot } from './snapshot.js';
|
|
4
|
+
export class File {
|
|
5
|
+
path;
|
|
6
|
+
constructor(filepath) {
|
|
7
|
+
this.path = filepath;
|
|
8
|
+
}
|
|
9
|
+
get exists() {
|
|
10
|
+
return fs.existsSync(this.path);
|
|
11
|
+
}
|
|
12
|
+
read() {
|
|
13
|
+
return this.exists ? fs.readFileSync(this.path, 'utf8') : undefined;
|
|
14
|
+
}
|
|
15
|
+
write(contents) {
|
|
16
|
+
fs.mkdirSync(path.parse(this.path).dir, { recursive: true });
|
|
17
|
+
fs.writeFileSync(this.path, contents);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* creates file if it doesn't exist, appends string or array of strings as new lines.
|
|
21
|
+
* File always ends with '\n', so contents don't need to be read before appending
|
|
22
|
+
*/
|
|
23
|
+
append(lines) {
|
|
24
|
+
if (!this.exists)
|
|
25
|
+
this.write('');
|
|
26
|
+
const contents = Array.isArray(lines) ? lines.join('\n') : lines;
|
|
27
|
+
fs.appendFileSync(this.path, contents + '\n');
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* @returns lines as strings, removes trailing '\n'
|
|
31
|
+
*/
|
|
32
|
+
lines() {
|
|
33
|
+
const contents = (this.read() || '').split('\n');
|
|
34
|
+
return contents.slice(0, contents.length - 1);
|
|
35
|
+
}
|
|
36
|
+
static get Adaptor() {
|
|
37
|
+
return FileAdaptor;
|
|
38
|
+
}
|
|
39
|
+
json() {
|
|
40
|
+
return new JsonFile(this);
|
|
41
|
+
}
|
|
42
|
+
static get json() {
|
|
43
|
+
return JsonFile;
|
|
44
|
+
}
|
|
45
|
+
ndjson() {
|
|
46
|
+
return new NdjsonFile(this);
|
|
47
|
+
}
|
|
48
|
+
static get ndjson() {
|
|
49
|
+
return NdjsonFile;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
class FileAdaptor {
|
|
53
|
+
file;
|
|
54
|
+
constructor(file) {
|
|
55
|
+
if (file instanceof File) {
|
|
56
|
+
const withExt = this.addExt(file.path);
|
|
57
|
+
if (withExt === file.path) {
|
|
58
|
+
this.file = file;
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
this.file = new File(withExt);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
this.file = new File(this.addExt(file));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
addExt(filepath) {
|
|
69
|
+
return filepath;
|
|
70
|
+
}
|
|
71
|
+
get exists() {
|
|
72
|
+
return this.file.exists;
|
|
73
|
+
}
|
|
74
|
+
get path() {
|
|
75
|
+
return this.file.path;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
class JsonFile extends FileAdaptor {
|
|
79
|
+
addExt(filepath) {
|
|
80
|
+
return filepath.endsWith('.json') ? filepath : filepath + '.json';
|
|
81
|
+
}
|
|
82
|
+
read() {
|
|
83
|
+
const contents = this.file.read();
|
|
84
|
+
return contents ? JSON.parse(contents) : undefined;
|
|
85
|
+
}
|
|
86
|
+
write(contents) {
|
|
87
|
+
this.file.write(JSON.stringify(snapshot(contents), null, 2));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
class NdjsonFile extends FileAdaptor {
|
|
91
|
+
addExt(filepath) {
|
|
92
|
+
return filepath.endsWith('.ndjson') ? filepath : filepath + '.ndjson';
|
|
93
|
+
}
|
|
94
|
+
lines() {
|
|
95
|
+
return this.file.lines().map((l) => JSON.parse(l));
|
|
96
|
+
}
|
|
97
|
+
append(lines) {
|
|
98
|
+
const contents = Array.isArray(lines)
|
|
99
|
+
? lines.map((l) => JSON.stringify(snapshot(l)))
|
|
100
|
+
: JSON.stringify(snapshot(lines));
|
|
101
|
+
this.file.append(contents);
|
|
102
|
+
}
|
|
103
|
+
}
|
package/.dist/Jwt.d.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { type JwtPayload, type SignOptions } from 'jsonwebtoken';
|
|
2
|
-
|
|
3
|
-
#private;
|
|
2
|
+
type JwtConfig = {
|
|
4
3
|
payload: JwtPayload;
|
|
5
4
|
options: SignOptions;
|
|
6
5
|
seconds: number;
|
|
7
6
|
key: string;
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
});
|
|
7
|
+
};
|
|
8
|
+
export declare class Jwt {
|
|
9
|
+
#private;
|
|
10
|
+
config: JwtConfig;
|
|
11
|
+
constructor(config: JwtConfig);
|
|
14
12
|
get now(): number;
|
|
15
13
|
get token(): string;
|
|
16
14
|
}
|
|
15
|
+
export {};
|
package/.dist/Jwt.js
CHANGED
|
@@ -1,35 +1,29 @@
|
|
|
1
1
|
import { default as jsonwebtoken } from 'jsonwebtoken';
|
|
2
2
|
import { merge } from 'lodash-es';
|
|
3
3
|
export class Jwt {
|
|
4
|
-
|
|
5
|
-
options;
|
|
6
|
-
seconds;
|
|
7
|
-
key;
|
|
4
|
+
config;
|
|
8
5
|
#saved;
|
|
9
|
-
constructor(
|
|
10
|
-
this.
|
|
11
|
-
this.options = options;
|
|
12
|
-
this.seconds = seconds;
|
|
13
|
-
this.key = key;
|
|
6
|
+
constructor(config) {
|
|
7
|
+
this.config = config;
|
|
14
8
|
this.#createToken();
|
|
15
9
|
}
|
|
16
10
|
get now() {
|
|
17
11
|
return Math.floor(Date.now() / 1000);
|
|
18
12
|
}
|
|
19
|
-
get token() {
|
|
20
|
-
if (this.#saved && this.#saved.exp > this.now) {
|
|
21
|
-
return this.#saved.token;
|
|
22
|
-
}
|
|
23
|
-
return this.#createToken();
|
|
24
|
-
}
|
|
25
13
|
#createToken() {
|
|
26
|
-
const exp = this.now + this.seconds;
|
|
14
|
+
const exp = this.now + this.config.seconds;
|
|
27
15
|
const payload = merge({
|
|
28
16
|
iat: this.now,
|
|
29
17
|
exp,
|
|
30
|
-
}, this.payload);
|
|
31
|
-
const token = jsonwebtoken.sign(payload, this.key, this.options);
|
|
18
|
+
}, this.config.payload);
|
|
19
|
+
const token = jsonwebtoken.sign(payload, this.config.key, this.config.options);
|
|
32
20
|
this.#saved = { token, exp };
|
|
33
21
|
return token;
|
|
34
22
|
}
|
|
23
|
+
get token() {
|
|
24
|
+
if (this.#saved && this.#saved.exp > this.now) {
|
|
25
|
+
return this.#saved.token;
|
|
26
|
+
}
|
|
27
|
+
return this.#createToken();
|
|
28
|
+
}
|
|
35
29
|
}
|
package/.dist/_index.d.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
export { Dir, TempDir, temp } from './Dir.js';
|
|
2
|
+
export { Cache } from './Cache.js';
|
|
1
3
|
export { Fetcher, type Route, Query, FetchOptions } from './Fetcher.js';
|
|
4
|
+
export { File } from './File.js';
|
|
2
5
|
export { Jwt } from './Jwt.js';
|
|
3
|
-
export { snapshot } from './snapshot.js';
|
|
4
6
|
export { Log } from './Log.js';
|
|
7
|
+
export { snapshot } from './snapshot.js';
|
|
8
|
+
export { timeout } from './timeout.js';
|
|
5
9
|
export { TypeWriter } from './TypeWriter.js';
|
package/.dist/_index.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
export { Dir, TempDir, temp } from './Dir.js';
|
|
2
|
+
export { Cache } from './Cache.js';
|
|
1
3
|
export { Fetcher } from './Fetcher.js';
|
|
4
|
+
export { File } from './File.js';
|
|
2
5
|
export { Jwt } from './Jwt.js';
|
|
3
|
-
export { snapshot } from './snapshot.js';
|
|
4
6
|
export { Log } from './Log.js';
|
|
7
|
+
export { snapshot } from './snapshot.js';
|
|
8
|
+
export { timeout } from './timeout.js';
|
|
5
9
|
export { TypeWriter } from './TypeWriter.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function timeout(ms: number): Promise<unknown>;
|
package/.dist/timeout.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brianbuie/node-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"license": "ISC",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,16 +25,17 @@
|
|
|
25
25
|
"node": ">=24"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@types/jsonwebtoken": "^9.0.10",
|
|
29
|
-
"@types/lodash-es": "^4.17.12",
|
|
30
|
-
"@types/node": "^24.9.1",
|
|
31
28
|
"chalk": "^5.6.2",
|
|
32
29
|
"extract-domain": "^5.0.2",
|
|
33
30
|
"jsonwebtoken": "^9.0.2",
|
|
34
31
|
"lodash-es": "^4.17.21",
|
|
35
|
-
"quicktype-core": "^23.2.6"
|
|
32
|
+
"quicktype-core": "^23.2.6",
|
|
33
|
+
"sanitize-filename": "^1.6.3"
|
|
36
34
|
},
|
|
37
35
|
"devDependencies": {
|
|
36
|
+
"@types/jsonwebtoken": "^9.0.10",
|
|
37
|
+
"@types/lodash-es": "^4.17.12",
|
|
38
|
+
"@types/node": "^24.9.1",
|
|
38
39
|
"typescript": "^5.9.3"
|
|
39
40
|
}
|
|
40
41
|
}
|
package/src/Cache.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { temp } from './Dir.js';
|
|
2
|
+
|
|
3
|
+
const cacheDir = temp.subDir('cache');
|
|
4
|
+
|
|
5
|
+
export class Cache<T> {
|
|
6
|
+
file;
|
|
7
|
+
ttl;
|
|
8
|
+
refresh;
|
|
9
|
+
|
|
10
|
+
constructor(key: string, ttl: number, refresh: () => T | Promise<T>) {
|
|
11
|
+
this.file = cacheDir.file(key).json<{ createdAt: number; value: T }>();
|
|
12
|
+
this.ttl = ttl;
|
|
13
|
+
this.refresh = refresh;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async read() {
|
|
17
|
+
const { createdAt, value } = this.file.read() || {};
|
|
18
|
+
if (value && createdAt && createdAt + this.ttl > Date.now()) return value;
|
|
19
|
+
return this.write();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async write() {
|
|
23
|
+
const value = await this.refresh();
|
|
24
|
+
this.file.write({ createdAt: Date.now(), value });
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/Dir.test.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { Dir, TempDir, temp } from './Dir.js';
|
|
4
|
+
|
|
5
|
+
describe('Dir', () => {
|
|
6
|
+
const testDir = temp.subDir('dir-test');
|
|
7
|
+
|
|
8
|
+
it('Sanitizes filenames', () => {
|
|
9
|
+
const name = testDir.sanitize(':/something/else.json');
|
|
10
|
+
assert(!name.includes('/'));
|
|
11
|
+
assert(!name.includes(':'));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
it('Creates sub directories', () => {
|
|
15
|
+
const subPath = 'sub/dir';
|
|
16
|
+
const sub = testDir.subDir(subPath);
|
|
17
|
+
assert(sub.path.includes(testDir.path));
|
|
18
|
+
assert(sub.path.includes(subPath));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it('TempDir.subDir returns instance of TempDir', () => {
|
|
22
|
+
const sub = temp.subDir('example');
|
|
23
|
+
assert(sub instanceof TempDir);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('Resolves filenames in folder', () => {
|
|
27
|
+
const txt = temp.filepath('test.txt');
|
|
28
|
+
assert(txt.includes(temp.path));
|
|
29
|
+
assert(txt.includes('test.txt'));
|
|
30
|
+
});
|
|
31
|
+
});
|
package/src/Dir.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import sanitizeFilename from 'sanitize-filename';
|
|
4
|
+
import { File } from './File.js';
|
|
5
|
+
import { snapshot } from './snapshot.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Reference to a specific directory with helpful methods for resolving filepaths,
|
|
9
|
+
* sanitizing filenames, and saving files
|
|
10
|
+
*/
|
|
11
|
+
export class Dir {
|
|
12
|
+
path;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param path can be relative to workspace or absolute
|
|
16
|
+
*/
|
|
17
|
+
constructor(_path: string) {
|
|
18
|
+
this.path = _path;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
create() {
|
|
22
|
+
fs.mkdirSync(this.path, { recursive: true });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create a new Dir inside the current Dir
|
|
27
|
+
* @param subPath to create in current Dir
|
|
28
|
+
* @example
|
|
29
|
+
* const folder = new Dir('example');
|
|
30
|
+
* // folder.path = './example'
|
|
31
|
+
* const child = folder.subDir('path/to/dir');
|
|
32
|
+
* // child.path = './example/path/to/dir'
|
|
33
|
+
*/
|
|
34
|
+
subDir(subPath: string) {
|
|
35
|
+
return new Dir(path.resolve(this.path, subPath));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
sanitize(name: string) {
|
|
39
|
+
return sanitizeFilename(name.replace('https://', '').replace('www.', ''), { replacement: '_' });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @param base - The file name with extension
|
|
44
|
+
* @example
|
|
45
|
+
* const folder = new Dir('example');
|
|
46
|
+
* const filepath = folder.resolve('file.json');
|
|
47
|
+
* // 'example/file.json'
|
|
48
|
+
*/
|
|
49
|
+
filepath(base: string) {
|
|
50
|
+
return path.resolve(this.path, this.sanitize(base));
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
file(base: string) {
|
|
54
|
+
return new File(this.filepath(base));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Extends Dir class with method to `clear()` contents
|
|
60
|
+
*/
|
|
61
|
+
export class TempDir extends Dir {
|
|
62
|
+
subDir(subPath: string) {
|
|
63
|
+
return new TempDir(path.resolve(this.path, subPath));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
clear() {
|
|
67
|
+
fs.rmSync(this.path, { recursive: true, force: true });
|
|
68
|
+
this.create();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Common temp dir location
|
|
74
|
+
*/
|
|
75
|
+
export const temp = new TempDir('.temp');
|
package/src/File.test.ts
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { isEqual } from 'lodash-es';
|
|
4
|
+
import { temp } from './Dir.js';
|
|
5
|
+
import { File } from './File.js';
|
|
6
|
+
|
|
7
|
+
const testDir = temp.subDir('file-test');
|
|
8
|
+
testDir.clear();
|
|
9
|
+
|
|
10
|
+
const thing = {
|
|
11
|
+
a: 'string',
|
|
12
|
+
b: 2,
|
|
13
|
+
c: true,
|
|
14
|
+
d: null,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe('FileAdaptor', () => {
|
|
18
|
+
it('Instances can be created', () => {
|
|
19
|
+
const test1 = new File.Adaptor(testDir.filepath('test1.txt'));
|
|
20
|
+
assert(test1.file.path.includes('test1.txt'));
|
|
21
|
+
|
|
22
|
+
const base = 'test2';
|
|
23
|
+
const eg1 = new File.json(testDir.filepath(base));
|
|
24
|
+
const eg2 = testDir.file(base).json();
|
|
25
|
+
const eg3File = new File(testDir.filepath(base));
|
|
26
|
+
const eg3 = eg3File.json();
|
|
27
|
+
assert(eg1.path === eg2.path && eg2.path === eg3.path);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('File.ndjson', () => {
|
|
32
|
+
it('Appends new lines correctly', () => {
|
|
33
|
+
const file = testDir.file('empty-lines').ndjson();
|
|
34
|
+
file.append([thing, thing]);
|
|
35
|
+
assert(file.lines().length === 2);
|
|
36
|
+
file.append(thing);
|
|
37
|
+
assert(file.lines().length === 3);
|
|
38
|
+
file.lines().forEach((line) => {
|
|
39
|
+
assert(isEqual(line, thing));
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('Adds file extension when needed', () => {
|
|
44
|
+
const test = testDir.file('test').ndjson();
|
|
45
|
+
assert(test.path.includes(testDir.path));
|
|
46
|
+
assert(test.path.includes('.ndjson'));
|
|
47
|
+
const test2 = testDir.file('test2').ndjson();
|
|
48
|
+
assert(!test2.path.includes('.ndjson.ndjson'));
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('File.json', () => {
|
|
53
|
+
it('Saves data as json', () => {
|
|
54
|
+
const file = testDir.file('jsonfile-data').json<typeof thing>();
|
|
55
|
+
file.write(thing);
|
|
56
|
+
assert(isEqual(file.read(), thing));
|
|
57
|
+
file.write(thing);
|
|
58
|
+
assert(isEqual(file.read(), thing));
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('Does not create file when reading', () => {
|
|
62
|
+
const file = testDir.file('test123').json();
|
|
63
|
+
const contents = file.read();
|
|
64
|
+
assert(contents === undefined);
|
|
65
|
+
assert(!file.exists);
|
|
66
|
+
});
|
|
67
|
+
});
|
package/src/File.ts
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { snapshot } from './snapshot.js';
|
|
4
|
+
|
|
5
|
+
export class File {
|
|
6
|
+
path;
|
|
7
|
+
|
|
8
|
+
constructor(filepath: string) {
|
|
9
|
+
this.path = filepath;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get exists() {
|
|
13
|
+
return fs.existsSync(this.path);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
read() {
|
|
17
|
+
return this.exists ? fs.readFileSync(this.path, 'utf8') : undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
write(contents: string) {
|
|
21
|
+
fs.mkdirSync(path.parse(this.path).dir, { recursive: true });
|
|
22
|
+
fs.writeFileSync(this.path, contents);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* creates file if it doesn't exist, appends string or array of strings as new lines.
|
|
27
|
+
* File always ends with '\n', so contents don't need to be read before appending
|
|
28
|
+
*/
|
|
29
|
+
append(lines: string | string[]) {
|
|
30
|
+
if (!this.exists) this.write('');
|
|
31
|
+
const contents = Array.isArray(lines) ? lines.join('\n') : lines;
|
|
32
|
+
fs.appendFileSync(this.path, contents + '\n');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @returns lines as strings, removes trailing '\n'
|
|
37
|
+
*/
|
|
38
|
+
lines() {
|
|
39
|
+
const contents = (this.read() || '').split('\n');
|
|
40
|
+
return contents.slice(0, contents.length - 1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
static get Adaptor() {
|
|
44
|
+
return FileAdaptor;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
json<T>() {
|
|
48
|
+
return new JsonFile<T>(this);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
static get json() {
|
|
52
|
+
return JsonFile;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
ndjson<T>() {
|
|
56
|
+
return new NdjsonFile<T>(this);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static get ndjson() {
|
|
60
|
+
return NdjsonFile;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class FileAdaptor {
|
|
65
|
+
file;
|
|
66
|
+
|
|
67
|
+
constructor(file: string | File) {
|
|
68
|
+
if (file instanceof File) {
|
|
69
|
+
const withExt = this.addExt(file.path);
|
|
70
|
+
if (withExt === file.path) {
|
|
71
|
+
this.file = file;
|
|
72
|
+
} else {
|
|
73
|
+
this.file = new File(withExt);
|
|
74
|
+
}
|
|
75
|
+
} else {
|
|
76
|
+
this.file = new File(this.addExt(file));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
addExt(filepath: string) {
|
|
81
|
+
return filepath;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
get exists() {
|
|
85
|
+
return this.file.exists;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
get path() {
|
|
89
|
+
return this.file.path;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
class JsonFile<T> extends FileAdaptor {
|
|
94
|
+
addExt(filepath: string) {
|
|
95
|
+
return filepath.endsWith('.json') ? filepath : filepath + '.json';
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
read() {
|
|
99
|
+
const contents = this.file.read();
|
|
100
|
+
return contents ? (JSON.parse(contents) as T) : undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
write(contents: T) {
|
|
104
|
+
this.file.write(JSON.stringify(snapshot(contents), null, 2));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
class NdjsonFile<T> extends FileAdaptor {
|
|
109
|
+
addExt(filepath: string) {
|
|
110
|
+
return filepath.endsWith('.ndjson') ? filepath : filepath + '.ndjson';
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
lines() {
|
|
114
|
+
return this.file.lines().map((l) => JSON.parse(l) as T);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
append(lines: T | T[]) {
|
|
118
|
+
const contents = Array.isArray(lines)
|
|
119
|
+
? lines.map((l) => JSON.stringify(snapshot(l)))
|
|
120
|
+
: JSON.stringify(snapshot(lines));
|
|
121
|
+
this.file.append(contents);
|
|
122
|
+
}
|
|
123
|
+
}
|
package/src/Jwt.ts
CHANGED
|
@@ -1,31 +1,22 @@
|
|
|
1
1
|
import { default as jsonwebtoken, type JwtPayload, type SignOptions } from 'jsonwebtoken';
|
|
2
2
|
import { merge } from 'lodash-es';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
type JwtConfig = {
|
|
5
5
|
payload: JwtPayload;
|
|
6
6
|
options: SignOptions;
|
|
7
7
|
seconds: number;
|
|
8
8
|
key: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export class Jwt {
|
|
12
|
+
config;
|
|
9
13
|
#saved?: {
|
|
10
14
|
exp: number;
|
|
11
15
|
token: string;
|
|
12
16
|
};
|
|
13
17
|
|
|
14
|
-
constructor({
|
|
15
|
-
|
|
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;
|
|
18
|
+
constructor(config: JwtConfig) {
|
|
19
|
+
this.config = config;
|
|
29
20
|
this.#createToken();
|
|
30
21
|
}
|
|
31
22
|
|
|
@@ -33,24 +24,24 @@ export class Jwt {
|
|
|
33
24
|
return Math.floor(Date.now() / 1000);
|
|
34
25
|
}
|
|
35
26
|
|
|
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
27
|
#createToken() {
|
|
44
|
-
const exp = this.now + this.seconds;
|
|
28
|
+
const exp = this.now + this.config.seconds;
|
|
45
29
|
const payload: JwtPayload = merge(
|
|
46
30
|
{
|
|
47
31
|
iat: this.now,
|
|
48
32
|
exp,
|
|
49
33
|
},
|
|
50
|
-
this.payload
|
|
34
|
+
this.config.payload
|
|
51
35
|
);
|
|
52
|
-
const token = jsonwebtoken.sign(payload, this.key, this.options);
|
|
36
|
+
const token = jsonwebtoken.sign(payload, this.config.key, this.config.options);
|
|
53
37
|
this.#saved = { token, exp };
|
|
54
38
|
return token;
|
|
55
39
|
}
|
|
40
|
+
|
|
41
|
+
get token() {
|
|
42
|
+
if (this.#saved && this.#saved.exp > this.now) {
|
|
43
|
+
return this.#saved.token;
|
|
44
|
+
}
|
|
45
|
+
return this.#createToken();
|
|
46
|
+
}
|
|
56
47
|
}
|
package/src/_index.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
export { Dir, TempDir, temp } from './Dir.js';
|
|
2
|
+
export { Cache } from './Cache.js';
|
|
1
3
|
export { Fetcher, type Route, Query, FetchOptions } from './Fetcher.js';
|
|
4
|
+
export { File } from './File.js';
|
|
2
5
|
export { Jwt } from './Jwt.js';
|
|
3
|
-
export { snapshot } from './snapshot.js';
|
|
4
6
|
export { Log } from './Log.js';
|
|
7
|
+
export { snapshot } from './snapshot.js';
|
|
8
|
+
export { timeout } from './timeout.js';
|
|
5
9
|
export { TypeWriter } from './TypeWriter.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert';
|
|
3
|
+
import { timeout } from './timeout.js';
|
|
4
|
+
|
|
5
|
+
describe('timeout', () => {
|
|
6
|
+
it('Waits correct amount of time', async () => {
|
|
7
|
+
const start = Date.now();
|
|
8
|
+
await timeout(500);
|
|
9
|
+
assert(Date.now() - start >= 500);
|
|
10
|
+
});
|
|
11
|
+
});
|