@monochromatic-dev/module-logger 0.2.0 → 0.4.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +53 -13
  3. package/dist/final/neutral/browser.d.mts +60 -0
  4. package/dist/final/neutral/browser.mjs +1 -0
  5. package/dist/final/neutral/index.d.mts +359 -596
  6. package/dist/final/neutral/index.mjs +2 -3
  7. package/dist/final/neutral/indexed-db-hsIfv7Cv.mjs +2 -0
  8. package/dist/final/neutral/types-BkkBXgY3.d.mts +76 -0
  9. package/dist/final/node/file-CRGb1hDK.mjs +1 -0
  10. package/dist/final/node/index.d.mts +359 -596
  11. package/dist/final/node/index.mjs +3 -3
  12. package/dist/final/node/node.d.mts +103 -0
  13. package/dist/final/node/node.mjs +1 -0
  14. package/dist/final/node/types-BkkBXgY3.d.mts +76 -0
  15. package/package.json +19 -4
  16. package/src/artifact-platform-split.unit.test.ts +140 -0
  17. package/src/browser.ts +14 -0
  18. package/src/create-logger.ts +183 -183
  19. package/src/create-logger.unit.test.ts +112 -112
  20. package/src/default-sinks.neutral.ts +47 -0
  21. package/src/default-sinks.node.ts +43 -0
  22. package/src/error-format.ts +23 -23
  23. package/src/index.ts +1 -4
  24. package/src/logger.ts +76 -51
  25. package/src/node.ts +23 -0
  26. package/src/restricted-global-scope.unit.test.ts +101 -0
  27. package/src/sink/console-control-chars.ts +64 -64
  28. package/src/sink/console-control-chars.unit.test.ts +14 -14
  29. package/src/sink/console.ts +194 -194
  30. package/src/sink/console.unit.test.ts +18 -18
  31. package/src/sink/file.ts +136 -140
  32. package/src/sink/file.unit.test.ts +19 -26
  33. package/src/sink/index.ts +4 -7
  34. package/src/sink/indexed-db-util.ts +42 -42
  35. package/src/sink/indexed-db.browser.test.ts +7 -7
  36. package/src/sink/indexed-db.ts +109 -109
  37. package/src/sink/indexed-db.unit.test.ts +5 -13
  38. package/src/sink/local-storage-key.ts +73 -73
  39. package/src/sink/local-storage-key.unit.test.ts +8 -8
  40. package/src/sink/local-storage-quota.ts +37 -37
  41. package/src/sink/local-storage-quota.unit.test.ts +8 -8
  42. package/src/sink/local-storage-store.ts +113 -113
  43. package/src/sink/local-storage-store.unit.test.ts +35 -35
  44. package/src/sink/local-storage.ts +72 -72
  45. package/src/sink/local-storage.unit.test.ts +27 -27
  46. package/src/sink/noop.ts +20 -20
  47. package/src/sink/noop.unit.test.ts +1 -1
  48. package/src/sink/opfs.browser.test.ts +7 -7
  49. package/src/sink/opfs.ts +62 -62
  50. package/src/sink/opfs.unit.test.ts +5 -13
  51. package/src/sink/record-buffer.ts +84 -84
  52. package/src/sink/record-buffer.unit.test.ts +20 -20
  53. package/src/sink/session-storage-quota.ts +34 -34
  54. package/src/sink/session-storage-quota.unit.test.ts +8 -8
  55. package/src/sink/session-storage-store.ts +72 -72
  56. package/src/sink/session-storage.ts +48 -48
  57. package/src/sink/session-storage.unit.test.ts +39 -39
  58. package/src/sink/web-storage-quota-error.ts +22 -22
  59. package/src/sink/web-storage-quota-error.unit.test.ts +2 -2
  60. package/src/sink/web-storage-runtime.ts +24 -24
  61. package/src/startup.unit.test.ts +18 -18
  62. package/src/tagged.ts +35 -35
  63. package/src/tagged.unit.test.ts +8 -8
  64. package/src/types.ts +39 -39
package/src/logger.ts CHANGED
@@ -1,67 +1,92 @@
1
+ import { createDefaultSinks, } from '#default-sinks';
2
+
1
3
  import { createLogger, } from './create-logger.ts';
2
- import { createConsoleSink, } from './sink/console.ts';
3
- import { createFileSink, } from './sink/file.ts';
4
- import { createIndexedDbSink, } from './sink/indexed-db.ts';
5
- import { createLocalStorageSink, } from './sink/local-storage.ts';
6
- import { createSessionStorageSink, } from './sink/session-storage.ts';
4
+
7
5
  import type {
6
+ Level,
8
7
  Logger,
9
- Sink,
10
8
  } from './types.ts';
11
9
 
12
10
  /**
13
- * Default sink backends to attempt, in priority order. Each runtime keeps
14
- * only the sinks whose `verify` confirms its backend: {@link createConsoleSink}
15
- * everywhere, {@link createIndexedDbSink} in browsers,
16
- * {@link createSessionStorageSink} wherever web storage round-trips (browsers,
17
- * Node 22+, Deno), {@link createLocalStorageSink} wherever `localStorage`
18
- * round-trips (browsers, Deno, Node launched with `--localstorage-file`),
19
- * {@link createFileSink} under Node. The noop sink is intentionally absent:
20
- * the console sink verifies wherever `console` and `queueMicrotask` exist,
21
- * so the default logger has a backend in every supported runtime, and a
22
- * custom `createLogger` whose sinks all fail verification surfaces the
23
- * "No logging backends available" error instead of silently discarding.
24
- * The OPFS sink is exported
25
- * but no longer a default: its stream stages writes until a close that a
26
- * crash never performs, so IndexedDB holds the persistent-browser slot; see
27
- * `DECISIONS.md`.
11
+ Default logger instance and its readiness promise, built on first use.
12
+ */
13
+ type DefaultInstance = {
14
+ readonly initPromise: Promise<void>;
15
+ readonly logger: Logger;
16
+ };
17
+
18
+ /**
19
+ Memo for the default instance. Empty until the first log or flush call, so
20
+ importing this module (or `tagged`, which reaches it) runs no sink
21
+ discovery: no timers, no I/O, no storage probes. Runtimes that forbid those
22
+ in global scope (Cloudflare Workers, issue #493) therefore pay nothing at
23
+ import and verify their sinks inside whatever handler logs first.
24
+ */
25
+ const memo: { current?: DefaultInstance; } = {};
26
+
27
+ /**
28
+ Builds the default instance on first use and returns it afterwards.
29
+
30
+ @returns Default logger and its readiness promise.
31
+
32
+ @example
33
+ ```ts
34
+ const { logger } = defaultInstance();
35
+ ```
28
36
  */
29
- const defaultSinks: readonly Sink[] = [
30
- createConsoleSink(),
31
- createIndexedDbSink(),
32
- createSessionStorageSink(),
33
- createLocalStorageSink(),
34
- createFileSink(),
35
- ];
37
+ function defaultInstance(): DefaultInstance {
38
+ memo.current ??= createLogger({ sinks: createDefaultSinks(), },);
39
+ return memo.current;
40
+ }
36
41
 
37
42
  /**
38
- * Default multi-sink logger plus its eager readiness promise, built by
39
- * applying {@link createLogger} to {@link defaultSinks}.
43
+ Builds one level method that forwards to the default instance, creating it
44
+ on the first call.
45
+
46
+ @param level - Severity the method logs at.
47
+
48
+ @returns Forwarding level method.
40
49
  */
41
- const {
42
- initPromise: defaultInitPromise,
43
- logger: defaultLogger,
44
- } = createLogger({ sinks: defaultSinks, },);
50
+ function forward(level: Level,): (message: string,) => void {
51
+ return function logAtLevel(message: string,): void {
52
+ defaultInstance()
53
+ .logger[level](message,);
54
+ };
55
+ }
45
56
 
46
57
  /**
47
- * Eager readiness promise. Consumers do not need to await this before logging;
48
- * {@link Logger.flush} awaits it internally, and startup records replay to
49
- * async sinks as they become available.
58
+ Awaits the default instance's own `flush`, creating the instance first so a
59
+ flush before any log still verifies the sinks and drains them.
50
60
  */
51
- export const initPromise: Promise<void> = defaultInitPromise;
61
+ async function flush(): Promise<void> {
62
+ await defaultInstance()
63
+ .logger
64
+ .flush();
65
+ }
52
66
 
53
67
  /**
54
- * Multi-sink logger that writes to all available backends.
55
- * Startup records replay to async sinks that verify after the log call.
56
- * Log calls throw only when initialization proves no backend is available,
57
- * which the console sink prevents in every supported runtime.
58
- *
59
- * @example
60
- * ```ts
61
- * import { logger, } from '\@monochromatic-dev/module-logger/logger';
62
- *
63
- * logger.error('unexpected shutdown',);
64
- * await logger.flush();
65
- * ```
68
+ Multi-sink logger that writes to all available backends, built lazily on
69
+ the first call. Startup records replay to async sinks that verify after
70
+ the log call. Log calls throw only when initialization proves no backend is
71
+ available, which the console sink prevents in every supported runtime.
72
+ `flush()` awaits verification internally, so no readiness promise is
73
+ exported: awaiting one at module top level was the mistake this design
74
+ removes.
75
+
76
+ @example
77
+ ```ts
78
+ import { logger, } from '\@monochromatic-dev/module-logger';
79
+
80
+ logger.error('unexpected shutdown',);
81
+ await logger.flush();
82
+ ```
66
83
  */
67
- export const logger: Logger = defaultLogger;
84
+ export const logger: Logger = {
85
+ debug: forward('debug',),
86
+ error: forward('error',),
87
+ fatal: forward('fatal',),
88
+ flush,
89
+ info: forward('info',),
90
+ trace: forward('trace',),
91
+ warn: forward('warn',),
92
+ };
package/src/node.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ Node-only entry (`\@monochromatic-dev/module-logger/node`).
3
+
4
+ Ships the file sink, whose static `node:fs/promises` and `node:path`
5
+ imports must never reach the platform-neutral root entry. Built only by
6
+ `rolldown.node.config.ts`, so the neutral artifact carries no `node:`
7
+ specifier at all.
8
+
9
+ @module
10
+ */
11
+
12
+ export { createFileSink, } from './sink/file.ts';
13
+
14
+ //region Internal seams
15
+ // Underscore-prefixed re-exports let `file.unit.test.ts` exercise the ancestor
16
+ // search through the built artifact (the `require-eventual-artifact` rule)
17
+ // without widening the documented API; they are not part of the public
18
+ // contract.
19
+ export {
20
+ findNodeModulesUp as _findNodeModulesUp,
21
+ NO_NODE_MODULES_FOUND as _NO_NODE_MODULES_FOUND,
22
+ } from './sink/file.ts';
23
+ //endregion Internal seams
@@ -0,0 +1,101 @@
1
+ /**
2
+ Guards issue #493: evaluating the root entry in a runtime that forbids
3
+ timers in global scope (Cloudflare Workers throw from `setTimeout` there)
4
+ must produce no `logger internal error` output, because the default logger
5
+ is built on first use, not at import. Once a handler runs, the first log
6
+ call builds it and the console sink verifies normally.
7
+
8
+ The built artifact is imported dynamically inside the test so the throwing
9
+ `setTimeout` is in place during module evaluation; this file therefore
10
+ imports nothing from the logger statically.
11
+
12
+ @module
13
+ */
14
+
15
+ import {
16
+ describe,
17
+ expect,
18
+ it,
19
+ } from '@monochromatic-dev/module-test/ts';
20
+
21
+ /**
22
+ Message the first log call inside the "handler" sends, distinct enough to
23
+ find in the console stub's calls.
24
+ */
25
+ const HANDLER_MESSAGE = 'first log inside a handler after a restricted import';
26
+
27
+ await describe({
28
+ name: 'default logger under a global-scope-restricted runtime',
29
+ // Both tests stub console methods, so they run one at a time.
30
+ concurrency: 1,
31
+ children: [
32
+ it({
33
+ name: 'importing the root entry while setTimeout throws writes no breadcrumb, and the first log inside a handler works',
34
+ fn: async ({ sinon, },) => {
35
+ const warn = sinon.stub(
36
+ console,
37
+ 'warn',
38
+ );
39
+ const info = sinon.stub(
40
+ console,
41
+ 'info',
42
+ );
43
+ /**
44
+ Timer stub that behaves like a Workers global scope: any timer is a
45
+ disallowed operation.
46
+ */
47
+ const forbidTimers = sinon.stub(
48
+ globalThis,
49
+ 'setTimeout',
50
+ )
51
+ .throws(new Error('Disallowed operation called within global scope',),);
52
+ /**
53
+ Root entry evaluated with timers forbidden, as a Worker isolate does.
54
+ */
55
+ const entry = await import('@monochromatic-dev/module-logger');
56
+ /**
57
+ `tagged` reaches the singleton through its default parameter; wrapping
58
+ must not build it either.
59
+ */
60
+ const l = entry.tagged({ tag: 'restricted', },);
61
+ expect(warn.callCount,)
62
+ .toBe(0,);
63
+ forbidTimers.restore();
64
+
65
+ // Inside a handler, timers are allowed again: the first log builds the
66
+ // default logger, its sinks verify, and the console sink writes.
67
+ l.info(HANDLER_MESSAGE,);
68
+ await entry.logger.flush();
69
+ expect(warn.callCount,)
70
+ .toBe(0,);
71
+ /**
72
+ Console lines that carried the handler message.
73
+ */
74
+ const landed = info.getCalls()
75
+ .filter(function carriesMessage(call,) {
76
+ return call.args
77
+ .some(function mentions(argument,) {
78
+ return String(argument,)
79
+ .includes(HANDLER_MESSAGE,);
80
+ },);
81
+ },);
82
+ expect(landed.length,)
83
+ .toBe(1,);
84
+ },
85
+ },),
86
+
87
+ it({
88
+ name: 'flush before any log still builds the default logger and resolves',
89
+ fn: async ({ sinon, },) => {
90
+ const warn = sinon.stub(
91
+ console,
92
+ 'warn',
93
+ );
94
+ const entry = await import('@monochromatic-dev/module-logger');
95
+ await entry.logger.flush();
96
+ expect(warn.callCount,)
97
+ .toBe(0,);
98
+ },
99
+ },),
100
+ ],
101
+ },);
@@ -1,68 +1,68 @@
1
1
  /**
2
- * Console-bound text crosses a syntax boundary: a terminal interprets C0 and
3
- * C1 control characters as commands (clear screen, set title, move cursor,
4
- * write clipboard). Log messages can carry attacker-influenced text, so the
5
- * console sink neutralizes every control character except newline and tab
6
- * before the text reaches `console.*` or `process.stderr`.
7
- *
8
- * @module
2
+ Console-bound text crosses a syntax boundary: a terminal interprets C0 and
3
+ C1 control characters as commands (clear screen, set title, move cursor,
4
+ write clipboard). Log messages can carry attacker-influenced text, so the
5
+ console sink neutralizes every control character except newline and tab
6
+ before the text reaches `console.*` or `process.stderr`.
7
+
8
+ @module
9
9
  */
10
10
 
11
11
  /**
12
- * First code unit above the C0 control range; everything below it except
13
- * newline and tab is neutralized.
12
+ First code unit above the C0 control range; everything below it except
13
+ newline and tab is neutralized.
14
14
  */
15
15
  const C0_CONTROL_LIMIT = 0x20;
16
16
 
17
17
  /**
18
- * Newline stays literal: multi-line messages (stack traces) are a core use.
18
+ Newline stays literal: multi-line messages (stack traces) are a core use.
19
19
  */
20
20
  const NEWLINE_CODE_UNIT = 0x0A;
21
21
 
22
22
  /**
23
- * Tab stays literal: indentation in multi-line messages is harmless.
23
+ Tab stays literal: indentation in multi-line messages is harmless.
24
24
  */
25
25
  const TAB_CODE_UNIT = 0x09;
26
26
 
27
27
  /**
28
- * DEL sits alone above the printable ASCII range and is a control character.
28
+ DEL sits alone above the printable ASCII range and is a control character.
29
29
  */
30
30
  const DELETE_CODE_UNIT = 0x7F;
31
31
 
32
32
  /**
33
- * First code unit of the C1 control range (8-bit CSI, OSC, and friends).
33
+ First code unit of the C1 control range (8-bit CSI, OSC, and friends).
34
34
  */
35
35
  const C1_CONTROL_START = 0x80;
36
36
 
37
37
  /**
38
- * Last code unit of the C1 control range.
38
+ Last code unit of the C1 control range.
39
39
  */
40
40
  const C1_CONTROL_END = 0x9F;
41
41
 
42
42
  /**
43
- * Radix for the hexadecimal digits inside a `\uXXXX` escape.
43
+ Radix for the hexadecimal digits inside a `\uXXXX` escape.
44
44
  */
45
45
  const HEX_RADIX = 16;
46
46
 
47
47
  /**
48
- * Digit count of a `\uXXXX` escape, zero-padded on the left.
48
+ Digit count of a `\uXXXX` escape, zero-padded on the left.
49
49
  */
50
50
  const UNICODE_ESCAPE_WIDTH = 4;
51
51
 
52
52
  /**
53
- * Reports whether one UTF-16 code unit is a control character the console
54
- * sink must neutralize.
55
- *
56
- * @param codeUnit - UTF-16 code unit read from the message.
57
- *
58
- * @returns Whether the code unit is a C0 control other than newline and tab,
59
- * DEL, or a C1 control.
60
- *
61
- * @example
62
- * ```ts
63
- * isNeutralizedControl(0x1B); // true (ESC)
64
- * isNeutralizedControl(0x0A); // false (newline stays)
65
- * ```
53
+ Reports whether one UTF-16 code unit is a control character the console
54
+ sink must neutralize.
55
+
56
+ @param codeUnit - UTF-16 code unit read from the message.
57
+
58
+ @returns Whether the code unit is a C0 control other than newline and tab,
59
+ DEL, or a C1 control.
60
+
61
+ @example
62
+ ```ts
63
+ isNeutralizedControl(0x1B); // true (ESC)
64
+ isNeutralizedControl(0x0A); // false (newline stays)
65
+ ```
66
66
  */
67
67
  function isNeutralizedControl(codeUnit: number,): boolean {
68
68
  if (codeUnit < C0_CONTROL_LIMIT)
@@ -75,18 +75,18 @@ function isNeutralizedControl(codeUnit: number,): boolean {
75
75
  }
76
76
 
77
77
  /**
78
- * Renders one code unit as a `\uXXXX` escape with uppercase hex digits (the
79
- * repository's escape-case convention) so the attempted control stays
80
- * visible for forensics instead of vanishing.
81
- *
82
- * @param codeUnit - UTF-16 code unit to escape.
83
- *
84
- * @returns Six-character escape such as `\u001B`.
85
- *
86
- * @example
87
- * ```ts
88
- * escapeCodeUnit(0x1B); // '\\u001B'
89
- * ```
78
+ Renders one code unit as a `\uXXXX` escape with uppercase hex digits (the
79
+ repository's escape-case convention) so the attempted control stays
80
+ visible for forensics instead of vanishing.
81
+
82
+ @param codeUnit - UTF-16 code unit to escape.
83
+
84
+ @returns Six-character escape such as `\u001B`.
85
+
86
+ @example
87
+ ```ts
88
+ escapeCodeUnit(0x1B); // '\\u001B'
89
+ ```
90
90
  */
91
91
  function escapeCodeUnit(codeUnit: number,): string {
92
92
  return `\\u${
@@ -101,36 +101,36 @@ function escapeCodeUnit(codeUnit: number,): string {
101
101
  }
102
102
 
103
103
  /**
104
- * Neutralizes terminal control characters in console-bound text. One linear
105
- * pass over the code points: each neutralized control becomes a `\uXXXX`
106
- * escape, everything else is copied through, and newline and tab pass
107
- * untouched. Well-formed and malformed escape sequences get no
108
- * special treatment because the introducer byte itself is neutralized, so a
109
- * trailing lone ESC, an unterminated OSC, and a nested ESC all lose their
110
- * teeth the same way.
111
- *
112
- * @param text - Message text destined for `console.*` or `process.stderr`.
113
- *
114
- * @returns Text with every neutralized control rendered as `\uXXXX`.
115
- *
116
- * @example
117
- * ```ts
118
- * neutralizeControlCharacters('title:\u001B]0;x\u0007 ok\n\tnext');
119
- * // => 'title:\\u001B]0;x\\u0007 ok\n\tnext'
120
- * ```
104
+ Neutralizes terminal control characters in console-bound text. One linear
105
+ pass over the code points: each neutralized control becomes a `\uXXXX`
106
+ escape, everything else is copied through, and newline and tab pass
107
+ untouched. Well-formed and malformed escape sequences get no
108
+ special treatment because the introducer byte itself is neutralized, so a
109
+ trailing lone ESC, an unterminated OSC, and a nested ESC all lose their
110
+ teeth the same way.
111
+
112
+ @param text - Message text destined for `console.*` or `process.stderr`.
113
+
114
+ @returns Text with every neutralized control rendered as `\uXXXX`.
115
+
116
+ @example
117
+ ```ts
118
+ neutralizeControlCharacters('title:\u001B]0;x\u0007 ok\n\tnext');
119
+ // => 'title:\\u001B]0;x\\u0007 ok\n\tnext'
120
+ ```
121
121
  */
122
122
  export function neutralizeControlCharacters(text: string,): string {
123
123
  /**
124
- * Output pieces in input order: each code point either verbatim or as its
125
- * escape, joined once at the end so no per-character string rebuild occurs.
124
+ Output pieces in input order: each code point either verbatim or as its
125
+ escape, joined once at the end so no per-character string rebuild occurs.
126
126
  */
127
127
  const pieces: string[] = [];
128
128
  for (const character of text) {
129
129
  /**
130
- * Leading code unit of this iteration element. String iteration walks
131
- * code points, so a surrogate pair arrives as one two-unit string whose
132
- * lead surrogate is never a control, and a lone surrogate passes the
133
- * same way.
130
+ Leading code unit of this iteration element. String iteration walks
131
+ code points, so a surrogate pair arrives as one two-unit string whose
132
+ lead surrogate is never a control, and a lone surrogate passes the
133
+ same way.
134
134
  */
135
135
  // oxlint-disable-next-line unicorn/prefer-code-point -- Classifier reads the lead code unit on purpose; controls below U+00A0 never sit inside a surrogate pair, so code-point decoding adds nothing.
136
136
  const codeUnit = character.charCodeAt(0,);
@@ -8,9 +8,9 @@ import {
8
8
  } from '@monochromatic-dev/module-logger';
9
9
 
10
10
  /**
11
- * Adversarial inputs at the terminal boundary paired with the exact output
12
- * the neutralizer must produce. Each case names the attack or the malformed
13
- * shape it pins.
11
+ Adversarial inputs at the terminal boundary paired with the exact output
12
+ the neutralizer must produce. Each case names the attack or the malformed
13
+ shape it pins.
14
14
  */
15
15
  const BOUNDARY_CASES: readonly {
16
16
  readonly name: string;
@@ -80,7 +80,7 @@ const BOUNDARY_CASES: readonly {
80
80
  ];
81
81
 
82
82
  /**
83
- * Inputs the neutralizer must return unchanged.
83
+ Inputs the neutralizer must return unchanged.
84
84
  */
85
85
  const PASSTHROUGH_CASES: readonly {
86
86
  readonly name: string;
@@ -117,17 +117,17 @@ const PASSTHROUGH_CASES: readonly {
117
117
  ];
118
118
 
119
119
  /**
120
- * One past the last C1 code unit; the exhaustive sweep covers every code unit
121
- * below it.
120
+ One past the last C1 code unit; the exhaustive sweep covers every code unit
121
+ below it.
122
122
  */
123
123
  const CONTROL_SWEEP_LENGTH = 0xA0;
124
124
 
125
125
  /**
126
- * Reports whether an output code unit is still a control the boundary forbids.
127
- *
128
- * @param codeUnit - UTF-16 code unit read from neutralizer output.
129
- *
130
- * @returns Whether the code unit should have been neutralized.
126
+ Reports whether an output code unit is still a control the boundary forbids.
127
+
128
+ @param codeUnit - UTF-16 code unit read from neutralizer output.
129
+
130
+ @returns Whether the code unit should have been neutralized.
131
131
  */
132
132
  function isForbiddenControl(codeUnit: number,): boolean {
133
133
  if (codeUnit < 0x20)
@@ -156,7 +156,7 @@ await describe({
156
156
  name: 'output never contains a neutralized control after the pass',
157
157
  fn: async () => {
158
158
  /**
159
- * Every code unit from NUL through U+009F, in one string.
159
+ Every code unit from NUL through U+009F, in one string.
160
160
  */
161
161
  const allControls = Array.from(
162
162
  { length: CONTROL_SWEEP_LENGTH, },
@@ -169,11 +169,11 @@ await describe({
169
169
  )
170
170
  .join('',);
171
171
  /**
172
- * Neutralized sweep; only newline and tab may survive as controls.
172
+ Neutralized sweep; only newline and tab may survive as controls.
173
173
  */
174
174
  const output = neutralizeControlCharacters(allControls,);
175
175
  /**
176
- * Output code units that are still forbidden controls; must stay empty.
176
+ Output code units that are still forbidden controls; must stay empty.
177
177
  */
178
178
  const leaked: number[] = [];
179
179
  for (const character of output) {