@dbx-tools/shared-core 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.projen/deps.json +29 -0
- package/.projen/files.json +11 -0
- package/.projen/tasks.json +121 -0
- package/README.md +220 -0
- package/index.ts +26 -0
- package/package.json +43 -0
- package/src/async.ts +209 -0
- package/src/error.ts +178 -0
- package/src/function.ts +91 -0
- package/src/hash.ts +261 -0
- package/src/http.ts +223 -0
- package/src/iterable.ts +790 -0
- package/src/log.ts +380 -0
- package/src/net.ts +535 -0
- package/src/object.ts +165 -0
- package/src/predicate.ts +151 -0
- package/src/string.ts +483 -0
- package/src/token.ts +136 -0
- package/test/tsconfig.json +14 -0
- package/tsconfig.json +40 -0
package/src/log.ts
ADDED
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tagged, leveled logging for every runtime - the one logger the whole
|
|
3
|
+
* monorepo shares.
|
|
4
|
+
*
|
|
5
|
+
* {@link logger} resolves a tagged {@link Logger} via a {@link LoggerFactory}
|
|
6
|
+
* chosen once at module load. {@link LogLevel} filtering runs through
|
|
7
|
+
* {@link shouldEmit} in the consola reporter or in per-level wrappers on the
|
|
8
|
+
* `console` fallbacks.
|
|
9
|
+
*
|
|
10
|
+
* Sink selection chains two factories with nullish coalescing (first match
|
|
11
|
+
* wins):
|
|
12
|
+
*
|
|
13
|
+
* 1. {@link createConsolaLoggerFactory} when consola resolves and
|
|
14
|
+
* `LOG_CONSOLA_DISABLED` is not truthy.
|
|
15
|
+
* 2. {@link createConsoleLoggerFactory} everywhere else.
|
|
16
|
+
*
|
|
17
|
+
* The console fallback writes formatted lines to `process.stderr` when it is
|
|
18
|
+
* available (Bun `inspect` per argument when `LOG_BUN_CONSOLE_DISABLED` is
|
|
19
|
+
* unset, otherwise `node:util` `formatWithOptions`), or delegates to global
|
|
20
|
+
* `console.*` when stderr or `node:util` is unavailable. Browser hosts omit
|
|
21
|
+
* the `LEVEL` text prefix (devtools show severity) and keep only the `[name]`
|
|
22
|
+
* tag.
|
|
23
|
+
*
|
|
24
|
+
* Env toggles (read once at init): `LOG_LEVEL`, `LOG_CONSOLA_DISABLED`,
|
|
25
|
+
* `LOG_BUN_CONSOLE_DISABLED`.
|
|
26
|
+
*
|
|
27
|
+
* Browser-safe: `process` / `Bun` / `window` / `document` are all reached
|
|
28
|
+
* through `globalThis` and guarded, and consola / `node:util` load lazily, so
|
|
29
|
+
* the module works in any runtime. Consola is an optional peer; the module
|
|
30
|
+
* loads fine without it.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { memoize } from "./function";
|
|
34
|
+
import { type NameLike, toBoolean } from "./object";
|
|
35
|
+
|
|
36
|
+
/** `process`-shaped view off `globalThis`, so no node types are needed. */
|
|
37
|
+
interface ProcessLike {
|
|
38
|
+
env?: Record<string, string | undefined>;
|
|
39
|
+
stderr?: { write?: (chunk: string) => void; isTTY?: unknown };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** `Bun`-shaped view off `globalThis`, so no `@types/bun` is needed. */
|
|
43
|
+
interface BunLike {
|
|
44
|
+
env?: Record<string, string | undefined>;
|
|
45
|
+
inspect?: (value: unknown, options?: { colors?: boolean; depth?: number }) => string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const globalProcess = (globalThis as { process?: ProcessLike }).process;
|
|
49
|
+
const globalBun = (globalThis as { Bun?: BunLike }).Bun;
|
|
50
|
+
|
|
51
|
+
const LOG_LEVELS = ["debug", "info", "warn", "error"] as const;
|
|
52
|
+
const LOG_LEVEL_RANK = Object.fromEntries(
|
|
53
|
+
LOG_LEVELS.map((level, index) => [level, index]),
|
|
54
|
+
) as Record<LogLevel, number>;
|
|
55
|
+
const LOG_LEVEL_COLORS = Object.fromEntries(
|
|
56
|
+
LOG_LEVELS.map((level) => {
|
|
57
|
+
let color = "\x1b[90m"; // gray
|
|
58
|
+
switch (level) {
|
|
59
|
+
case "info":
|
|
60
|
+
color = "\x1b[34m"; // blue
|
|
61
|
+
break;
|
|
62
|
+
case "warn":
|
|
63
|
+
color = "\x1b[33m"; // yellow
|
|
64
|
+
break;
|
|
65
|
+
case "error":
|
|
66
|
+
color = "\x1b[31m"; // red
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
return [level, color];
|
|
70
|
+
}),
|
|
71
|
+
) as Record<LogLevel, string>;
|
|
72
|
+
const LOG_LEVEL_COLOR_RESET = "\x1b[0m";
|
|
73
|
+
const DEFAULT_LEVEL: LogLevel = "info";
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Supported severities, lowest to highest: `debug`, `info`, `warn`, `error`.
|
|
77
|
+
* A call below the active threshold is discarded before any string work or
|
|
78
|
+
* sink I/O.
|
|
79
|
+
*
|
|
80
|
+
* The threshold comes from `process.env.LOG_LEVEL` on every call
|
|
81
|
+
* (case-insensitive, default `info` when unset, empty, or unknown) so test
|
|
82
|
+
* runners and embedders can change verbosity after import without restarting
|
|
83
|
+
* the process.
|
|
84
|
+
*/
|
|
85
|
+
export type LogLevel = (typeof LOG_LEVELS)[number];
|
|
86
|
+
|
|
87
|
+
export type Logger = {
|
|
88
|
+
[K in LogLevel]: (...args: any[]) => void;
|
|
89
|
+
} & {
|
|
90
|
+
/** Success message (consola `success`; falls back to `info`-level output). */
|
|
91
|
+
success: (...args: any[]) => void;
|
|
92
|
+
/** Start / in-progress message (consola `start`; falls back to `info`-level output). */
|
|
93
|
+
start: (...args: any[]) => void;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** `(name?) => Logger` sink constructor returned by the init-time factory chain. */
|
|
97
|
+
type LoggerFactory = (name?: string) => Logger;
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Consola-backed {@link LoggerFactory}, or `undefined` when consola is
|
|
101
|
+
* disabled, fails to import, or throws during setup.
|
|
102
|
+
*
|
|
103
|
+
* Builds one `createConsola` instance (badge off, level `verbose`) whose
|
|
104
|
+
* reporter calls {@link shouldEmit} on `logObj.type` before delegating to
|
|
105
|
+
* consola's default reporters. In Node, when `processStdErr` is set,
|
|
106
|
+
* recognized {@link LogLevel} types are redirected to stderr for that write.
|
|
107
|
+
* Tags use `withTag` (rendered `[name]`, not merged into args).
|
|
108
|
+
*/
|
|
109
|
+
async function createConsolaLoggerFactory(
|
|
110
|
+
globalProcessStdErr: any,
|
|
111
|
+
): Promise<LoggerFactory | undefined> {
|
|
112
|
+
const consolaDisabled = toBoolean(globalProcess?.env?.LOG_CONSOLA_DISABLED);
|
|
113
|
+
if (!consolaDisabled) {
|
|
114
|
+
try {
|
|
115
|
+
const { consola, createConsola, LogLevels } = await import("consola");
|
|
116
|
+
const defaultOptions = consola.options;
|
|
117
|
+
const createConsolaOptions = {
|
|
118
|
+
...consola.options,
|
|
119
|
+
defaults: { badge: false },
|
|
120
|
+
// `LOG_LEVEL` is enforced in {@link shouldEmit}; keep consola permissive
|
|
121
|
+
// so a threshold change after import is not blocked by its own filter.
|
|
122
|
+
level: LogLevels.verbose,
|
|
123
|
+
reporters: [
|
|
124
|
+
{
|
|
125
|
+
log: (logObj, ctx) => {
|
|
126
|
+
const logLevel = parseLogLevel(logObj.type);
|
|
127
|
+
if (!shouldEmit(logLevel, true)) return;
|
|
128
|
+
const ctxStdout = ctx.options.stdout;
|
|
129
|
+
try {
|
|
130
|
+
if (globalProcessStdErr !== undefined && logLevel !== undefined) {
|
|
131
|
+
ctx.options.stdout = globalProcessStdErr;
|
|
132
|
+
}
|
|
133
|
+
defaultOptions.reporters.forEach((reporter) => reporter.log(logObj, ctx));
|
|
134
|
+
} finally {
|
|
135
|
+
ctx.options.stdout = ctxStdout;
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
} as NonNullable<Parameters<typeof createConsola>[0]>;
|
|
141
|
+
const consolaLogger = createConsola(createConsolaOptions);
|
|
142
|
+
return (name?: string) => {
|
|
143
|
+
return name ? consolaLogger.withTag(name) : consolaLogger;
|
|
144
|
+
};
|
|
145
|
+
} catch (error) {
|
|
146
|
+
console.trace("Consola is not available, fallback to console", error);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Console {@link LoggerFactory}; always succeeds.
|
|
154
|
+
*
|
|
155
|
+
* When `process.stderr` is available, formats each line and writes only to
|
|
156
|
+
* stderr. Bun `inspect` formats each argument when enabled; otherwise
|
|
157
|
+
* `util.formatWithOptions` when `node:util` loads, or a `JSON.stringify`
|
|
158
|
+
* fallback when it does not. When stderr is unavailable, binds
|
|
159
|
+
* {@link createFormatter} prefixes into global `console.*` calls.
|
|
160
|
+
*/
|
|
161
|
+
async function createConsoleLoggerFactory(globalProcessStdErr: any): Promise<LoggerFactory> {
|
|
162
|
+
// Indirect specifier so this browser-safe module doesn't statically pull in
|
|
163
|
+
// node types: `node:util` is optional (the JSON fallback covers its absence).
|
|
164
|
+
const nodeUtil = "node:util";
|
|
165
|
+
const utils: { inspect?: any } | undefined = await import(nodeUtil).catch(() => undefined);
|
|
166
|
+
const bunInspect =
|
|
167
|
+
globalBun !== undefined && !toBoolean(globalBun.env?.LOG_BUN_CONSOLE_DISABLED)
|
|
168
|
+
? globalBun.inspect
|
|
169
|
+
: undefined;
|
|
170
|
+
const resetColorsPrefix = bunInspect !== undefined;
|
|
171
|
+
|
|
172
|
+
const inspect = (arg: any, colors?: boolean) => {
|
|
173
|
+
if (Array.isArray(arg) || (typeof arg === "object" && arg !== null)) {
|
|
174
|
+
if (bunInspect !== undefined) {
|
|
175
|
+
return bunInspect(arg, {
|
|
176
|
+
colors: colors,
|
|
177
|
+
depth: utils?.inspect?.defaultOptions?.depth ?? undefined,
|
|
178
|
+
});
|
|
179
|
+
} else if (utils !== undefined) {
|
|
180
|
+
return utils.inspect(arg, {
|
|
181
|
+
...utils?.inspect?.defaultOptions,
|
|
182
|
+
colors: false,
|
|
183
|
+
});
|
|
184
|
+
} else {
|
|
185
|
+
const seen = new WeakSet();
|
|
186
|
+
return JSON.stringify(arg, (_, value) => {
|
|
187
|
+
if (typeof value === "object" && value !== null) {
|
|
188
|
+
if (seen.has(value)) return "[Circular]";
|
|
189
|
+
seen.add(value);
|
|
190
|
+
}
|
|
191
|
+
return value;
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
} else {
|
|
195
|
+
return String(arg);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
const factory = (name?: string) => {
|
|
200
|
+
const prefixFormatter = createFormatter(name, globalProcessStdErr, resetColorsPrefix);
|
|
201
|
+
const logger = Object.fromEntries(
|
|
202
|
+
LOG_LEVELS.map((level) => {
|
|
203
|
+
const { prefix, colors, resetColors } = prefixFormatter(level);
|
|
204
|
+
const emitter = (...args: any[]) => {
|
|
205
|
+
if (!shouldEmit(level, true)) return;
|
|
206
|
+
if (globalProcessStdErr !== undefined) {
|
|
207
|
+
const messageParts = prefix ? [prefix] : [];
|
|
208
|
+
messageParts.push(...args.map((arg) => inspect(arg, colors)));
|
|
209
|
+
if (resetColors) messageParts.push(LOG_LEVEL_COLOR_RESET);
|
|
210
|
+
globalProcessStdErr.write(messageParts.join(" ") + "\n");
|
|
211
|
+
} else {
|
|
212
|
+
let levelFn = console[level];
|
|
213
|
+
if (typeof levelFn !== "function") {
|
|
214
|
+
levelFn = console.log;
|
|
215
|
+
}
|
|
216
|
+
if (prefix) levelFn = levelFn.bind(console, prefix);
|
|
217
|
+
levelFn(...args);
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
return [level, emitter];
|
|
221
|
+
}),
|
|
222
|
+
) as Logger;
|
|
223
|
+
// consola sugar the console fallback doesn't have natively - route to info.
|
|
224
|
+
logger.success = logger.info;
|
|
225
|
+
logger.start = logger.info;
|
|
226
|
+
return logger;
|
|
227
|
+
};
|
|
228
|
+
const defaultFactory = memoize(factory);
|
|
229
|
+
return (name?: string) => (name ? factory(name) : defaultFactory());
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Module-scoped {@link LoggerFactory}, initialized once via top-level `await`.
|
|
234
|
+
* Delegates to {@link createConsolaLoggerFactory}, then
|
|
235
|
+
* {@link createConsoleLoggerFactory}. Each {@link logger} call invokes the
|
|
236
|
+
* chosen factory with a resolved tag. Env toggles are read only during init.
|
|
237
|
+
*/
|
|
238
|
+
const createLogger: LoggerFactory = await (async () => {
|
|
239
|
+
const globalProcessStdErr =
|
|
240
|
+
globalProcess && typeof globalProcess.stderr?.write === "function"
|
|
241
|
+
? globalProcess.stderr
|
|
242
|
+
: undefined;
|
|
243
|
+
return (
|
|
244
|
+
(await createConsolaLoggerFactory(globalProcessStdErr)) ??
|
|
245
|
+
(await createConsoleLoggerFactory(globalProcessStdErr))
|
|
246
|
+
);
|
|
247
|
+
})();
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Build a line prefix of `LEVEL [name]` on Node/Bun hosts, or `[name]` alone
|
|
251
|
+
* in browsers (either part omitted when absent). Applies per-level ANSI color
|
|
252
|
+
* when `streamSupportsColor` is true for the given stderr stream.
|
|
253
|
+
*/
|
|
254
|
+
function createFormatter(
|
|
255
|
+
name: any,
|
|
256
|
+
stream: any,
|
|
257
|
+
resetColorsPrefix?: boolean,
|
|
258
|
+
): (level?: LogLevel) => { prefix: string; colors: boolean; resetColors: boolean } {
|
|
259
|
+
const glob = globalThis as { window?: unknown; document?: unknown };
|
|
260
|
+
const isBrowser = glob.window !== undefined && glob.document !== undefined;
|
|
261
|
+
const supportsColor = !isBrowser ? streamSupportsColor(stream) : false;
|
|
262
|
+
const namePrefix = name ? "[" + name + "]" : undefined;
|
|
263
|
+
return (level?: LogLevel) => {
|
|
264
|
+
const color = supportsColor && level !== undefined ? LOG_LEVEL_COLORS[level] : undefined;
|
|
265
|
+
let prefix = [!isBrowser && level ? level.toUpperCase() : undefined, namePrefix]
|
|
266
|
+
.filter(Boolean)
|
|
267
|
+
.join(" ");
|
|
268
|
+
let resetColors = false;
|
|
269
|
+
if (color) {
|
|
270
|
+
const applyResetColorsPrefix = resetColorsPrefix || "info" === level;
|
|
271
|
+
if (!applyResetColorsPrefix) resetColors = true;
|
|
272
|
+
prefix = applyResetColorsPrefix ? color + prefix + LOG_LEVEL_COLOR_RESET : color + prefix;
|
|
273
|
+
}
|
|
274
|
+
return { prefix, colors: color ? true : false, resetColors };
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** True when `stream` is a TTY and the terminal is not `dumb`. */
|
|
279
|
+
function streamSupportsColor(stream?: unknown): boolean {
|
|
280
|
+
if (globalProcess === undefined || typeof stream !== "object" || stream === null) return false;
|
|
281
|
+
const { isTTY } = stream as { isTTY?: unknown };
|
|
282
|
+
if (isTTY !== true) return false;
|
|
283
|
+
const term = globalProcess.env?.TERM?.toLowerCase();
|
|
284
|
+
if ("dumb" == term) return false;
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Parse a raw value as {@link LogLevel} (trimmed, case-insensitive). */
|
|
289
|
+
function parseLogLevel(raw: unknown): LogLevel | undefined {
|
|
290
|
+
if (raw === undefined || raw === null) {
|
|
291
|
+
return undefined;
|
|
292
|
+
}
|
|
293
|
+
let text = typeof raw === "string" ? raw : String(raw);
|
|
294
|
+
for (let i = 0; i < 2; i++) {
|
|
295
|
+
if (i > 0) {
|
|
296
|
+
const normalized = text.trim().toLowerCase();
|
|
297
|
+
if (text === normalized) break;
|
|
298
|
+
text = normalized;
|
|
299
|
+
}
|
|
300
|
+
if (!text) break;
|
|
301
|
+
else if (text in LOG_LEVEL_RANK) {
|
|
302
|
+
return text as LogLevel;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** Active threshold from `process.env.LOG_LEVEL`, default {@link DEFAULT_LEVEL}. */
|
|
309
|
+
function activeLevel(): LogLevel {
|
|
310
|
+
return parseLogLevel(globalProcess?.env?.LOG_LEVEL) ?? DEFAULT_LEVEL;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* Whether `raw` meets the current `LOG_LEVEL` threshold.
|
|
315
|
+
*
|
|
316
|
+
* Parses `raw` as a {@link LogLevel}; when parsing fails, returns
|
|
317
|
+
* `defaultResult` if supplied, otherwise `false`. Used for consola reporter
|
|
318
|
+
* lines where unknown `logObj.type` values should pass through when
|
|
319
|
+
* `defaultResult` is `true`.
|
|
320
|
+
*/
|
|
321
|
+
function shouldEmit(raw: unknown, defaultResult?: boolean): boolean {
|
|
322
|
+
const level = parseLogLevel(raw);
|
|
323
|
+
if (level === undefined) return defaultResult ?? false;
|
|
324
|
+
return LOG_LEVEL_RANK[level] >= LOG_LEVEL_RANK[activeLevel()];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Whether a call at `level` would be emitted at the current
|
|
329
|
+
* `process.env.LOG_LEVEL` threshold. Use before building expensive debug
|
|
330
|
+
* payloads.
|
|
331
|
+
*
|
|
332
|
+
* @example
|
|
333
|
+
* if (isLevelEnabled("debug")) {
|
|
334
|
+
* log.debug("mounts:resolver", { contribution: await heavySnapshot() });
|
|
335
|
+
* }
|
|
336
|
+
*/
|
|
337
|
+
export function isLevelEnabled(level: LogLevel): boolean {
|
|
338
|
+
return shouldEmit(level);
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const LOGGER_NAME_REGEX = /^(?:[a-z][a-z0-9+.-]*:\/\/)?.*\/([^/.]+)(?:\.[^/]+)?$/i;
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Derive the tag string from a logger name, plugin, or path. Slash- and
|
|
345
|
+
* URL-shaped strings use the last path segment (extension stripped); plain
|
|
346
|
+
* strings pass through unchanged. Empty or missing names -> `undefined`
|
|
347
|
+
* (untagged sink).
|
|
348
|
+
*/
|
|
349
|
+
function extractLoggerName(loggerName: NameLike | string | undefined): string | undefined {
|
|
350
|
+
if (!loggerName) return undefined;
|
|
351
|
+
if (typeof loggerName === "string") {
|
|
352
|
+
const match = loggerName.match(LOGGER_NAME_REGEX);
|
|
353
|
+
return match?.[1] ?? loggerName;
|
|
354
|
+
}
|
|
355
|
+
return extractLoggerName(loggerName.name);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Build a tagged logger for a plugin or module.
|
|
360
|
+
*
|
|
361
|
+
* The tag is `[name]` when `loggerName` is a non-empty string, a
|
|
362
|
+
* {@link NameLike} with `name`, or a file / URL path (the basename without
|
|
363
|
+
* extension is used). Consola applies the tag via `withTag` (render-time).
|
|
364
|
+
* The `console` fallbacks prepend `LEVEL [name]` on Node/Bun hosts or `[name]`
|
|
365
|
+
* alone in browsers via {@link createFormatter}.
|
|
366
|
+
*
|
|
367
|
+
* Calls below `process.env.LOG_LEVEL` are dropped by {@link shouldEmit} before
|
|
368
|
+
* the sink formats or writes the line.
|
|
369
|
+
*
|
|
370
|
+
* @example
|
|
371
|
+
* import { log } from "@dbx-tools/shared-core";
|
|
372
|
+
*
|
|
373
|
+
* const logger = log.logger("genie/chat");
|
|
374
|
+
* logger.info("starting");
|
|
375
|
+
* logger.warn("missing optional config", { reason: "no env var" });
|
|
376
|
+
*/
|
|
377
|
+
export function logger(loggerName: NameLike | string | undefined): Logger {
|
|
378
|
+
const name = extractLoggerName(loggerName);
|
|
379
|
+
return createLogger(name);
|
|
380
|
+
}
|