@gtkx/utils 0.21.0 → 1.0.0-rc.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 +173 -0
- package/dist/class.d.ts +17 -11
- package/dist/class.d.ts.map +1 -1
- package/dist/class.js +27 -1
- package/dist/class.js.map +1 -1
- package/dist/collection.d.ts +17 -45
- package/dist/collection.d.ts.map +1 -1
- package/dist/collection.js +42 -76
- package/dist/collection.js.map +1 -1
- package/dist/error.d.ts +16 -8
- package/dist/error.d.ts.map +1 -1
- package/dist/error.js +35 -8
- package/dist/error.js.map +1 -1
- package/dist/graceful-shutdown.d.ts +9 -57
- package/dist/graceful-shutdown.d.ts.map +1 -1
- package/dist/graceful-shutdown.js +70 -75
- package/dist/graceful-shutdown.js.map +1 -1
- package/dist/index.d.ts +9 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- package/dist/index.js.map +1 -1
- package/dist/log.d.ts +103 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +129 -0
- package/dist/log.js.map +1 -0
- package/dist/package-version.d.ts +7 -0
- package/dist/package-version.d.ts.map +1 -0
- package/dist/package-version.js +8 -0
- package/dist/package-version.js.map +1 -0
- package/dist/reflect.d.ts +11 -0
- package/dist/reflect.d.ts.map +1 -0
- package/dist/reflect.js +14 -0
- package/dist/reflect.js.map +1 -0
- package/dist/source.d.ts +12 -25
- package/dist/source.d.ts.map +1 -1
- package/dist/source.js +20 -41
- package/dist/source.js.map +1 -1
- package/dist/string.d.ts +13 -40
- package/dist/string.d.ts.map +1 -1
- package/dist/string.js +18 -48
- package/dist/string.js.map +1 -1
- package/package.json +20 -7
- package/src/class.ts +30 -11
- package/src/collection.ts +43 -81
- package/src/error.ts +38 -8
- package/src/graceful-shutdown.ts +87 -114
- package/src/index.ts +18 -9
- package/src/log.ts +164 -0
- package/src/package-version.ts +9 -0
- package/src/reflect.ts +13 -0
- package/src/source.ts +24 -43
- package/src/string.ts +19 -47
package/src/error.ts
CHANGED
|
@@ -1,12 +1,42 @@
|
|
|
1
|
+
const isObject = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null;
|
|
2
|
+
|
|
3
|
+
const isErrorLike = (value: unknown): value is { message: string } =>
|
|
4
|
+
isObject(value) && "message" in value && typeof value.message === "string";
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
|
-
*
|
|
7
|
+
* Extracts a human-readable message from an unknown thrown value, falling back to its string form.
|
|
3
8
|
*
|
|
4
|
-
*
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
* @param error The caught value to describe.
|
|
10
|
+
*/
|
|
11
|
+
export const errorMessage = (error: unknown): string =>
|
|
12
|
+
Error.isError(error) || isErrorLike(error) ? error.message : String(error);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Coerces an unknown thrown value into an `Error`, reusing it if it already is one and otherwise
|
|
16
|
+
* wrapping its message while copying any error-like own properties.
|
|
17
|
+
*
|
|
18
|
+
* @param error The caught value to normalize.
|
|
19
|
+
*/
|
|
20
|
+
export const normalizeError = (error: unknown): Error => {
|
|
21
|
+
if (Error.isError(error)) return error;
|
|
22
|
+
return Object.assign(new Error(errorMessage(error)), isErrorLike(error) ? error : {});
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const readStream = (value: unknown): string => {
|
|
26
|
+
if (typeof value === "string") return value;
|
|
27
|
+
if (value instanceof Uint8Array || Buffer.isBuffer(value)) return value.toString();
|
|
28
|
+
return "";
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Combines the `stderr` and `stdout` fields of a failed child-process error into a single trimmed string.
|
|
8
33
|
*
|
|
9
|
-
* @param error -
|
|
10
|
-
* @returns
|
|
34
|
+
* @param error The child-process error to read output from.
|
|
35
|
+
* @returns The joined output, or `undefined` when neither stream carried any text.
|
|
11
36
|
*/
|
|
12
|
-
export const
|
|
37
|
+
export const formatChildProcessError = (error: unknown): string | undefined => {
|
|
38
|
+
if (!isObject(error)) return undefined;
|
|
39
|
+
const { stderr, stdout } = error;
|
|
40
|
+
const details = [readStream(stderr), readStream(stdout)].filter(Boolean).join("\n").trim();
|
|
41
|
+
return details.length > 0 ? details : undefined;
|
|
42
|
+
};
|
package/src/graceful-shutdown.ts
CHANGED
|
@@ -1,141 +1,114 @@
|
|
|
1
|
-
|
|
2
|
-
* Cross-process graceful shutdown primitive for long-running Node processes.
|
|
3
|
-
*
|
|
4
|
-
* The helper installs `SIGINT`/`SIGTERM`/`SIGHUP` handlers, routes the first
|
|
5
|
-
* delivered signal through a user-supplied close callback, and escalates either
|
|
6
|
-
* on a second `SIGINT` or after a configurable timeout. On completion it calls
|
|
7
|
-
* `process.exit` with the canonical exit code for the signal.
|
|
8
|
-
*/
|
|
1
|
+
import { error } from "./log.js";
|
|
9
2
|
|
|
10
|
-
const HANDLED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"] as const satisfies
|
|
3
|
+
const HANDLED_SIGNALS = ["SIGINT", "SIGTERM", "SIGHUP"] as const satisfies NodeJS.Signals[];
|
|
11
4
|
const DEFAULT_FORCE_KILL_TIMEOUT_MS = 5000;
|
|
5
|
+
const DEFAULT_COALESCE_WINDOW_MS = 500;
|
|
12
6
|
|
|
13
7
|
/**
|
|
14
|
-
* Maps
|
|
15
|
-
*
|
|
16
|
-
* `143` (`SIGTERM`). A `null` signal — i.e. a clean exit — yields `0`.
|
|
8
|
+
* Maps a terminating signal to its conventional process exit code (130 for `SIGINT`, 143 otherwise),
|
|
9
|
+
* or 0 when no signal is given.
|
|
17
10
|
*
|
|
18
|
-
* @param signal
|
|
19
|
-
* @returns The exit code shells expect for `signal`.
|
|
11
|
+
* @param signal The signal that triggered termination, or `null`.
|
|
20
12
|
*/
|
|
21
13
|
export const exitCodeForSignal = (signal: NodeJS.Signals | null): number => {
|
|
22
14
|
if (!signal) return 0;
|
|
23
15
|
return signal === "SIGINT" ? 130 : 143;
|
|
24
16
|
};
|
|
25
17
|
|
|
26
|
-
/**
|
|
27
|
-
* Caller-supplied behaviour for {@link installGracefulShutdown}.
|
|
28
|
-
*/
|
|
29
18
|
export type GracefulShutdownOptions = {
|
|
30
|
-
/**
|
|
31
|
-
* Invoked once on the first delivered signal. Its returned promise is
|
|
32
|
-
* awaited before {@link process.exit} fires; rejection is logged but does
|
|
33
|
-
* not block the exit.
|
|
34
|
-
*/
|
|
35
19
|
onSignal: (signal: NodeJS.Signals) => void | Promise<void>;
|
|
36
|
-
/**
|
|
37
|
-
* Invoked when escalation is required: on a second `SIGINT`, or when
|
|
38
|
-
* {@link GracefulShutdownOptions.forceKillAfterMs} elapses before the
|
|
39
|
-
* primary close finishes.
|
|
40
|
-
*/
|
|
41
20
|
onForce?: () => void;
|
|
42
|
-
/**
|
|
43
|
-
* Milliseconds to wait for the primary close before invoking `onForce`
|
|
44
|
-
* and exiting. Defaults to {@link DEFAULT_FORCE_KILL_TIMEOUT_MS}. Set to
|
|
45
|
-
* `0` to disable timeout-based escalation.
|
|
46
|
-
*/
|
|
47
21
|
forceKillAfterMs?: number;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
* when the caller wants to propagate a child's own exit code instead.
|
|
51
|
-
*/
|
|
52
|
-
exitCode?: (signal: NodeJS.Signals) => number;
|
|
22
|
+
coalesceWindowMs?: number;
|
|
23
|
+
exitCode?: (signal: NodeJS.Signals, graceful: boolean) => number;
|
|
53
24
|
};
|
|
54
25
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
uninstall: () => void;
|
|
26
|
+
type ShutdownState = {
|
|
27
|
+
options: GracefulShutdownOptions;
|
|
28
|
+
forceKillMs: number;
|
|
29
|
+
coalesceWindowMs: number;
|
|
30
|
+
firstSignal: NodeJS.Signals | null;
|
|
31
|
+
exited: boolean;
|
|
32
|
+
coalescing: boolean;
|
|
33
|
+
forceTimer: NodeJS.Timeout | null;
|
|
34
|
+
coalesceTimer: NodeJS.Timeout | null;
|
|
65
35
|
};
|
|
66
36
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
* @returns A handle that detaches the installed signal handlers.
|
|
78
|
-
*/
|
|
79
|
-
export const installGracefulShutdown = (options: GracefulShutdownOptions): GracefulShutdownHandle => {
|
|
80
|
-
const forceKillMs = options.forceKillAfterMs ?? DEFAULT_FORCE_KILL_TIMEOUT_MS;
|
|
37
|
+
const clearTimers = (state: ShutdownState): void => {
|
|
38
|
+
if (state.forceTimer) {
|
|
39
|
+
clearTimeout(state.forceTimer);
|
|
40
|
+
state.forceTimer = null;
|
|
41
|
+
}
|
|
42
|
+
if (state.coalesceTimer) {
|
|
43
|
+
clearTimeout(state.coalesceTimer);
|
|
44
|
+
state.coalesceTimer = null;
|
|
45
|
+
}
|
|
46
|
+
};
|
|
81
47
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
48
|
+
const finish = (state: ShutdownState, signal: NodeJS.Signals, graceful: boolean): void => {
|
|
49
|
+
if (state.exited) return;
|
|
50
|
+
state.exited = true;
|
|
51
|
+
clearTimers(state);
|
|
52
|
+
const { exitCode } = state.options;
|
|
53
|
+
const code = exitCode ? exitCode(signal, graceful) : graceful ? 0 : exitCodeForSignal(signal);
|
|
54
|
+
process.exit(code);
|
|
55
|
+
};
|
|
85
56
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
57
|
+
const beginShutdown = (state: ShutdownState, signal: NodeJS.Signals): void => {
|
|
58
|
+
state.firstSignal = signal;
|
|
59
|
+
if (state.coalesceWindowMs > 0) {
|
|
60
|
+
state.coalescing = true;
|
|
61
|
+
state.coalesceTimer = setTimeout(() => {
|
|
62
|
+
state.coalescing = false;
|
|
63
|
+
}, state.coalesceWindowMs);
|
|
64
|
+
state.coalesceTimer.unref();
|
|
65
|
+
}
|
|
66
|
+
if (state.options.onForce && state.forceKillMs > 0) {
|
|
67
|
+
state.forceTimer = setTimeout(() => {
|
|
68
|
+
state.options.onForce?.();
|
|
69
|
+
finish(state, signal, false);
|
|
70
|
+
}, state.forceKillMs);
|
|
71
|
+
state.forceTimer.unref();
|
|
72
|
+
}
|
|
73
|
+
Promise.resolve()
|
|
74
|
+
.then(() => state.options.onSignal(signal))
|
|
75
|
+
.then(
|
|
76
|
+
() => finish(state, signal, true),
|
|
77
|
+
(reason: unknown) => {
|
|
78
|
+
error("graceful shutdown failed", reason);
|
|
79
|
+
finish(state, signal, false);
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
};
|
|
92
83
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
84
|
+
const handle = (state: ShutdownState, signal: NodeJS.Signals): void => {
|
|
85
|
+
if (state.firstSignal === null) {
|
|
86
|
+
beginShutdown(state, signal);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
if (state.coalescing) return;
|
|
90
|
+
state.options.onForce?.();
|
|
91
|
+
finish(state, signal, false);
|
|
92
|
+
};
|
|
100
93
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
if (signal === "SIGINT" && firstSignal === "SIGINT") {
|
|
120
|
-
options.onForce?.();
|
|
121
|
-
finish(signal);
|
|
122
|
-
}
|
|
94
|
+
/**
|
|
95
|
+
* Registers handlers for `SIGINT`, `SIGTERM`, and `SIGHUP` that run the given cleanup callback once
|
|
96
|
+
* and then exit, forcing exit if a repeated signal arrives or the cleanup exceeds its timeout.
|
|
97
|
+
*
|
|
98
|
+
* @param options The shutdown callbacks and timing configuration.
|
|
99
|
+
*/
|
|
100
|
+
export const installGracefulShutdown = (options: GracefulShutdownOptions): void => {
|
|
101
|
+
const state: ShutdownState = {
|
|
102
|
+
options,
|
|
103
|
+
forceKillMs: options.forceKillAfterMs ?? DEFAULT_FORCE_KILL_TIMEOUT_MS,
|
|
104
|
+
coalesceWindowMs: options.coalesceWindowMs ?? DEFAULT_COALESCE_WINDOW_MS,
|
|
105
|
+
firstSignal: null,
|
|
106
|
+
exited: false,
|
|
107
|
+
coalescing: false,
|
|
108
|
+
forceTimer: null,
|
|
109
|
+
coalesceTimer: null,
|
|
123
110
|
};
|
|
124
|
-
|
|
125
|
-
const handlers = new Map<NodeJS.Signals, () => void>();
|
|
126
111
|
for (const sig of HANDLED_SIGNALS) {
|
|
127
|
-
|
|
128
|
-
handlers.set(sig, listener);
|
|
129
|
-
process.on(sig, listener);
|
|
112
|
+
process.on(sig, () => handle(state, sig));
|
|
130
113
|
}
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
uninstall: () => {
|
|
134
|
-
for (const [sig, listener] of handlers) {
|
|
135
|
-
process.removeListener(sig, listener);
|
|
136
|
-
}
|
|
137
|
-
handlers.clear();
|
|
138
|
-
clearTimer();
|
|
139
|
-
},
|
|
140
|
-
};
|
|
141
114
|
};
|
package/src/index.ts
CHANGED
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
export type
|
|
2
|
-
export {
|
|
3
|
-
export { errorMessage } from "./error.js";
|
|
1
|
+
export { type AnyClass, getParentClass, walkClassChain } from "./class.js";
|
|
2
|
+
export { isSameArray, isShallowEqual, sortStrings, sortStringsBy, uniqBy } from "./collection.js";
|
|
3
|
+
export { errorMessage, formatChildProcessError, normalizeError } from "./error.js";
|
|
4
|
+
export { exitCodeForSignal, installGracefulShutdown } from "./graceful-shutdown.js";
|
|
4
5
|
export {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
createLogger,
|
|
7
|
+
debug,
|
|
8
|
+
error,
|
|
9
|
+
info,
|
|
10
|
+
Logger,
|
|
11
|
+
type LoggerOptions,
|
|
12
|
+
logger,
|
|
13
|
+
type OutputStream,
|
|
14
|
+
warn,
|
|
15
|
+
} from "./log.js";
|
|
16
|
+
export { packageVersion } from "./package-version.js";
|
|
17
|
+
export { callMethod } from "./reflect.js";
|
|
18
|
+
export { sanitizeIdentifier, sourceStringLiteral, toCamelIdentifier } from "./source.js";
|
|
19
|
+
export { lowerFirst, toCamelCase, toKebabCase, toPascalCase, upperFirst } from "./string.js";
|
package/src/log.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
|
|
3
|
+
const BASE_PREFIX = "[gtkx]";
|
|
4
|
+
|
|
5
|
+
type Colors = ReturnType<typeof pc.createColors>;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Minimal writable-stream shape a {@link Logger} writes formatted lines to.
|
|
9
|
+
*/
|
|
10
|
+
export type OutputStream = {
|
|
11
|
+
write(chunk: string): unknown;
|
|
12
|
+
/** Whether the stream is a terminal, used to decide if colored output is emitted. */
|
|
13
|
+
isTTY?: boolean | undefined;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Options for constructing a {@link Logger}.
|
|
18
|
+
*/
|
|
19
|
+
export type LoggerOptions = {
|
|
20
|
+
/** Namespace appended to the log prefix and matched against debug configuration. */
|
|
21
|
+
namespace?: string | undefined;
|
|
22
|
+
/** Stream to write log lines to; defaults to `process.stderr`. */
|
|
23
|
+
stream?: OutputStream | undefined;
|
|
24
|
+
/** Forces debug output on or off; when omitted it is resolved from `--debug` and `GTKX_DEBUG`. */
|
|
25
|
+
debugEnabled?: boolean | undefined;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const colorsFor = (stream: OutputStream): Colors => pc.createColors(pc.isColorSupported && stream.isTTY === true);
|
|
29
|
+
|
|
30
|
+
const formatValue = (value: unknown): string => {
|
|
31
|
+
if (typeof value === "string") return value;
|
|
32
|
+
if (value instanceof Error) return value.stack ?? value.message;
|
|
33
|
+
try {
|
|
34
|
+
return JSON.stringify(value);
|
|
35
|
+
} catch {
|
|
36
|
+
return String(value);
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const resolveDebugEnabled = (namespace: string | undefined, argv: string[], env: NodeJS.ProcessEnv): boolean => {
|
|
41
|
+
if (argv.includes("--debug")) return true;
|
|
42
|
+
const spec = env.GTKX_DEBUG;
|
|
43
|
+
if (!spec) return false;
|
|
44
|
+
const names = spec.split(/[\s,]+/).filter((name) => name.length > 0);
|
|
45
|
+
if (names.includes("1") || names.includes("*")) return true;
|
|
46
|
+
return namespace !== undefined && names.includes(namespace);
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const prefixFor = (namespace: string | undefined): string =>
|
|
50
|
+
namespace === undefined ? BASE_PREFIX : `[gtkx:${namespace}]`;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Writes prefixed, optionally colored log lines to an output stream, with debug lines gated by
|
|
54
|
+
* command-line and environment configuration.
|
|
55
|
+
*/
|
|
56
|
+
export class Logger {
|
|
57
|
+
private stream: OutputStream;
|
|
58
|
+
private prefix: string;
|
|
59
|
+
private debugEnabled: boolean;
|
|
60
|
+
private colors: Colors;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param options Namespace, target stream, and debug configuration for the logger.
|
|
64
|
+
*/
|
|
65
|
+
constructor(options: LoggerOptions = {}) {
|
|
66
|
+
this.stream = options.stream ?? process.stderr;
|
|
67
|
+
this.prefix = prefixFor(options.namespace);
|
|
68
|
+
this.debugEnabled = options.debugEnabled ?? resolveDebugEnabled(options.namespace, process.argv, process.env);
|
|
69
|
+
this.colors = colorsFor(this.stream);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private write(message: string, rest: unknown[]): void {
|
|
73
|
+
const suffix = rest.length === 0 ? "" : ` ${rest.map(formatValue).join(" ")}`;
|
|
74
|
+
this.stream.write(`${this.prefix} ${message}${suffix}\n`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Writes an informational line.
|
|
79
|
+
*
|
|
80
|
+
* @param message The message text.
|
|
81
|
+
* @param rest Extra values appended after the message, formatted for display.
|
|
82
|
+
*/
|
|
83
|
+
info(message: string, ...rest: unknown[]): void {
|
|
84
|
+
this.write(message, rest);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Writes a line marked as a warning.
|
|
89
|
+
*
|
|
90
|
+
* @param message The message text.
|
|
91
|
+
* @param rest Extra values appended after the message, formatted for display.
|
|
92
|
+
*/
|
|
93
|
+
warn(message: string, ...rest: unknown[]): void {
|
|
94
|
+
this.write(`${this.colors.yellow("warn")} ${message}`, rest);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Writes a line marked as an error.
|
|
99
|
+
*
|
|
100
|
+
* @param message The message text.
|
|
101
|
+
* @param rest Extra values appended after the message, formatted for display.
|
|
102
|
+
*/
|
|
103
|
+
error(message: string, ...rest: unknown[]): void {
|
|
104
|
+
this.write(`${this.colors.red("error")} ${message}`, rest);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Writes a line only when debug output is enabled for this logger.
|
|
109
|
+
*
|
|
110
|
+
* @param message The message text.
|
|
111
|
+
* @param rest Extra values appended after the message, formatted for display.
|
|
112
|
+
*/
|
|
113
|
+
debug(message: string, ...rest: unknown[]): void {
|
|
114
|
+
if (!this.debugEnabled) return;
|
|
115
|
+
this.write(message, rest);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Creates a {@link Logger} scoped to the given namespace.
|
|
121
|
+
*
|
|
122
|
+
* @param namespace Namespace added to the log prefix and matched against debug configuration.
|
|
123
|
+
* @param options Further logger options excluding the namespace.
|
|
124
|
+
*/
|
|
125
|
+
export const createLogger = (namespace: string, options: Omit<LoggerOptions, "namespace"> = {}): Logger =>
|
|
126
|
+
new Logger({ ...options, namespace });
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* The default namespace-less {@link Logger} backing the module-level {@link info}, {@link warn},
|
|
130
|
+
* {@link error}, and {@link debug} functions.
|
|
131
|
+
*/
|
|
132
|
+
export const logger: Logger = new Logger();
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Writes an informational line through the shared {@link logger}.
|
|
136
|
+
*
|
|
137
|
+
* @param message The message text.
|
|
138
|
+
* @param rest Extra values appended after the message, formatted for display.
|
|
139
|
+
*/
|
|
140
|
+
export const info = (message: string, ...rest: unknown[]): void => logger.info(message, ...rest);
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Writes a warning line through the shared {@link logger}.
|
|
144
|
+
*
|
|
145
|
+
* @param message The message text.
|
|
146
|
+
* @param rest Extra values appended after the message, formatted for display.
|
|
147
|
+
*/
|
|
148
|
+
export const warn = (message: string, ...rest: unknown[]): void => logger.warn(message, ...rest);
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Writes an error line through the shared {@link logger}.
|
|
152
|
+
*
|
|
153
|
+
* @param message The message text.
|
|
154
|
+
* @param rest Extra values appended after the message, formatted for display.
|
|
155
|
+
*/
|
|
156
|
+
export const error = (message: string, ...rest: unknown[]): void => logger.error(message, ...rest);
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Writes a debug line through the shared {@link logger} when debug output is enabled.
|
|
160
|
+
*
|
|
161
|
+
* @param message The message text.
|
|
162
|
+
* @param rest Extra values appended after the message, formatted for display.
|
|
163
|
+
*/
|
|
164
|
+
export const debug = (message: string, ...rest: unknown[]): void => logger.debug(message, ...rest);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reads the `version` field from the `package.json` next to the calling module.
|
|
5
|
+
*
|
|
6
|
+
* @param importMetaUrl The caller's `import.meta.url`, used to resolve the sibling `package.json`.
|
|
7
|
+
*/
|
|
8
|
+
export const packageVersion = (importMetaUrl: string): string =>
|
|
9
|
+
(createRequire(importMetaUrl)("../package.json") as { version: string }).version;
|
package/src/reflect.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Calls the named method on `target` with the given arguments, returning `undefined` when the
|
|
3
|
+
* property is missing or not callable.
|
|
4
|
+
*
|
|
5
|
+
* @param target The object to read the method from and bind as `this`.
|
|
6
|
+
* @param method The method name to look up.
|
|
7
|
+
* @param args Arguments passed to the method.
|
|
8
|
+
* @returns The method's return value, or `undefined` when it is not a function.
|
|
9
|
+
*/
|
|
10
|
+
export const callMethod = (target: object, method: string, args: unknown[]): unknown => {
|
|
11
|
+
const fn = Reflect.get(target, method);
|
|
12
|
+
return typeof fn === "function" ? Reflect.apply(fn, target, args) : undefined;
|
|
13
|
+
};
|
package/src/source.ts
CHANGED
|
@@ -1,11 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
* Helpers for shaping values into safe JavaScript/TypeScript source fragments:
|
|
3
|
-
* a reserved-word-safe identifier and a source-safe string literal. Both are
|
|
4
|
-
* pure and runtime-agnostic, intended for code generators that emit TypeScript.
|
|
5
|
-
*/
|
|
1
|
+
import { toCamelCase } from "./string.js";
|
|
6
2
|
|
|
7
|
-
|
|
8
|
-
const RESERVED: ReadonlySet<string> = new Set([
|
|
3
|
+
const RESERVED: Set<string> = new Set([
|
|
9
4
|
"arguments",
|
|
10
5
|
"await",
|
|
11
6
|
"break",
|
|
@@ -56,48 +51,34 @@ const RESERVED: ReadonlySet<string> = new Set([
|
|
|
56
51
|
]);
|
|
57
52
|
|
|
58
53
|
/**
|
|
59
|
-
*
|
|
60
|
-
*
|
|
54
|
+
* Returns the name unchanged, or with a trailing underscore when it collides with a reserved word,
|
|
55
|
+
* so it is safe to emit as a JavaScript identifier.
|
|
61
56
|
*
|
|
62
|
-
*
|
|
63
|
-
* example the output of a case-conversion helper); the only transformation
|
|
64
|
-
* applied is appending an underscore when `name` collides with a reserved
|
|
65
|
-
* word or global identifier, so `toIdentifier("class")` is `"class_"` and
|
|
66
|
-
* `toIdentifier("iconName")` is `"iconName"`.
|
|
67
|
-
*
|
|
68
|
-
* @param name - The candidate identifier.
|
|
69
|
-
* @returns A reserved-word-safe identifier.
|
|
57
|
+
* @param name The candidate identifier.
|
|
70
58
|
*/
|
|
71
|
-
export const
|
|
59
|
+
export const sanitizeIdentifier = (name: string): string => (RESERVED.has(name) ? `${name}_` : name);
|
|
72
60
|
|
|
73
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Converts a name to camelCase and sanitizes it into a valid JavaScript identifier.
|
|
63
|
+
*
|
|
64
|
+
* @param name The name to convert.
|
|
65
|
+
*/
|
|
66
|
+
export const toCamelIdentifier = (name: string): string => sanitizeIdentifier(toCamelCase(name));
|
|
74
67
|
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
return "\\u003E";
|
|
81
|
-
case "\u2028":
|
|
82
|
-
return "\\u2028";
|
|
83
|
-
case "\u2029":
|
|
84
|
-
return "\\u2029";
|
|
85
|
-
default:
|
|
86
|
-
return char;
|
|
87
|
-
}
|
|
68
|
+
const SOURCE_ESCAPES: Record<string, string> = {
|
|
69
|
+
"<": "\\u003C",
|
|
70
|
+
">": "\\u003E",
|
|
71
|
+
"\u2028": "\\u2028",
|
|
72
|
+
"\u2029": "\\u2029",
|
|
88
73
|
};
|
|
89
74
|
|
|
75
|
+
const UNSAFE_SOURCE_CHARS = new RegExp(`[${Object.keys(SOURCE_ESCAPES).join("")}]`, "g");
|
|
76
|
+
|
|
90
77
|
/**
|
|
91
|
-
*
|
|
92
|
-
* source.
|
|
93
|
-
*
|
|
94
|
-
* Builds a double-quoted literal with `JSON.stringify`, then escapes the
|
|
95
|
-
* characters that are valid inside a JSON string yet unsafe inside JavaScript
|
|
96
|
-
* source: the angle brackets that could otherwise break out of an enclosing
|
|
97
|
-
* `</script>` and the U+2028/U+2029 line separators that prematurely terminate
|
|
98
|
-
* a string literal. The escaped form parses back to the original value.
|
|
78
|
+
* Encodes a string as a JavaScript string literal, additionally escaping characters that are unsafe
|
|
79
|
+
* to embed in generated source (angle brackets and the line and paragraph separators).
|
|
99
80
|
*
|
|
100
|
-
* @param value
|
|
101
|
-
* @returns A source-safe double-quoted string literal.
|
|
81
|
+
* @param value The string to encode.
|
|
102
82
|
*/
|
|
103
|
-
export const
|
|
83
|
+
export const sourceStringLiteral = (value: string): string =>
|
|
84
|
+
JSON.stringify(value).replace(UNSAFE_SOURCE_CHARS, (char) => SOURCE_ESCAPES[char] ?? char);
|