@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.
Files changed (52) hide show
  1. package/README.md +173 -0
  2. package/dist/class.d.ts +17 -11
  3. package/dist/class.d.ts.map +1 -1
  4. package/dist/class.js +27 -1
  5. package/dist/class.js.map +1 -1
  6. package/dist/collection.d.ts +17 -45
  7. package/dist/collection.d.ts.map +1 -1
  8. package/dist/collection.js +42 -76
  9. package/dist/collection.js.map +1 -1
  10. package/dist/error.d.ts +16 -8
  11. package/dist/error.d.ts.map +1 -1
  12. package/dist/error.js +35 -8
  13. package/dist/error.js.map +1 -1
  14. package/dist/graceful-shutdown.d.ts +9 -57
  15. package/dist/graceful-shutdown.d.ts.map +1 -1
  16. package/dist/graceful-shutdown.js +70 -75
  17. package/dist/graceful-shutdown.js.map +1 -1
  18. package/dist/index.d.ts +9 -6
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +9 -5
  21. package/dist/index.js.map +1 -1
  22. package/dist/log.d.ts +103 -0
  23. package/dist/log.d.ts.map +1 -0
  24. package/dist/log.js +129 -0
  25. package/dist/log.js.map +1 -0
  26. package/dist/package-version.d.ts +7 -0
  27. package/dist/package-version.d.ts.map +1 -0
  28. package/dist/package-version.js +8 -0
  29. package/dist/package-version.js.map +1 -0
  30. package/dist/reflect.d.ts +11 -0
  31. package/dist/reflect.d.ts.map +1 -0
  32. package/dist/reflect.js +14 -0
  33. package/dist/reflect.js.map +1 -0
  34. package/dist/source.d.ts +12 -25
  35. package/dist/source.d.ts.map +1 -1
  36. package/dist/source.js +20 -41
  37. package/dist/source.js.map +1 -1
  38. package/dist/string.d.ts +13 -40
  39. package/dist/string.d.ts.map +1 -1
  40. package/dist/string.js +18 -48
  41. package/dist/string.js.map +1 -1
  42. package/package.json +20 -7
  43. package/src/class.ts +30 -11
  44. package/src/collection.ts +43 -81
  45. package/src/error.ts +38 -8
  46. package/src/graceful-shutdown.ts +87 -114
  47. package/src/index.ts +18 -9
  48. package/src/log.ts +164 -0
  49. package/src/package-version.ts +9 -0
  50. package/src/reflect.ts +13 -0
  51. package/src/source.ts +24 -43
  52. 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
- * Coerces an unknown thrown value into a human-readable string.
7
+ * Extracts a human-readable message from an unknown thrown value, falling back to its string form.
3
8
  *
4
- * Returns `error.message` when `error` is an `Error` instance, otherwise
5
- * delegates to `String(error)`. Use at boundaries where exceptions are
6
- * surfaced to logs, IPC frames, or user-facing output and the type cannot
7
- * be narrowed otherwise.
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 - The caught value of unknown shape.
10
- * @returns A string describing the error.
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 errorMessage = (error: unknown): string => (error instanceof Error ? error.message : String(error));
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
+ };
@@ -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 readonly NodeJS.Signals[];
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 the POSIX signal that ended a process into the exit code shells use to
15
- * report it. `SIGINT` yields `130` (Ctrl-C), every other tracked signal yields
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 - The signal name, or `null` for a clean exit.
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
- * Overrides the exit code computed from the triggering signal. Use this
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
- * Handle returned by {@link installGracefulShutdown}, used to detach the
57
- * helper's signal handlers (for example in tests).
58
- */
59
- export type GracefulShutdownHandle = {
60
- /**
61
- * Removes the installed signal handlers and cancels any pending
62
- * escalation timer. Idempotent.
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
- * Installs the graceful-shutdown primitive on the current process.
69
- *
70
- * On the first matching signal: invokes `onSignal`, optionally schedules
71
- * `onForce` after `forceKillAfterMs`, awaits the close, then exits. A second
72
- * `SIGINT` (the canonical "force-kill" gesture) invokes `onForce`
73
- * immediately. The exit code defaults to {@link exitCodeForSignal} but can
74
- * be overridden by `exitCode`.
75
- *
76
- * @param options - Shutdown behaviour.
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
- let firstSignal: NodeJS.Signals | null = null;
83
- let exited = false;
84
- let forceTimer: NodeJS.Timeout | null = null;
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
- const clearTimer = (): void => {
87
- if (forceTimer) {
88
- clearTimeout(forceTimer);
89
- forceTimer = null;
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
- const finish = (signal: NodeJS.Signals): void => {
94
- if (exited) return;
95
- exited = true;
96
- clearTimer();
97
- const code = options.exitCode ? options.exitCode(signal) : exitCodeForSignal(signal);
98
- process.exit(code);
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
- const handle = (signal: NodeJS.Signals): void => {
102
- if (firstSignal === null) {
103
- firstSignal = signal;
104
- if (options.onForce && forceKillMs > 0) {
105
- forceTimer = setTimeout(() => {
106
- options.onForce?.();
107
- finish(signal);
108
- }, forceKillMs);
109
- forceTimer.unref?.();
110
- }
111
- Promise.resolve()
112
- .then(() => options.onSignal(signal))
113
- .catch((error: unknown) => {
114
- console.error("Graceful shutdown error:", error);
115
- })
116
- .finally(() => finish(signal));
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
- const listener = (): void => handle(sig);
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 { AnyClass } from "./class.js";
2
- export { isShallowArrayEqual, isShallowEqual, omit, reverseNumericEnum } from "./collection.js";
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
- exitCodeForSignal,
6
- type GracefulShutdownHandle,
7
- installGracefulShutdown,
8
- } from "./graceful-shutdown.js";
9
- export { quote, toIdentifier } from "./source.js";
10
- export { toCamelCase, toKebabCase, toPascalCase, toUpperFirst } from "./string.js";
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
- /** Reserved words and global identifiers a generated identifier must not collide with. */
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
- * Rewrites a candidate name into a JavaScript identifier safe to use at
60
- * variable, parameter, or property position.
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
- * The input is expected to already use valid identifier characters (for
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 toIdentifier = (name: string): string => (RESERVED.has(name) ? `${name}_` : name);
59
+ export const sanitizeIdentifier = (name: string): string => (RESERVED.has(name) ? `${name}_` : name);
72
60
 
73
- const UNSAFE_SOURCE_CHARS = /[<>\u2028\u2029]/g;
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 escapeSourceChar = (char: string): string => {
76
- switch (char) {
77
- case "<":
78
- return "\\u003C";
79
- case ">":
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
- * Quotes a string for safe embedding as a literal in generated TypeScript
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 - The string to embed.
101
- * @returns A source-safe double-quoted string literal.
81
+ * @param value The string to encode.
102
82
  */
103
- export const quote = (value: string): string => JSON.stringify(value).replace(UNSAFE_SOURCE_CHARS, escapeSourceChar);
83
+ export const sourceStringLiteral = (value: string): string =>
84
+ JSON.stringify(value).replace(UNSAFE_SOURCE_CHARS, (char) => SOURCE_ESCAPES[char] ?? char);