@linxiraos/pi-utils 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +449 -0
- package/README.md +36 -0
- package/dist/types/abortable.d.ts +32 -0
- package/dist/types/acp/connection.d.ts +118 -0
- package/dist/types/acp/protocol.d.ts +526 -0
- package/dist/types/acp/schema.d.ts +41 -0
- package/dist/types/acp/stream.d.ts +8 -0
- package/dist/types/acp/transport.d.ts +81 -0
- package/dist/types/acp.d.ts +6 -0
- package/dist/types/async.d.ts +20 -0
- package/dist/types/binary.d.ts +17 -0
- package/dist/types/browsers.d.ts +68 -0
- package/dist/types/chalk.d.ts +125 -0
- package/dist/types/cli.d.ts +134 -0
- package/dist/types/color.d.ts +102 -0
- package/dist/types/dates.d.ts +7 -0
- package/dist/types/dirs.d.ts +282 -0
- package/dist/types/docx/converter.d.ts +46 -0
- package/dist/types/docx/xml.d.ts +26 -0
- package/dist/types/docx/zip.d.ts +6 -0
- package/dist/types/docx.d.ts +11 -0
- package/dist/types/dom/core.d.ts +431 -0
- package/dist/types/dom/parser.d.ts +7 -0
- package/dist/types/dom/selector.d.ts +5 -0
- package/dist/types/dom.d.ts +5 -0
- package/dist/types/env.d.ts +124 -0
- package/dist/types/fetch-retry.d.ts +96 -0
- package/dist/types/file-lock.d.ts +23 -0
- package/dist/types/format.d.ts +37 -0
- package/dist/types/frontmatter.d.ts +46 -0
- package/dist/types/fs-error.d.ts +31 -0
- package/dist/types/glob.d.ts +28 -0
- package/dist/types/headers.d.ts +34 -0
- package/dist/types/index.d.ts +39 -0
- package/dist/types/json-parse.d.ts +74 -0
- package/dist/types/json.d.ts +14 -0
- package/dist/types/logger/rotating-file.d.ts +18 -0
- package/dist/types/logger.d.ts +96 -0
- package/dist/types/loop-phase.d.ts +10 -0
- package/dist/types/lru.d.ts +46 -0
- package/dist/types/marked/core.d.ts +445 -0
- package/dist/types/marked.d.ts +2 -0
- package/dist/types/mermaid-ascii.d.ts +11 -0
- package/dist/types/mime.d.ts +29 -0
- package/dist/types/module-timer.d.ts +1 -0
- package/dist/types/path-tree.d.ts +76 -0
- package/dist/types/path.d.ts +2 -0
- package/dist/types/peek-file.d.ts +29 -0
- package/dist/types/postmortem.d.ts +104 -0
- package/dist/types/process-name.d.ts +7 -0
- package/dist/types/procmgr.d.ts +74 -0
- package/dist/types/prompt.d.ts +18 -0
- package/dist/types/ptree.d.ts +110 -0
- package/dist/types/readability/readability.d.ts +9 -0
- package/dist/types/readability/readerable.d.ts +10 -0
- package/dist/types/readability/types.d.ts +70 -0
- package/dist/types/readability.d.ts +4 -0
- package/dist/types/ring.d.ts +93 -0
- package/dist/types/runtime-install.d.ts +78 -0
- package/dist/types/sanitize-text.d.ts +29 -0
- package/dist/types/snowflake.d.ts +25 -0
- package/dist/types/stderr-guard.d.ts +22 -0
- package/dist/types/stream.d.ts +67 -0
- package/dist/types/tab-spacing.d.ts +24 -0
- package/dist/types/temp.d.ts +17 -0
- package/dist/types/template.d.ts +62 -0
- package/dist/types/timing-buffer.d.ts +22 -0
- package/dist/types/tls-fetch.d.ts +37 -0
- package/dist/types/turndown/gfm.d.ts +11 -0
- package/dist/types/turndown/html.d.ts +5 -0
- package/dist/types/turndown/service.d.ts +21 -0
- package/dist/types/turndown/types.d.ts +70 -0
- package/dist/types/turndown.d.ts +4 -0
- package/dist/types/type-guards.d.ts +5 -0
- package/dist/types/vendor/mermaid-ascii/ascii/ansi.d.ts +41 -0
- package/dist/types/vendor/mermaid-ascii/ascii/canvas.d.ts +89 -0
- package/dist/types/vendor/mermaid-ascii/ascii/class-diagram.d.ts +7 -0
- package/dist/types/vendor/mermaid-ascii/ascii/converter.d.ts +12 -0
- package/dist/types/vendor/mermaid-ascii/ascii/draw.d.ts +66 -0
- package/dist/types/vendor/mermaid-ascii/ascii/edge-bundling.d.ts +48 -0
- package/dist/types/vendor/mermaid-ascii/ascii/edge-routing.d.ts +43 -0
- package/dist/types/vendor/mermaid-ascii/ascii/er-diagram.d.ts +7 -0
- package/dist/types/vendor/mermaid-ascii/ascii/grid.d.ts +56 -0
- package/dist/types/vendor/mermaid-ascii/ascii/index.d.ts +65 -0
- package/dist/types/vendor/mermaid-ascii/ascii/multiline-utils.d.ts +27 -0
- package/dist/types/vendor/mermaid-ascii/ascii/pathfinder.d.ts +17 -0
- package/dist/types/vendor/mermaid-ascii/ascii/sequence.d.ts +7 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/circle.d.ts +11 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/corners.d.ts +34 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/diamond.d.ts +11 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/hexagon.d.ts +11 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/index.d.ts +26 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/rectangle.d.ts +31 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/rounded.d.ts +11 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/special.d.ts +59 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/stadium.d.ts +17 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/state.d.ts +30 -0
- package/dist/types/vendor/mermaid-ascii/ascii/shapes/types.d.ts +55 -0
- package/dist/types/vendor/mermaid-ascii/ascii/types.d.ts +206 -0
- package/dist/types/vendor/mermaid-ascii/ascii/validate.d.ts +51 -0
- package/dist/types/vendor/mermaid-ascii/ascii/xychart.d.ts +2 -0
- package/dist/types/vendor/mermaid-ascii/class/parser.d.ts +6 -0
- package/dist/types/vendor/mermaid-ascii/class/types.d.ts +102 -0
- package/dist/types/vendor/mermaid-ascii/er/parser.d.ts +6 -0
- package/dist/types/vendor/mermaid-ascii/er/types.d.ts +76 -0
- package/dist/types/vendor/mermaid-ascii/index.d.ts +1 -0
- package/dist/types/vendor/mermaid-ascii/multiline-utils.d.ts +9 -0
- package/dist/types/vendor/mermaid-ascii/parser.d.ts +7 -0
- package/dist/types/vendor/mermaid-ascii/sequence/parser.d.ts +6 -0
- package/dist/types/vendor/mermaid-ascii/sequence/types.d.ts +130 -0
- package/dist/types/vendor/mermaid-ascii/text-metrics.d.ts +21 -0
- package/dist/types/vendor/mermaid-ascii/types.d.ts +114 -0
- package/dist/types/vendor/mermaid-ascii/xychart/colors.d.ts +25 -0
- package/dist/types/vendor/mermaid-ascii/xychart/parser.d.ts +6 -0
- package/dist/types/vendor/mermaid-ascii/xychart/types.d.ts +145 -0
- package/dist/types/version.d.ts +18 -0
- package/dist/types/vterm/buffer.d.ts +99 -0
- package/dist/types/vterm/terminal.d.ts +44 -0
- package/dist/types/vterm.d.ts +8 -0
- package/dist/types/which.d.ts +37 -0
- package/dist/types/worker-host.d.ts +51 -0
- package/dist/types/xml.d.ts +31 -0
- package/package.json +59 -0
- package/src/abortable.ts +109 -0
- package/src/acp/connection.ts +344 -0
- package/src/acp/protocol.ts +466 -0
- package/src/acp/schema.ts +160 -0
- package/src/acp/stream.ts +82 -0
- package/src/acp/transport.ts +213 -0
- package/src/acp.ts +6 -0
- package/src/async.ts +91 -0
- package/src/binary.ts +50 -0
- package/src/browsers.ts +501 -0
- package/src/chalk.ts +312 -0
- package/src/cli.ts +529 -0
- package/src/color.ts +302 -0
- package/src/dates.ts +194 -0
- package/src/dirs.ts +1026 -0
- package/src/docx/converter.ts +681 -0
- package/src/docx/xml.ts +166 -0
- package/src/docx/zip.ts +87 -0
- package/src/docx.ts +20 -0
- package/src/dom/core.ts +1254 -0
- package/src/dom/parser.ts +370 -0
- package/src/dom/selector.ts +290 -0
- package/src/dom.ts +33 -0
- package/src/env.ts +400 -0
- package/src/fetch-retry.ts +379 -0
- package/src/file-lock.ts +69 -0
- package/src/format.ts +113 -0
- package/src/frontmatter.ts +205 -0
- package/src/fs-error.ts +56 -0
- package/src/glob.ts +189 -0
- package/src/headers.ts +167 -0
- package/src/index.ts +59 -0
- package/src/json-parse.ts +821 -0
- package/src/json.ts +23 -0
- package/src/logger/rotating-file.ts +149 -0
- package/src/logger.ts +798 -0
- package/src/loop-phase.ts +49 -0
- package/src/lru.ts +185 -0
- package/src/marked/core.ts +1576 -0
- package/src/marked.ts +2 -0
- package/src/mermaid-ascii.ts +31 -0
- package/src/mime.ts +159 -0
- package/src/module-timer.ts +148 -0
- package/src/path-tree.ts +147 -0
- package/src/path.ts +28 -0
- package/src/peek-file.ts +188 -0
- package/src/postmortem.ts +433 -0
- package/src/process-name.ts +57 -0
- package/src/procmgr.ts +247 -0
- package/src/prompt.ts +545 -0
- package/src/ptree.ts +412 -0
- package/src/readability/readability.ts +533 -0
- package/src/readability/readerable.ts +51 -0
- package/src/readability/types.ts +72 -0
- package/src/readability.ts +11 -0
- package/src/ring.ts +169 -0
- package/src/runtime-install.ts +393 -0
- package/src/sanitize-text.ts +96 -0
- package/src/snowflake.ts +121 -0
- package/src/stderr-guard.ts +155 -0
- package/src/stream.ts +454 -0
- package/src/tab-spacing.ts +336 -0
- package/src/temp.ts +134 -0
- package/src/template.ts +586 -0
- package/src/timing-buffer.ts +47 -0
- package/src/tls-fetch.ts +178 -0
- package/src/turndown/gfm.ts +106 -0
- package/src/turndown/html.ts +257 -0
- package/src/turndown/service.ts +334 -0
- package/src/turndown/types.ts +81 -0
- package/src/turndown.ts +5 -0
- package/src/type-guards.ts +17 -0
- package/src/vendor/mermaid-ascii/NOTICE +33 -0
- package/src/vendor/mermaid-ascii/ascii/ansi.ts +409 -0
- package/src/vendor/mermaid-ascii/ascii/canvas.ts +476 -0
- package/src/vendor/mermaid-ascii/ascii/class-diagram.ts +699 -0
- package/src/vendor/mermaid-ascii/ascii/converter.ts +271 -0
- package/src/vendor/mermaid-ascii/ascii/draw.ts +1382 -0
- package/src/vendor/mermaid-ascii/ascii/edge-bundling.ts +328 -0
- package/src/vendor/mermaid-ascii/ascii/edge-routing.ts +297 -0
- package/src/vendor/mermaid-ascii/ascii/er-diagram.ts +441 -0
- package/src/vendor/mermaid-ascii/ascii/grid.ts +578 -0
- package/src/vendor/mermaid-ascii/ascii/index.ts +187 -0
- package/src/vendor/mermaid-ascii/ascii/multiline-utils.ts +78 -0
- package/src/vendor/mermaid-ascii/ascii/pathfinder.ts +277 -0
- package/src/vendor/mermaid-ascii/ascii/sequence.ts +460 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/circle.ts +27 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/corners.ts +127 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/diamond.ts +27 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/hexagon.ts +27 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/index.ts +101 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/rectangle.ts +175 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/rounded.ts +27 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/special.ts +296 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/stadium.ts +114 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/state.ts +192 -0
- package/src/vendor/mermaid-ascii/ascii/shapes/types.ts +73 -0
- package/src/vendor/mermaid-ascii/ascii/types.ts +273 -0
- package/src/vendor/mermaid-ascii/ascii/validate.ts +120 -0
- package/src/vendor/mermaid-ascii/ascii/xychart.ts +875 -0
- package/src/vendor/mermaid-ascii/class/parser.ts +290 -0
- package/src/vendor/mermaid-ascii/class/types.ts +121 -0
- package/src/vendor/mermaid-ascii/er/parser.ts +181 -0
- package/src/vendor/mermaid-ascii/er/types.ts +91 -0
- package/src/vendor/mermaid-ascii/index.ts +14 -0
- package/src/vendor/mermaid-ascii/multiline-utils.ts +30 -0
- package/src/vendor/mermaid-ascii/parser.ts +645 -0
- package/src/vendor/mermaid-ascii/sequence/parser.ts +207 -0
- package/src/vendor/mermaid-ascii/sequence/types.ts +146 -0
- package/src/vendor/mermaid-ascii/text-metrics.ts +71 -0
- package/src/vendor/mermaid-ascii/types.ts +164 -0
- package/src/vendor/mermaid-ascii/xychart/colors.ts +140 -0
- package/src/vendor/mermaid-ascii/xychart/parser.ts +115 -0
- package/src/vendor/mermaid-ascii/xychart/types.ts +150 -0
- package/src/version.ts +99 -0
- package/src/vterm/buffer.ts +218 -0
- package/src/vterm/terminal.ts +773 -0
- package/src/vterm.ts +8 -0
- package/src/which.ts +234 -0
- package/src/worker-host.ts +100 -0
- package/src/xml.ts +313 -0
package/src/ring.ts
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A fixed-capacity circular buffer that supports efficient push/pop/shift/unshift operations.
|
|
3
|
+
* When the buffer is full, adding new items overwrites the oldest items (FIFO behavior).
|
|
4
|
+
*
|
|
5
|
+
* @template T The type of elements stored in the buffer.
|
|
6
|
+
*/
|
|
7
|
+
export class RingBuffer<T> {
|
|
8
|
+
#buf: (T | undefined)[];
|
|
9
|
+
#head = 0;
|
|
10
|
+
#size = 0;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Creates a new ring buffer with the specified capacity.
|
|
14
|
+
*
|
|
15
|
+
* @param capacity - The maximum number of elements the buffer can hold. Must be positive.
|
|
16
|
+
*/
|
|
17
|
+
constructor(public readonly capacity: number) {
|
|
18
|
+
this.#buf = new Array(capacity);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The number of elements currently in the buffer.
|
|
23
|
+
*/
|
|
24
|
+
get length(): number {
|
|
25
|
+
return this.#size;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Whether the buffer is at full capacity.
|
|
30
|
+
*/
|
|
31
|
+
get isFull(): boolean {
|
|
32
|
+
return this.#size === this.capacity;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Whether the buffer is empty (contains no elements).
|
|
37
|
+
*/
|
|
38
|
+
get isEmpty(): boolean {
|
|
39
|
+
return this.#size === 0;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Adds an item to the end of the buffer.
|
|
44
|
+
* If the buffer is full, the oldest item is overwritten and returned.
|
|
45
|
+
*
|
|
46
|
+
* @param item - The item to add.
|
|
47
|
+
* @returns The overwritten item if the buffer was full, otherwise `undefined`.
|
|
48
|
+
*/
|
|
49
|
+
push(item: T): T | undefined {
|
|
50
|
+
const idx = (this.#head + this.#size) % this.capacity;
|
|
51
|
+
const overwritten = this.#size === this.capacity ? this.#buf[idx] : undefined;
|
|
52
|
+
this.#buf[idx] = item;
|
|
53
|
+
if (this.#size === this.capacity) {
|
|
54
|
+
this.#head = (this.#head + 1) % this.capacity;
|
|
55
|
+
} else {
|
|
56
|
+
this.#size++;
|
|
57
|
+
}
|
|
58
|
+
return overwritten;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Removes and returns the first (oldest) item from the buffer.
|
|
63
|
+
*
|
|
64
|
+
* @returns The removed item, or `undefined` if the buffer is empty.
|
|
65
|
+
*/
|
|
66
|
+
shift(): T | undefined {
|
|
67
|
+
if (this.#size === 0) return undefined;
|
|
68
|
+
const item = this.#buf[this.#head];
|
|
69
|
+
this.#buf[this.#head] = undefined;
|
|
70
|
+
this.#head = (this.#head + 1) % this.capacity;
|
|
71
|
+
this.#size--;
|
|
72
|
+
return item;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Removes and returns the last (newest) item from the buffer.
|
|
77
|
+
*
|
|
78
|
+
* @returns The removed item, or `undefined` if the buffer is empty.
|
|
79
|
+
*/
|
|
80
|
+
pop(): T | undefined {
|
|
81
|
+
if (this.#size === 0) return undefined;
|
|
82
|
+
const idx = (this.#head + this.#size - 1) % this.capacity;
|
|
83
|
+
const item = this.#buf[idx];
|
|
84
|
+
this.#buf[idx] = undefined;
|
|
85
|
+
this.#size--;
|
|
86
|
+
return item;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Adds an item to the beginning of the buffer.
|
|
91
|
+
* If the buffer is full, the newest item is overwritten and returned.
|
|
92
|
+
*
|
|
93
|
+
* @param item - The item to add.
|
|
94
|
+
* @returns The overwritten item if the buffer was full, otherwise `undefined`.
|
|
95
|
+
*/
|
|
96
|
+
unshift(item: T): T | undefined {
|
|
97
|
+
this.#head = (this.#head - 1 + this.capacity) % this.capacity;
|
|
98
|
+
const overwritten = this.#size === this.capacity ? this.#buf[this.#head] : undefined;
|
|
99
|
+
this.#buf[this.#head] = item;
|
|
100
|
+
if (this.#size < this.capacity) this.#size++;
|
|
101
|
+
return overwritten;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Returns the element at the specified index without removing it.
|
|
106
|
+
* Supports negative indices (e.g., `-1` for the last element).
|
|
107
|
+
*
|
|
108
|
+
* @param index - The zero-based index, or negative index from the end.
|
|
109
|
+
* @returns The element at the index, or `undefined` if the index is out of bounds.
|
|
110
|
+
*/
|
|
111
|
+
at(index: number): T | undefined {
|
|
112
|
+
if (index < 0) index += this.#size;
|
|
113
|
+
if (index < 0 || index >= this.#size) return undefined;
|
|
114
|
+
return this.#buf[(this.#head + index) % this.capacity];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Returns the first (oldest) element without removing it.
|
|
119
|
+
*
|
|
120
|
+
* @returns The first element, or `undefined` if the buffer is empty.
|
|
121
|
+
*/
|
|
122
|
+
peek(): T | undefined {
|
|
123
|
+
return this.at(0);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Returns the last (newest) element without removing it.
|
|
128
|
+
*
|
|
129
|
+
* @returns The last element, or `undefined` if the buffer is empty.
|
|
130
|
+
*/
|
|
131
|
+
peekBack(): T | undefined {
|
|
132
|
+
return this.at(this.#size - 1);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Removes all elements from the buffer, resetting it to an empty state.
|
|
137
|
+
*/
|
|
138
|
+
clear(): void {
|
|
139
|
+
this.#buf.fill(undefined, 0, this.capacity);
|
|
140
|
+
this.#head = 0;
|
|
141
|
+
this.#size = 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Returns an iterator that yields elements in logical order (oldest to newest).
|
|
146
|
+
* Allows the buffer to be used with `for...of` loops and spread syntax.
|
|
147
|
+
*
|
|
148
|
+
* @yields Elements in FIFO order.
|
|
149
|
+
*/
|
|
150
|
+
*[Symbol.iterator](): Iterator<T> {
|
|
151
|
+
for (let i = 0; i < this.#size; i++) {
|
|
152
|
+
yield this.#buf[(this.#head + i) % this.capacity] as T;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Creates a new array containing all elements in logical order (oldest to newest).
|
|
158
|
+
*
|
|
159
|
+
* @returns A new array with all buffer elements.
|
|
160
|
+
*/
|
|
161
|
+
toArray(): T[] {
|
|
162
|
+
if (this.#head + this.#size <= this.capacity) {
|
|
163
|
+
return this.#buf.slice(this.#head, this.#head + this.#size) as T[];
|
|
164
|
+
}
|
|
165
|
+
const tail = this.#buf.slice(this.#head, this.capacity);
|
|
166
|
+
const head = this.#buf.slice(0, (this.#head + this.#size) % this.capacity);
|
|
167
|
+
return tail.concat(head) as T[];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as fsp from "node:fs/promises";
|
|
3
|
+
import * as Module from "node:module";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* On-demand runtime dependency support for native-heavy optional packages
|
|
8
|
+
* (Transformers.js, fastembed) that are never bundled into the CLI or the
|
|
9
|
+
* compiled binary. Consumers `bun install` a pinned dependency set into a
|
|
10
|
+
* cache directory on first use ({@link ensureRuntimeInstalled}) and load the
|
|
11
|
+
* entrypoint via `createRequire`.
|
|
12
|
+
*
|
|
13
|
+
* Bun's compiled-binary module resolver only finds `<pkg>/index.js` for bare
|
|
14
|
+
* specifiers loaded from the *real* filesystem — it ignores `main`/`exports`
|
|
15
|
+
* (issue #1763). Runtime-installed graphs (`@huggingface/transformers` →
|
|
16
|
+
* `onnxruntime-node` → `onnxruntime-common`, `fastembed` →
|
|
17
|
+
* `@anush008/tokenizers` → platform binding) all point `main`/`exports` at
|
|
18
|
+
* nested files, so the stock resolver cannot load any of them. We patch
|
|
19
|
+
* `Module._resolveFilename` to resolve those bare specifiers against the
|
|
20
|
+
* registered runtime caches ourselves, honoring `main`/`exports`.
|
|
21
|
+
*
|
|
22
|
+
* This module is filesystem-pure aside from {@link installRuntimeModuleResolver}
|
|
23
|
+
* mutating the `node:module` resolver, so the resolution logic is unit-testable
|
|
24
|
+
* without a compiled binary.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Conditions honored when resolving an `exports` map for a CommonJS `require`. */
|
|
28
|
+
const RUNTIME_CONDITIONS: Record<string, true> = { node: true, require: true, default: true };
|
|
29
|
+
|
|
30
|
+
/** Extension probes appended to a `main`/`exports` target that lacks one. */
|
|
31
|
+
const RUNTIME_EXTENSIONS: readonly string[] = [".js", ".cjs", ".mjs", ".json", ".node"];
|
|
32
|
+
|
|
33
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
34
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Walk a conditional `exports` target (string, array of fallbacks, or a
|
|
39
|
+
* condition object) and return the first relative path that matches a runtime
|
|
40
|
+
* condition in declaration order. Returns `null` when nothing applies (e.g.
|
|
41
|
+
* an `import`-only entry).
|
|
42
|
+
*/
|
|
43
|
+
export function selectConditionalTarget(target: unknown): string | null {
|
|
44
|
+
if (typeof target === "string") return target;
|
|
45
|
+
if (Array.isArray(target)) {
|
|
46
|
+
for (const entry of target) {
|
|
47
|
+
const resolved = selectConditionalTarget(entry);
|
|
48
|
+
if (resolved) return resolved;
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
if (isRecord(target)) {
|
|
53
|
+
for (const condition in target) {
|
|
54
|
+
if (!RUNTIME_CONDITIONS[condition]) continue;
|
|
55
|
+
const resolved = selectConditionalTarget(target[condition]);
|
|
56
|
+
if (resolved) return resolved;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Resolve a relative target inside a package to a concrete file path, probing extensions and `index`. */
|
|
63
|
+
function resolveFileTarget(pkgDir: string, relative: string): string | null {
|
|
64
|
+
const base = path.join(pkgDir, relative);
|
|
65
|
+
const candidates = [base, ...RUNTIME_EXTENSIONS.map(ext => base + ext)];
|
|
66
|
+
for (const candidate of candidates) {
|
|
67
|
+
try {
|
|
68
|
+
const stat = fs.statSync(candidate);
|
|
69
|
+
if (stat.isFile()) return candidate;
|
|
70
|
+
if (stat.isDirectory()) {
|
|
71
|
+
const indexed = resolveFileTarget(candidate, "index");
|
|
72
|
+
if (indexed) return indexed;
|
|
73
|
+
}
|
|
74
|
+
} catch {
|
|
75
|
+
// missing candidate — keep probing
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function resolveExportsEntry(
|
|
82
|
+
pkgDir: string,
|
|
83
|
+
exports: Record<string, unknown>,
|
|
84
|
+
subpath: string | undefined,
|
|
85
|
+
): string | null {
|
|
86
|
+
let subpathMap = false;
|
|
87
|
+
for (const key in exports) {
|
|
88
|
+
subpathMap = key === "." || key.startsWith("./");
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
if (subpathMap) {
|
|
92
|
+
const key = subpath ? `./${subpath}` : ".";
|
|
93
|
+
if (!(key in exports)) return null;
|
|
94
|
+
const target = selectConditionalTarget(exports[key]);
|
|
95
|
+
return target ? resolveFileTarget(pkgDir, target) : null;
|
|
96
|
+
}
|
|
97
|
+
// A bare condition map only describes the package root, so a subpath
|
|
98
|
+
// request falls through to plain path joining at the call site.
|
|
99
|
+
if (subpath) return null;
|
|
100
|
+
const target = selectConditionalTarget(exports);
|
|
101
|
+
return target ? resolveFileTarget(pkgDir, target) : null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Split a bare specifier into its package name and optional subpath, handling
|
|
106
|
+
* scoped packages (`@scope/name/sub` → `@scope/name` + `sub`).
|
|
107
|
+
*/
|
|
108
|
+
export function splitBareSpecifier(specifier: string): { packageName: string; subpath: string | undefined } {
|
|
109
|
+
const segments = specifier.split("/");
|
|
110
|
+
const take = specifier.startsWith("@") ? 2 : 1;
|
|
111
|
+
const packageName = segments.slice(0, take).join("/");
|
|
112
|
+
const subpath = segments.length > take ? segments.slice(take).join("/") : undefined;
|
|
113
|
+
return { packageName, subpath };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolve a bare specifier against an installed `node_modules` directory,
|
|
118
|
+
* honoring `exports` (CommonJS conditions), then `main`, then `index.js`.
|
|
119
|
+
* Returns an absolute file path, or `null` when the package/entry is absent.
|
|
120
|
+
*/
|
|
121
|
+
export function resolveRuntimeModule(runtimeNodeModules: string, specifier: string): string | null {
|
|
122
|
+
const { packageName, subpath } = splitBareSpecifier(specifier);
|
|
123
|
+
const pkgDir = path.join(runtimeNodeModules, ...packageName.split("/"));
|
|
124
|
+
const manifest = readManifest(pkgDir);
|
|
125
|
+
if (!manifest) return subpath ? resolveFileTarget(pkgDir, subpath) : null;
|
|
126
|
+
|
|
127
|
+
const { exports } = manifest;
|
|
128
|
+
if (typeof exports === "string" || isRecord(exports)) {
|
|
129
|
+
const map = typeof exports === "string" ? { ".": exports } : exports;
|
|
130
|
+
const resolved = resolveExportsEntry(pkgDir, map, subpath);
|
|
131
|
+
if (resolved) return resolved;
|
|
132
|
+
}
|
|
133
|
+
if (subpath) return resolveFileTarget(pkgDir, subpath);
|
|
134
|
+
if (typeof manifest.main === "string") {
|
|
135
|
+
const resolved = resolveFileTarget(pkgDir, manifest.main);
|
|
136
|
+
if (resolved) return resolved;
|
|
137
|
+
}
|
|
138
|
+
return resolveFileTarget(pkgDir, "index.js");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function readManifest(pkgDir: string): Record<string, unknown> | null {
|
|
142
|
+
try {
|
|
143
|
+
const parsed: unknown = JSON.parse(fs.readFileSync(path.join(pkgDir, "package.json"), "utf8"));
|
|
144
|
+
return isRecord(parsed) ? parsed : null;
|
|
145
|
+
} catch {
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
interface ModuleResolver {
|
|
151
|
+
_resolveFilename(request: string, parent: unknown, isMain: boolean, options?: unknown): string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
interface ResolverRegistration {
|
|
155
|
+
runtimeNodeModules: string;
|
|
156
|
+
stubs: Record<string, string>;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const REGISTRY = Symbol.for("omp.runtimeModuleResolver.registry");
|
|
160
|
+
const PATCHED = Symbol.for("omp.runtimeModuleResolver.patched");
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The registration list lives on `globalThis` so a bundled copy and a
|
|
164
|
+
* source copy of this module in one process share the same registry — the
|
|
165
|
+
* resolver is patched once per process, and the patched closure must see
|
|
166
|
+
* every registration.
|
|
167
|
+
*/
|
|
168
|
+
function resolverRegistry(): ResolverRegistration[] {
|
|
169
|
+
const holder = globalThis as { [REGISTRY]?: ResolverRegistration[] };
|
|
170
|
+
holder[REGISTRY] ??= [];
|
|
171
|
+
return holder[REGISTRY];
|
|
172
|
+
}
|
|
173
|
+
function pathContains(root: string, candidate: string): boolean {
|
|
174
|
+
const relative = path.relative(root, candidate);
|
|
175
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function parentFilename(parent: unknown): string | null {
|
|
179
|
+
if (!isRecord(parent)) return null;
|
|
180
|
+
const filename = parent.filename;
|
|
181
|
+
return typeof filename === "string" ? filename : null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface RuntimeResolverOptions {
|
|
185
|
+
/** Absolute path to the runtime cache's `node_modules`. */
|
|
186
|
+
runtimeNodeModules: string;
|
|
187
|
+
/** Bare specifier → absolute file path overrides (e.g. `sharp` → no-op stub). */
|
|
188
|
+
stubs?: Record<string, string>;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Patch `node:module`'s resolver (idempotently) so bare specifiers that the
|
|
193
|
+
* stock compiled-binary resolver cannot find fall back to the registered
|
|
194
|
+
* runtime caches. Stock resolution is tried first and kept for anything
|
|
195
|
+
* outside the registered roots (bundled imports, node builtins, host or
|
|
196
|
+
* extension trees). Multiple runtime roots may register; they are consulted
|
|
197
|
+
* in registration order. Returns an uninstaller that drops the registration
|
|
198
|
+
* and restores the stock resolver once no registrations remain.
|
|
199
|
+
*
|
|
200
|
+
* One stock "success" is distrusted: the compiled-binary resolver ignores
|
|
201
|
+
* `main`/`exports` for real-FS packages (Bun #1763), so a package shipping
|
|
202
|
+
* its TS source next to `dist/` (e.g. `@huggingface/hub`'s root `index.ts`)
|
|
203
|
+
* resolves to the wrong file. When the stock hit lands inside a registered
|
|
204
|
+
* runtime root, the manifest-aware resolution wins.
|
|
205
|
+
*
|
|
206
|
+
* KNOWN LIMITATION (Bun 1.3.14): while any JS override of
|
|
207
|
+
* `Module._resolveFilename` is installed, Bun routes `createRequire(...)`
|
|
208
|
+
* resolution through it with `parent === undefined` — the requester context is
|
|
209
|
+
* never passed, so relative requires from a `createRequire` require fail with
|
|
210
|
+
* "Cannot find module './x' from ''". The override cannot recover what it is
|
|
211
|
+
* never given. Keep this patch scoped to dedicated worker/runtime processes
|
|
212
|
+
* (tiny-inference, fastembed); never install it in the main agent process,
|
|
213
|
+
* where legacy-pi extensions rely on `createRequire` relative requires.
|
|
214
|
+
*/
|
|
215
|
+
export function installRuntimeModuleResolver({ runtimeNodeModules, stubs = {} }: RuntimeResolverOptions): () => void {
|
|
216
|
+
const registry = resolverRegistry();
|
|
217
|
+
const existing = registry.find(entry => entry.runtimeNodeModules === runtimeNodeModules);
|
|
218
|
+
if (existing) Object.assign(existing.stubs, stubs);
|
|
219
|
+
else registry.push({ runtimeNodeModules, stubs: { ...stubs } });
|
|
220
|
+
|
|
221
|
+
const resolver = (Module as unknown as { default?: ModuleResolver } & ModuleResolver).default ?? Module;
|
|
222
|
+
const target = resolver as unknown as ModuleResolver & { [PATCHED]?: () => void };
|
|
223
|
+
const uninstall = (): void => {
|
|
224
|
+
const entries = resolverRegistry();
|
|
225
|
+
const index = entries.findIndex(entry => entry.runtimeNodeModules === runtimeNodeModules);
|
|
226
|
+
if (index !== -1) entries.splice(index, 1);
|
|
227
|
+
if (entries.length === 0) target[PATCHED]?.();
|
|
228
|
+
};
|
|
229
|
+
if (target[PATCHED]) return uninstall;
|
|
230
|
+
const pristine = target._resolveFilename;
|
|
231
|
+
const original = pristine.bind(target);
|
|
232
|
+
target._resolveFilename = (request: string, parent: unknown, isMain: boolean, options?: unknown): string => {
|
|
233
|
+
let stockResolved: string | null = null;
|
|
234
|
+
let stockError: unknown;
|
|
235
|
+
try {
|
|
236
|
+
stockResolved = original(request, parent, isMain, options);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
stockError = error;
|
|
239
|
+
}
|
|
240
|
+
const bare = !request.startsWith(".") && !request.startsWith("node:") && !path.isAbsolute(request);
|
|
241
|
+
if (bare) {
|
|
242
|
+
const parentFile = parentFilename(parent);
|
|
243
|
+
for (const registration of resolverRegistry()) {
|
|
244
|
+
const parentInRuntime = parentFile !== null && pathContains(registration.runtimeNodeModules, parentFile);
|
|
245
|
+
if (parentInRuntime) {
|
|
246
|
+
const stub = registration.stubs[request];
|
|
247
|
+
if (stub) return stub;
|
|
248
|
+
if (!stockResolved || !pathContains(registration.runtimeNodeModules, stockResolved)) {
|
|
249
|
+
const fallback = resolveRuntimeModule(registration.runtimeNodeModules, request);
|
|
250
|
+
if (fallback) return fallback;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (stockResolved) {
|
|
254
|
+
// Correct a stock hit only inside the top-level package the
|
|
255
|
+
// request names. A hit in a nested node_modules (e.g. tar's
|
|
256
|
+
// minizlib resolving its own minipass@3 under
|
|
257
|
+
// <root>/minizlib/node_modules/) is version-correct — overriding
|
|
258
|
+
// it with the top-level instance would cross major versions.
|
|
259
|
+
const { packageName } = splitBareSpecifier(request);
|
|
260
|
+
const pkgDir = path.join(registration.runtimeNodeModules, ...packageName.split("/"));
|
|
261
|
+
if (!stockResolved.startsWith(pkgDir + path.sep)) continue;
|
|
262
|
+
if (path.relative(pkgDir, stockResolved).split(path.sep).includes("node_modules")) continue;
|
|
263
|
+
const expected = resolveRuntimeModule(registration.runtimeNodeModules, request);
|
|
264
|
+
if (expected) return expected;
|
|
265
|
+
} else {
|
|
266
|
+
const stub = registration.stubs[request];
|
|
267
|
+
if (stub) return stub;
|
|
268
|
+
const fallback = resolveRuntimeModule(registration.runtimeNodeModules, request);
|
|
269
|
+
if (fallback) return fallback;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
if (stockResolved) return stockResolved;
|
|
274
|
+
throw stockError;
|
|
275
|
+
};
|
|
276
|
+
target[PATCHED] = () => {
|
|
277
|
+
target._resolveFilename = pristine;
|
|
278
|
+
delete target[PATCHED];
|
|
279
|
+
};
|
|
280
|
+
return uninstall;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Pinned dependency set materialized into a runtime cache directory. */
|
|
284
|
+
export interface RuntimeInstallSpec {
|
|
285
|
+
dependencies: Record<string, string>;
|
|
286
|
+
/** Version pins forced across the whole runtime tree (bun `overrides`), e.g. dislodging a transitive dep. */
|
|
287
|
+
overrides?: Record<string, string>;
|
|
288
|
+
/** Packages whose lifecycle scripts bun may run during the install. */
|
|
289
|
+
trustedDependencies?: string[];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
export type RuntimeInstallPhase = "initiate" | "download" | "done";
|
|
293
|
+
|
|
294
|
+
export interface EnsureRuntimeInstalledOptions {
|
|
295
|
+
/** Directory owning the runtime `package.json` + `node_modules`. */
|
|
296
|
+
runtimeDir: string;
|
|
297
|
+
install: RuntimeInstallSpec;
|
|
298
|
+
/** Package whose installed manifest marks the runtime complete; defaults to the first dependency. */
|
|
299
|
+
probePackage?: string;
|
|
300
|
+
/** Phase notifications (progress UI); not emitted when already installed. */
|
|
301
|
+
onPhase?: (phase: RuntimeInstallPhase) => void;
|
|
302
|
+
lockAttempts?: number;
|
|
303
|
+
lockSleepMs?: number;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function isErrnoCode(error: unknown, code: string): boolean {
|
|
307
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === code;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async function acquireInstallLock(runtimeDir: string, attempts: number, sleepMs: number): Promise<() => Promise<void>> {
|
|
311
|
+
const lockDir = `${runtimeDir}.lock`;
|
|
312
|
+
await fsp.mkdir(path.dirname(lockDir), { recursive: true });
|
|
313
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
314
|
+
try {
|
|
315
|
+
await fsp.mkdir(lockDir);
|
|
316
|
+
return async () => {
|
|
317
|
+
await fsp.rm(lockDir, { recursive: true, force: true });
|
|
318
|
+
};
|
|
319
|
+
} catch (error) {
|
|
320
|
+
if (!isErrnoCode(error, "EEXIST")) throw error;
|
|
321
|
+
await Bun.sleep(sleepMs);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
throw new Error(`Timed out waiting for runtime install lock: ${lockDir}`);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export async function writeRuntimeManifest(runtimeDir: string, install: RuntimeInstallSpec): Promise<void> {
|
|
328
|
+
await fsp.mkdir(runtimeDir, { recursive: true });
|
|
329
|
+
const manifest: Record<string, unknown> = {
|
|
330
|
+
private: true,
|
|
331
|
+
type: "module",
|
|
332
|
+
dependencies: install.dependencies,
|
|
333
|
+
};
|
|
334
|
+
if (install.overrides && Object.keys(install.overrides).length) manifest.overrides = install.overrides;
|
|
335
|
+
if (install.trustedDependencies?.length) manifest.trustedDependencies = install.trustedDependencies;
|
|
336
|
+
await Bun.write(path.join(runtimeDir, "package.json"), `${JSON.stringify(manifest, null, "\t")}\n`);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function readPipe(stream: ReadableStream<Uint8Array> | null): Promise<string> {
|
|
340
|
+
if (!stream) return "";
|
|
341
|
+
return new Response(stream).text();
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async function runRuntimeInstall(runtimeDir: string): Promise<void> {
|
|
345
|
+
// `process.execPath` is plain bun in source/bundle mode and the compiled
|
|
346
|
+
// binary otherwise; BUN_BE_BUN makes the compiled binary act as bun.
|
|
347
|
+
const proc = Bun.spawn([process.execPath, "install", "--cwd", runtimeDir, "--production"], {
|
|
348
|
+
env: { ...Bun.env, BUN_BE_BUN: "1" },
|
|
349
|
+
stdout: "pipe",
|
|
350
|
+
stderr: "pipe",
|
|
351
|
+
});
|
|
352
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
353
|
+
readPipe(proc.stdout as ReadableStream<Uint8Array> | null),
|
|
354
|
+
readPipe(proc.stderr as ReadableStream<Uint8Array> | null),
|
|
355
|
+
proc.exited,
|
|
356
|
+
]);
|
|
357
|
+
if (exitCode === 0) return;
|
|
358
|
+
const output = `${stdout}\n${stderr}`.trim();
|
|
359
|
+
throw new Error(
|
|
360
|
+
`Failed to install runtime at ${runtimeDir} with ${process.execPath} install (exit ${exitCode}): ${output}`,
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Materialize a pinned dependency set into `runtimeDir` (idempotent,
|
|
366
|
+
* cross-process safe via a lock directory). Returns `runtimeDir`.
|
|
367
|
+
*/
|
|
368
|
+
export async function ensureRuntimeInstalled(options: EnsureRuntimeInstalledOptions): Promise<string> {
|
|
369
|
+
const { runtimeDir, install, onPhase, lockAttempts = 240, lockSleepMs = 250 } = options;
|
|
370
|
+
let probePackage = options.probePackage;
|
|
371
|
+
if (!probePackage) {
|
|
372
|
+
for (const name in install.dependencies) {
|
|
373
|
+
probePackage = name;
|
|
374
|
+
break;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (!probePackage) throw new Error(`Runtime install at ${runtimeDir} declares no dependencies`);
|
|
378
|
+
const probeManifest = Bun.file(path.join(runtimeDir, "node_modules", ...probePackage.split("/"), "package.json"));
|
|
379
|
+
if (await probeManifest.exists()) return runtimeDir;
|
|
380
|
+
|
|
381
|
+
onPhase?.("initiate");
|
|
382
|
+
const releaseLock = await acquireInstallLock(runtimeDir, lockAttempts, lockSleepMs);
|
|
383
|
+
try {
|
|
384
|
+
if (await probeManifest.exists()) return runtimeDir;
|
|
385
|
+
await writeRuntimeManifest(runtimeDir, install);
|
|
386
|
+
onPhase?.("download");
|
|
387
|
+
await runRuntimeInstall(runtimeDir);
|
|
388
|
+
onPhase?.("done");
|
|
389
|
+
return runtimeDir;
|
|
390
|
+
} finally {
|
|
391
|
+
await releaseLock();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strip ANSI escape sequences, remove control characters / lone surrogates,
|
|
3
|
+
* and normalize line endings.
|
|
4
|
+
*
|
|
5
|
+
* Bun-native implementation of the former native `sanitizeText` (see
|
|
6
|
+
* `crates/pi-natives/src/text.rs::sanitize_text`). JavaScript strings are
|
|
7
|
+
* already UTF-16 code-unit arrays. `toWellFormed()` handles the uncommon
|
|
8
|
+
* malformed path; when it changes the input, replacement characters are
|
|
9
|
+
* dropped and the normalized result goes through the well-formed sanitizer.
|
|
10
|
+
*
|
|
11
|
+
* Fast path: well-formed input with no controls or ANSI returns the original
|
|
12
|
+
* string after the control probe.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const ESC_CHAR = "\x1b";
|
|
16
|
+
|
|
17
|
+
// Well-formed strings only need control/ANSI detection: C0 (excl. \t \n),
|
|
18
|
+
// CR, DEL, and C1. ESC (0x1B) is in \x0B-\x1F.
|
|
19
|
+
const CONTROL_RE = /[\x00-\x08\x0B-\x1F\x7F-\x9F]/g;
|
|
20
|
+
|
|
21
|
+
const REPLACEMENT_CHAR = "\ufffd";
|
|
22
|
+
|
|
23
|
+
export function sanitizeText(text: string): string {
|
|
24
|
+
const wellFormed = text.toWellFormed();
|
|
25
|
+
if (wellFormed !== text) {
|
|
26
|
+
return sanitizeWellFormedText(wellFormed.replaceAll(REPLACEMENT_CHAR, ""));
|
|
27
|
+
}
|
|
28
|
+
return sanitizeWellFormedText(text);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function sanitizeWellFormedText(text: string): string {
|
|
32
|
+
CONTROL_RE.lastIndex = 0;
|
|
33
|
+
if (CONTROL_RE.exec(text) === null) return text;
|
|
34
|
+
|
|
35
|
+
const stripped = text.indexOf(ESC_CHAR) === -1 ? text : Bun.stripANSI(text);
|
|
36
|
+
CONTROL_RE.lastIndex = 0;
|
|
37
|
+
return stripped.replace(CONTROL_RE, "");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Escape the three XML-significant characters (`&`, `<`, `>`) in text destined
|
|
42
|
+
* for an XML/markup element body. Allocation-conscious: returns the input
|
|
43
|
+
* unchanged (same reference) when nothing needs escaping. Quotes are left as-is
|
|
44
|
+
* — use it for element text, not attribute values.
|
|
45
|
+
*/
|
|
46
|
+
export function escapeXmlText(input: string): string {
|
|
47
|
+
let firstEscapable = -1;
|
|
48
|
+
for (let index = 0; index < input.length; index++) {
|
|
49
|
+
const char = input.charCodeAt(index);
|
|
50
|
+
if (char === 38 || char === 60 || char === 62) {
|
|
51
|
+
firstEscapable = index;
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (firstEscapable === -1) return input;
|
|
56
|
+
|
|
57
|
+
let output = input.slice(0, firstEscapable);
|
|
58
|
+
for (let index = firstEscapable; index < input.length; index++) {
|
|
59
|
+
const char = input[index];
|
|
60
|
+
if (char === "&") output += "&";
|
|
61
|
+
else if (char === "<") output += "<";
|
|
62
|
+
else if (char === ">") output += ">";
|
|
63
|
+
else output += char;
|
|
64
|
+
}
|
|
65
|
+
return output;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Escape XML-significant characters for an attribute VALUE: the three body
|
|
70
|
+
* characters (`&`, `<`, `>`) plus the double quote (`"` → `"`) that would
|
|
71
|
+
* otherwise close the attribute. Allocation-conscious: returns the input
|
|
72
|
+
* unchanged (same reference) when nothing needs escaping. Use it for attribute
|
|
73
|
+
* values; {@link escapeXmlText} is for element bodies and leaves `"` intact.
|
|
74
|
+
*/
|
|
75
|
+
export function escapeXmlAttribute(input: string): string {
|
|
76
|
+
let firstEscapable = -1;
|
|
77
|
+
for (let index = 0; index < input.length; index++) {
|
|
78
|
+
const char = input.charCodeAt(index);
|
|
79
|
+
if (char === 38 || char === 60 || char === 62 || char === 34) {
|
|
80
|
+
firstEscapable = index;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (firstEscapable === -1) return input;
|
|
85
|
+
|
|
86
|
+
let output = input.slice(0, firstEscapable);
|
|
87
|
+
for (let index = firstEscapable; index < input.length; index++) {
|
|
88
|
+
const char = input[index];
|
|
89
|
+
if (char === "&") output += "&";
|
|
90
|
+
else if (char === "<") output += "<";
|
|
91
|
+
else if (char === ">") output += ">";
|
|
92
|
+
else if (char === '"') output += """;
|
|
93
|
+
else output += char;
|
|
94
|
+
}
|
|
95
|
+
return output;
|
|
96
|
+
}
|