rusteerio 0.0.0-stage → 0.1.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 (206) hide show
  1. package/CONTRIBUTING.md +73 -0
  2. package/Cargo.lock +1051 -0
  3. package/Cargo.toml +36 -0
  4. package/LICENSE +21 -0
  5. package/README.md +252 -2
  6. package/RELEASING.md +138 -0
  7. package/THIRD_PARTY_NOTICES.md +7443 -0
  8. package/build.rs +3 -0
  9. package/dist/chunk-DKULX6VO.mjs +3185 -0
  10. package/dist/chunk-DKULX6VO.mjs.map +1 -0
  11. package/dist/chunk-OHPGEY3E.mjs +32 -0
  12. package/dist/chunk-OHPGEY3E.mjs.map +1 -0
  13. package/dist/index.cjs +3230 -0
  14. package/dist/index.cjs.map +1 -0
  15. package/dist/index.d.mts +113 -0
  16. package/dist/index.d.ts +113 -0
  17. package/dist/index.mjs +38 -0
  18. package/dist/index.mjs.map +1 -0
  19. package/dist/slim.cjs +2441 -0
  20. package/dist/slim.cjs.map +1 -0
  21. package/dist/slim.d.mts +11 -0
  22. package/dist/slim.d.ts +11 -0
  23. package/dist/slim.mjs +19 -0
  24. package/dist/slim.mjs.map +1 -0
  25. package/dist/types-BP6TpxE5.d.mts +532 -0
  26. package/dist/types-BP6TpxE5.d.ts +532 -0
  27. package/dist/utils.cjs +61 -0
  28. package/dist/utils.cjs.map +1 -0
  29. package/dist/utils.d.mts +13 -0
  30. package/dist/utils.d.ts +13 -0
  31. package/dist/utils.mjs +17 -0
  32. package/dist/utils.mjs.map +1 -0
  33. package/native/src/arena.rs +1781 -0
  34. package/native/src/attributes.rs +923 -0
  35. package/native/src/codec.rs +172 -0
  36. package/native/src/diagnostics.rs +1249 -0
  37. package/native/src/extraction.rs +226 -0
  38. package/native/src/gc.rs +166 -0
  39. package/native/src/hook_sink.rs +727 -0
  40. package/native/src/hook_stream.rs +513 -0
  41. package/native/src/lib.rs +2095 -0
  42. package/native/src/location.rs +392 -0
  43. package/native/src/runtime.rs +189 -0
  44. package/native/src/selector.rs +1287 -0
  45. package/native/src/sink.rs +261 -0
  46. package/native/src/stream.rs +334 -0
  47. package/native/src/tokenizer.rs +355 -0
  48. package/native/src/tracker.rs +232 -0
  49. package/native/vendor/README.md +25 -0
  50. package/native/vendor/html5ever-0.35.0/.cargo_vcs_info.json +6 -0
  51. package/native/vendor/html5ever-0.35.0/Cargo.lock +828 -0
  52. package/native/vendor/html5ever-0.35.0/Cargo.toml +85 -0
  53. package/native/vendor/html5ever-0.35.0/LICENSE-APACHE +201 -0
  54. package/native/vendor/html5ever-0.35.0/LICENSE-MIT +25 -0
  55. package/native/vendor/html5ever-0.35.0/README.md +60 -0
  56. package/native/vendor/html5ever-0.35.0/benches/html5ever.rs +86 -0
  57. package/native/vendor/html5ever-0.35.0/data/bench/lipsum-zh.html +19 -0
  58. package/native/vendor/html5ever-0.35.0/data/bench/lipsum.html +40 -0
  59. package/native/vendor/html5ever-0.35.0/data/bench/medium-fragment.html +24 -0
  60. package/native/vendor/html5ever-0.35.0/data/bench/small-fragment.html +7 -0
  61. package/native/vendor/html5ever-0.35.0/data/bench/strong.html +1 -0
  62. package/native/vendor/html5ever-0.35.0/data/bench/tiny-fragment.html +1 -0
  63. package/native/vendor/html5ever-0.35.0/examples/arena.rs +356 -0
  64. package/native/vendor/html5ever-0.35.0/examples/capi/tokenize.c +74 -0
  65. package/native/vendor/html5ever-0.35.0/examples/noop-tokenize.rs +47 -0
  66. package/native/vendor/html5ever-0.35.0/examples/noop-tree-builder.rs +132 -0
  67. package/native/vendor/html5ever-0.35.0/examples/print-tree-actions.rs +185 -0
  68. package/native/vendor/html5ever-0.35.0/examples/tokenize.rs +111 -0
  69. package/native/vendor/html5ever-0.35.0/src/driver.rs +148 -0
  70. package/native/vendor/html5ever-0.35.0/src/lib.rs +30 -0
  71. package/native/vendor/html5ever-0.35.0/src/macros.rs +34 -0
  72. package/native/vendor/html5ever-0.35.0/src/serialize/mod.rs +255 -0
  73. package/native/vendor/html5ever-0.35.0/src/tokenizer/char_ref/mod.rs +451 -0
  74. package/native/vendor/html5ever-0.35.0/src/tokenizer/interface.rs +99 -0
  75. package/native/vendor/html5ever-0.35.0/src/tokenizer/mod.rs +2305 -0
  76. package/native/vendor/html5ever-0.35.0/src/tokenizer/states.rs +97 -0
  77. package/native/vendor/html5ever-0.35.0/src/tree_builder/data.rs +170 -0
  78. package/native/vendor/html5ever-0.35.0/src/tree_builder/mod.rs +1848 -0
  79. package/native/vendor/html5ever-0.35.0/src/tree_builder/rules.rs +1494 -0
  80. package/native/vendor/html5ever-0.35.0/src/tree_builder/tag_sets.rs +114 -0
  81. package/native/vendor/html5ever-0.35.0/src/tree_builder/types.rs +91 -0
  82. package/native/vendor/html5ever-0.35.0/src/util/str.rs +52 -0
  83. package/native/vendor/html5gum-0.8.4/Cargo.lock +1006 -0
  84. package/native/vendor/html5gum-0.8.4/Cargo.toml +109 -0
  85. package/native/vendor/html5gum-0.8.4/LICENSE +19 -0
  86. package/native/vendor/html5gum-0.8.4/README.md +110 -0
  87. package/native/vendor/html5gum-0.8.4/benches/files/0050.html +2131 -0
  88. package/native/vendor/html5gum-0.8.4/benches/files/6D30.html +1004 -0
  89. package/native/vendor/html5gum-0.8.4/benches/files.rs +32 -0
  90. package/native/vendor/html5gum-0.8.4/benches/patterns.rs +31 -0
  91. package/native/vendor/html5gum-0.8.4/src/arrayvec.rs +29 -0
  92. package/native/vendor/html5gum-0.8.4/src/char_validator.rs +134 -0
  93. package/native/vendor/html5gum-0.8.4/src/emitters/callback.rs +676 -0
  94. package/native/vendor/html5gum-0.8.4/src/emitters/default.rs +207 -0
  95. package/native/vendor/html5gum-0.8.4/src/emitters/emitter.rs +344 -0
  96. package/native/vendor/html5gum-0.8.4/src/emitters/html5ever.rs +306 -0
  97. package/native/vendor/html5gum-0.8.4/src/emitters/mod.rs +25 -0
  98. package/native/vendor/html5gum-0.8.4/src/entities.rs +3342 -0
  99. package/native/vendor/html5gum-0.8.4/src/error.rs +96 -0
  100. package/native/vendor/html5gum-0.8.4/src/htmlstring.rs +130 -0
  101. package/native/vendor/html5gum-0.8.4/src/lib.rs +66 -0
  102. package/native/vendor/html5gum-0.8.4/src/machine.rs +2136 -0
  103. package/native/vendor/html5gum-0.8.4/src/machine_helper.rs +277 -0
  104. package/native/vendor/html5gum-0.8.4/src/read_helper.rs +244 -0
  105. package/native/vendor/html5gum-0.8.4/src/reader.rs +440 -0
  106. package/native/vendor/html5gum-0.8.4/src/span.rs +98 -0
  107. package/native/vendor/html5gum-0.8.4/src/state.rs +16 -0
  108. package/native/vendor/html5gum-0.8.4/src/testutils.rs +71 -0
  109. package/native/vendor/html5gum-0.8.4/src/tokenizer.rs +189 -0
  110. package/native/vendor/html5gum-0.8.4/src/utils.rs +88 -0
  111. package/native/vendor/markup5ever-0.35.0/.cargo_vcs_info.json +6 -0
  112. package/native/vendor/markup5ever-0.35.0/Cargo.toml +45 -0
  113. package/native/vendor/markup5ever-0.35.0/LICENSE-APACHE +201 -0
  114. package/native/vendor/markup5ever-0.35.0/LICENSE-MIT +25 -0
  115. package/native/vendor/markup5ever-0.35.0/interface/mod.rs +422 -0
  116. package/native/vendor/markup5ever-0.35.0/interface/tree_builder.rs +280 -0
  117. package/native/vendor/markup5ever-0.35.0/lib.rs +59 -0
  118. package/native/vendor/markup5ever-0.35.0/serialize.rs +77 -0
  119. package/native/vendor/markup5ever-0.35.0/util/buffer_queue.rs +333 -0
  120. package/native/vendor/markup5ever-0.35.0/util/smallcharset.rs +88 -0
  121. package/package.json +137 -4
  122. package/rusteerio.darwin-arm64.node +0 -0
  123. package/rusteerio.darwin-x64.node +0 -0
  124. package/rusteerio.linux-arm64-gnu.node +0 -0
  125. package/rusteerio.linux-arm64-musl.node +0 -0
  126. package/rusteerio.linux-x64-gnu.node +0 -0
  127. package/rusteerio.linux-x64-musl.node +0 -0
  128. package/rusteerio.win32-arm64-msvc.node +0 -0
  129. package/rusteerio.win32-x64-msvc.node +0 -0
  130. package/scripts/check-native.mjs +15 -0
  131. package/scripts/check-release.mjs +235 -0
  132. package/scripts/fix-declarations.mjs +27 -0
  133. package/scripts/generate-notices.mjs +176 -0
  134. package/scripts/notices/README.md +13 -0
  135. package/scripts/notices/cheerio-MIT.txt +21 -0
  136. package/scripts/notices/html5ever-AUTHORS.txt +20 -0
  137. package/scripts/notices/html5ever-COPYRIGHT.txt +8 -0
  138. package/scripts/notices/html5ever-LICENSE-APACHE.txt +201 -0
  139. package/scripts/notices/html5ever-LICENSE-MIT.txt +25 -0
  140. package/scripts/notices/napi-rs-MIT.txt +43 -0
  141. package/scripts/notices/neon-MIT.txt +19 -0
  142. package/scripts/notices/runtime-01c266bced4a434da005.txt +3 -0
  143. package/scripts/notices/runtime-0a54e647fe54104658b5.txt +14 -0
  144. package/scripts/notices/runtime-0b74dfa0bcee5c420c6b.txt +25 -0
  145. package/scripts/notices/runtime-0d542e0c8804e39aa7f3.txt +177 -0
  146. package/scripts/notices/runtime-0f96a83840e146e43c0e.txt +21 -0
  147. package/scripts/notices/runtime-123a331b5dbf04c30097.txt +25 -0
  148. package/scripts/notices/runtime-172020dbfd5b53a226df.txt +34 -0
  149. package/scripts/notices/runtime-209fbbe0ad52d9235e37.txt +26 -0
  150. package/scripts/notices/runtime-23f18e03dc49df91622f.txt +23 -0
  151. package/scripts/notices/runtime-268872b9816f90fd8e85.txt +220 -0
  152. package/scripts/notices/runtime-35242e7a83f69875e6ed.txt +176 -0
  153. package/scripts/notices/runtime-378f5840b258e2779c39.txt +25 -0
  154. package/scripts/notices/runtime-4108245a1f2df9d4e94d.txt +25 -0
  155. package/scripts/notices/runtime-4455bf75a91154108304.txt +21 -0
  156. package/scripts/notices/runtime-553fffcd9b1cb158bc3e.txt +13 -0
  157. package/scripts/notices/runtime-62c7a1e35f56406896d7.txt +176 -0
  158. package/scripts/notices/runtime-6485b8ed310d3f0340bf.txt +25 -0
  159. package/scripts/notices/runtime-69fef7b0f322a6555415.txt +201 -0
  160. package/scripts/notices/runtime-6df43f6f4b5d4587f3d8.txt +187 -0
  161. package/scripts/notices/runtime-799e9ca9d179295ef372.txt +24 -0
  162. package/scripts/notices/runtime-7b63ecd5f1902af1b637.txt +25 -0
  163. package/scripts/notices/runtime-7bc7d5de2a0b793d4e1a.txt +21 -0
  164. package/scripts/notices/runtime-7e12e5df4bae12cb2158.txt +24 -0
  165. package/scripts/notices/runtime-861399f8c21c042b1105.txt +12 -0
  166. package/scripts/notices/runtime-8ada45cd9f843acf64e4.txt +201 -0
  167. package/scripts/notices/runtime-90eb64f0279b0d9432ac.txt +12 -0
  168. package/scripts/notices/runtime-a60eea817514531668d7.txt +201 -0
  169. package/scripts/notices/runtime-a7c936ff1ed8fa340172.txt +25 -0
  170. package/scripts/notices/runtime-ab6eec6caf0fa5775e41.txt +275 -0
  171. package/scripts/notices/runtime-b1181a40b2a7b25cf66f.txt +19 -0
  172. package/scripts/notices/runtime-b71bd43a069ca0641a9e.txt +25 -0
  173. package/scripts/notices/runtime-c16f8dcf1a368b83be78.txt +201 -0
  174. package/scripts/notices/runtime-c2cfccb812fe482101a8.txt +21 -0
  175. package/scripts/notices/runtime-e99d88d232bf57d70f0f.txt +25 -0
  176. package/scripts/notices/runtime-f32fb3b417a194167cfa.txt +9 -0
  177. package/scripts/notices/runtime-f5062c9a188d81dfe66b.txt +39 -0
  178. package/scripts/notices/runtime-f9070a832833fa7488fd.txt +4 -0
  179. package/scripts/notices/runtime-fab3dd6bdab226f1c086.txt +373 -0
  180. package/scripts/notices/runtime-fba3318d0c600380177e.txt +62 -0
  181. package/scripts/notices/runtime-ff8f68cb076caf8cefe7.txt +25 -0
  182. package/scripts/notices/runtime-ff92bed461f50338dd70.txt +74 -0
  183. package/scripts/notices/runtime.json +1632 -0
  184. package/scripts/notices/sources.json +555 -0
  185. package/scripts/notices/xray-MIT.txt +21 -0
  186. package/src/attributes.ts +271 -0
  187. package/src/codec.ts +74 -0
  188. package/src/dom.ts +87 -0
  189. package/src/extract-plan.ts +97 -0
  190. package/src/extract.ts +130 -0
  191. package/src/hooks.ts +220 -0
  192. package/src/index.ts +223 -0
  193. package/src/io.ts +491 -0
  194. package/src/native.ts +135 -0
  195. package/src/nodes.ts +290 -0
  196. package/src/parse.ts +53 -0
  197. package/src/parser-types.ts +157 -0
  198. package/src/router.ts +327 -0
  199. package/src/selection.ts +566 -0
  200. package/src/slim.ts +12 -0
  201. package/src/transport.ts +104 -0
  202. package/src/types.ts +186 -0
  203. package/src/utils.ts +29 -0
  204. package/src/workers.ts +21 -0
  205. package/tsconfig.json +15 -0
  206. package/tsup.config.ts +16 -0
package/src/io.ts ADDED
@@ -0,0 +1,491 @@
1
+ import { Readable, Writable } from 'node:stream';
2
+ import { request as requestHTTP } from 'node:http';
3
+ import { request as requestHTTPS } from 'node:https';
4
+ import { stringify as stringifyQuery } from 'node:querystring';
5
+ import type { EventEmitter } from 'node:events';
6
+ import { MIMEType } from 'node:util';
7
+ import { createAPI, flattenOptions, load } from './index.js';
8
+ import { requireNative, type NativeStream } from './native.js';
9
+ import { createDiagnosticSession, createTokenizerSession, createTreeAdapterSession, deliverParseErrors, parserOptions } from './hooks.js';
10
+ import { routeDocument } from './router.js';
11
+ import type { CheerioAPI, CheerioOptions, CheerioRequestOptions, DecodeStreamOptions, EncodingOptions } from './types.js';
12
+
13
+ export type LoadCallback = (error: Error | null | undefined, document: CheerioAPI) => void;
14
+
15
+ function encodingLabel(label: string | null | undefined): string | undefined {
16
+ if (!label) return undefined;
17
+ if (label.trim().toLowerCase() === 'x-user-defined') return 'x-user-defined';
18
+ try { return new TextDecoder(label.trim()).encoding; } catch { return undefined; }
19
+ }
20
+
21
+ /** WHATWG decoding with BOM, transport headers, and the HTML meta prescan. */
22
+ function sniffEncoding(buffer: Uint8Array, options: EncodingOptions = {}): string {
23
+ let label: string | undefined;
24
+ if (buffer[0] === 0xef && buffer[1] === 0xbb && buffer[2] === 0xbf) label = 'utf-8';
25
+ else if (buffer[0] === 0xff && buffer[1] === 0xfe) label = 'utf-16le';
26
+ else if (buffer[0] === 0xfe && buffer[1] === 0xff) label = 'utf-16be';
27
+ label ??= encodingLabel((options as EncodingOptions & { userEncoding?: string }).userEncoding);
28
+ label ??= encodingLabel(options.transportLayerEncodingLabel);
29
+ if (!label && buffer[0] === 0 && buffer[1] === 0x3c && buffer[2] === 0 && buffer[3] === 0x3f && buffer[4] === 0 && buffer[5] === 0x78) label = 'utf-16be';
30
+ if (!label && buffer[0] === 0x3c && buffer[1] === 0 && buffer[2] === 0x3f && buffer[3] === 0 && buffer[4] === 0x78 && buffer[5] === 0) label = 'utf-16le';
31
+ if (!label) {
32
+ const prefix = Buffer.from(buffer.subarray(0, options.maxBytes ?? 1024)).toString('latin1').replace(/<!--[\s\S]*?(?:-->|$)/g, '');
33
+ for (const token of prefix.matchAll(/<meta\b([^>]*)>/gi)) {
34
+ const attributes: Record<string, string> = {};
35
+ for (const attribute of token[1].matchAll(/([^\s=/'">]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g)) {
36
+ const key = attribute[1].toLowerCase();
37
+ if (!(key in attributes)) attributes[key] = attribute[2] ?? attribute[3] ?? attribute[4] ?? '';
38
+ }
39
+ const declared = attributes.charset ?? (attributes['http-equiv']?.toLowerCase() === 'content-type'
40
+ ? /\bcharset\s*=\s*["']?([^\s;"']+)/i.exec(attributes.content ?? '')?.[1] : undefined);
41
+ label = encodingLabel(declared);
42
+ if (label) {
43
+ if (label === 'utf-16le' || label === 'utf-16be') label = 'utf-8';
44
+ if (label === 'x-user-defined') label = 'windows-1252';
45
+ break;
46
+ }
47
+ }
48
+ if (!label) {
49
+ const declaration = /<\?xml\b[^>]*\bencoding\s*=\s*["']([^"']+)["']/i.exec(prefix);
50
+ label = encodingLabel(declaration?.[1]);
51
+ if (label === 'utf-16le' || label === 'utf-16be') label = 'utf-8';
52
+ }
53
+ }
54
+ label ??= encodingLabel(options.defaultEncoding) ?? 'windows-1252';
55
+ return label;
56
+ }
57
+
58
+ export function decodeBuffer(buffer: Uint8Array, options: EncodingOptions = {}): string {
59
+ const label = sniffEncoding(buffer, options);
60
+ if (label === 'x-user-defined') {
61
+ let decoded = '';
62
+ for (const byte of buffer) decoded += String.fromCharCode(byte < 0x80 ? byte : 0xf700 + byte);
63
+ return decoded;
64
+ }
65
+ return new TextDecoder(label).decode(buffer);
66
+ }
67
+
68
+ function decodedInput(buffer: Uint8Array, options: DecodeStreamOptions): [string, CheerioOptions] {
69
+ const { encoding, ...parserOptions } = options;
70
+ const xml = !!(options.xmlMode || options.xml);
71
+ return [decodeBuffer(buffer, { defaultEncoding: xml ? 'utf-8' : 'windows-1252', ...encoding }), parserOptions];
72
+ }
73
+
74
+ export function loadBuffer(buffer: Buffer | Uint8Array, options: DecodeStreamOptions = {}): CheerioAPI {
75
+ return load(...decodedInput(buffer, options));
76
+ }
77
+
78
+ function completion(callback: LoadCallback): (error?: Error | null, document?: CheerioAPI) => void {
79
+ let completed = false;
80
+ return (error, document) => {
81
+ if (completed) return;
82
+ completed = true;
83
+ callback(error, document as CheerioAPI);
84
+ };
85
+ }
86
+
87
+ function requestHeaders(input: NonNullable<CheerioRequestOptions['requestOptions']>['headers']): Headers {
88
+ if (!input) return new Headers();
89
+ if (input instanceof Headers) return new Headers(input);
90
+ const headers = new Headers();
91
+ if (Array.isArray(input)) {
92
+ if (input.length % 2) throw new TypeError('HTTP header arrays must contain name/value pairs');
93
+ for (let index = 0; index < input.length; index += 2) headers.append(input[index], input[index + 1]);
94
+ } else {
95
+ const entries: Iterable<[string, string | string[] | undefined]> = Symbol.iterator in input
96
+ ? input as Iterable<[string, string | string[] | undefined]> : Object.entries(input);
97
+ for (const [name, value] of entries) {
98
+ if (value == null) continue;
99
+ for (const item of Array.isArray(value) ? value : [value]) headers.append(name, item);
100
+ }
101
+ }
102
+ return headers;
103
+ }
104
+
105
+ interface HTTPResponse {
106
+ status: number;
107
+ ok: boolean;
108
+ headers: Headers;
109
+ body: ReadableStream<Uint8Array> | null;
110
+ rawHeaders?: Record<string, string | string[] | undefined>;
111
+ }
112
+
113
+ /** Node transport exposes informational responses and leaves wire headers alone. */
114
+ async function requestDocument(url: URL, method: string, headers: Headers, body: NonNullable<CheerioRequestOptions['requestOptions']>['body'], options: NonNullable<CheerioRequestOptions['requestOptions']>, signal: AbortSignal): Promise<HTTPResponse> {
115
+ if (options.typeOfService != null && (!Number.isInteger(options.typeOfService) || options.typeOfService < 0 || options.typeOfService > 255)) {
116
+ throw Object.assign(new Error('typeOfService must be an integer between 0 and 255'), { name: 'InvalidArgumentError', code: 'UND_ERR_INVALID_ARG' });
117
+ }
118
+ let payload: string | Uint8Array | Readable | ReadableStream<Uint8Array> | null | undefined = body as string | Uint8Array | Readable | null | undefined;
119
+ if (body && typeof body === 'object' && !(body instanceof Uint8Array) && !(body instanceof Readable)) {
120
+ // Request supplies Node's built-in multipart encoder for either Node or
121
+ // Undici FormData without requiring the Undici package at runtime.
122
+ let form: FormData;
123
+ if (body instanceof FormData) form = body;
124
+ else {
125
+ form = new FormData();
126
+ for (const [name, value] of body as import('./types.js').FormDataLike) {
127
+ if (typeof value === 'string') form.append(name, value);
128
+ else if ('name' in value && typeof value.name === 'string') form.append(name, value as Blob, value.name);
129
+ else form.append(name, value as Blob);
130
+ }
131
+ }
132
+ const encoded = new Request(url, { method: 'POST', body: form });
133
+ // Node 20's encoder omits the final CRLF used by Undici 7. Keep the
134
+ // request streaming while normalizing that suffix across Node versions.
135
+ payload = Readable.from((async function* () {
136
+ let previous = -1;
137
+ let last = -1;
138
+ for await (const chunk of Readable.fromWeb(encoded.body! as import('node:stream/web').ReadableStream<Uint8Array>)) {
139
+ const bytes = chunk as Buffer;
140
+ if (bytes.length >= 2) previous = bytes[bytes.length - 2];
141
+ else if (bytes.length === 1) previous = last;
142
+ if (bytes.length) last = bytes[bytes.length - 1];
143
+ yield bytes;
144
+ }
145
+ if (previous !== 13 || last !== 10) yield Buffer.from('\r\n');
146
+ })());
147
+ if (!requestHeaders(options.headers).has('content-type')) headers.set('content-type', encoded.headers.get('content-type')!);
148
+ }
149
+ if (payload != null && !(payload instanceof Readable) && !(typeof payload === 'object' && 'getReader' in payload) && !headers.has('content-length') && !headers.has('transfer-encoding')) {
150
+ headers.set('content-length', String(typeof payload === 'string' ? Buffer.byteLength(payload) : payload.byteLength));
151
+ }
152
+ const declaredLength = headers.has('content-length') ? Number(headers.get('content-length')) : undefined;
153
+ const lengthError = () => Object.assign(new Error('Request body length does not match content-length header'), { name: 'RequestContentLengthMismatchError', code: 'UND_ERR_REQ_CONTENT_LENGTH_MISMATCH' });
154
+ const knownLength = typeof payload === 'string' ? Buffer.byteLength(payload) : payload instanceof Uint8Array ? payload.byteLength : undefined;
155
+ if (declaredLength != null && knownLength != null && declaredLength !== knownLength) throw lengthError();
156
+ if (options.reset === true && !headers.has('connection')) headers.set('connection', 'close');
157
+ return new Promise<HTTPResponse>((resolve, reject) => {
158
+ let informationalError: Error | undefined;
159
+ const request = (url.protocol === 'https:' ? requestHTTPS : requestHTTP)(url, {
160
+ method, headers: Object.fromEntries(headers), signal,
161
+ ...(typeof options.highWaterMark === 'number' ? { highWaterMark: options.highWaterMark } : {}),
162
+ }, response => {
163
+ if (informationalError) { response.destroy(); return; }
164
+ const rawHeaders: Record<string, string | string[]> = {};
165
+ for (let index = 0; index < response.rawHeaders.length; index += 2) {
166
+ const name = response.rawHeaders[index].toLowerCase();
167
+ const value = response.rawHeaders[index + 1];
168
+ if (!Object.hasOwn(rawHeaders, name)) Object.defineProperty(rawHeaders, name, { value, enumerable: true, writable: true, configurable: true });
169
+ else rawHeaders[name] = [...(Array.isArray(rawHeaders[name]) ? rawHeaders[name] : [rawHeaders[name]]) as string[], value];
170
+ }
171
+ const responseHeaders = new Headers();
172
+ for (const [name, value] of Object.entries(rawHeaders)) {
173
+ if (value == null) continue;
174
+ for (const item of Array.isArray(value) ? name === 'content-type' ? value.slice(0, 1) : value : [value]) responseHeaders.append(name, item);
175
+ }
176
+ const status = response.statusCode ?? 0;
177
+ resolve({ status, ok: status >= 200 && status < 300, headers: responseHeaders, rawHeaders, body: Readable.toWeb(response) as ReadableStream<Uint8Array> });
178
+ });
179
+ request.once('error', error => reject(signal.aborted ? signal.reason : error));
180
+ request.on('information', information => {
181
+ try { options.onInfo?.({ statusCode: information.statusCode, headers: information.headers as Record<string, string | string[]> }); }
182
+ catch (error) { informationalError = error as Error; reject(error); request.destroy(error as Error); }
183
+ });
184
+ request.once('socket', socket => {
185
+ const transport = socket as typeof socket & { setTypeOfService?: (value?: number) => void };
186
+ try {
187
+ if (transport.setTypeOfService && options.typeOfService != null) transport.setTypeOfService(options.typeOfService);
188
+ } catch (error) { request.destroy(error as Error); return; }
189
+ if (socket.connecting) {
190
+ const timer = setTimeout(() => request.destroy(Object.assign(new Error('Connect Timeout Error'), { name: 'ConnectTimeoutError', code: 'UND_ERR_CONNECT_TIMEOUT' })), 10000);
191
+ timer.unref();
192
+ socket.once(url.protocol === 'https:' ? 'secureConnect' : 'connect', () => clearTimeout(timer));
193
+ request.once('close', () => clearTimeout(timer));
194
+ }
195
+ });
196
+ if (payload instanceof Readable || payload && typeof payload === 'object' && 'getReader' in payload) {
197
+ const source = payload instanceof Readable ? payload : Readable.fromWeb(payload as unknown as import('node:stream/web').ReadableStream<Uint8Array>);
198
+ if (declaredLength != null) {
199
+ let sent = 0;
200
+ source.on('data', (chunk: Buffer | string) => {
201
+ sent += typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.byteLength;
202
+ if (sent > declaredLength) { request.destroy(lengthError()); source.destroy(); }
203
+ });
204
+ source.once('end', () => { if (sent !== declaredLength) request.destroy(lengthError()); });
205
+ }
206
+ source.once('error', error => request.destroy(error));
207
+ request.once('close', () => { if (!source.destroyed) source.destroy(); });
208
+ source.pipe(request);
209
+ } else request.end(payload);
210
+ });
211
+ }
212
+
213
+ /** A Node writable that feeds the native parser with bounded backpressure. */
214
+ export function stringStream(options: CheerioOptions, callback: LoadCallback): Writable {
215
+ const complete = completion(callback);
216
+ const opts = flattenOptions(options);
217
+ const xml = !!opts._useHtmlParser2;
218
+ const binding = requireNative();
219
+ let native: NativeStream | undefined;
220
+ let writeNative: (chunk: string) => Promise<void>;
221
+ let finishNative: () => Promise<CheerioAPI>;
222
+ let abortNative: () => void;
223
+ let carry = '';
224
+ if (opts._useHtmlParser2 && opts.Tokenizer) {
225
+ const session = createTokenizerSession(opts);
226
+ // Arbitrary user tokenizers can emit offsets for any earlier input. Retain
227
+ // their source until end rather than changing their callback contract.
228
+ const chunks: string[] = [];
229
+ writeNative = async (chunk) => { chunks.push(chunk); session.write(chunk); };
230
+ finishNative = async () => {
231
+ const document = new binding.NativeDocument(chunks.join(''), true, JSON.stringify(session.end()));
232
+ chunks.length = 0;
233
+ return createAPI(routeDocument(document, opts), opts);
234
+ };
235
+ abortNative = () => { chunks.length = 0; };
236
+ } else if (!opts._useHtmlParser2 && opts.treeAdapter) {
237
+ const session = createTreeAdapterSession(opts, true);
238
+ writeNative = (chunk) => session.write(chunk);
239
+ finishNative = async () => createAPI(await session.end(), opts);
240
+ abortNative = () => session.abort();
241
+ } else if (!opts._useHtmlParser2 && opts.onParseError) {
242
+ const session = createDiagnosticSession(opts, true);
243
+ writeNative = (chunk) => session.write(chunk);
244
+ finishNative = async () => createAPI(await session.end(), opts);
245
+ abortNative = () => session.abort();
246
+ } else {
247
+ if (!binding.NativeStream) throw new Error('This native build does not support incremental parsing');
248
+ native = new binding.NativeStream(JSON.stringify(parserOptions('', opts)), true);
249
+ writeNative = (chunk) => native!.write(chunk);
250
+ finishNative = async () => {
251
+ const document = await native!.finish();
252
+ deliverParseErrors(document, opts);
253
+ return createAPI(routeDocument(document, opts), opts);
254
+ };
255
+ abortNative = () => native!.abort();
256
+ }
257
+ const stream = new Writable({
258
+ decodeStrings: false,
259
+ write(chunk, _encoding, done) {
260
+ if (typeof chunk !== 'string') throw new TypeError(xml ? 'Expected a string' : 'Parser can work only with string streams.');
261
+ if (opts._useHtmlParser2 && opts.Tokenizer) { writeNative(chunk).then(() => done(), (error: Error) => done(error)); return; }
262
+ let text = carry + chunk;
263
+ carry = '';
264
+ const last = text.charCodeAt(text.length - 1);
265
+ if (last >= 0xd800 && last <= 0xdbff) { carry = text.slice(-1); text = text.slice(0, -1); }
266
+ if (!text) { done(); return; }
267
+ writeNative(text).then(() => done(), (error: Error) => done(error));
268
+ },
269
+ final(done) {
270
+ (async () => {
271
+ if (carry) await writeNative(carry);
272
+ return finishNative();
273
+ })().then((document) => {
274
+ complete(null, document);
275
+ done();
276
+ }, (error: Error) => done(error));
277
+ },
278
+ destroy(error, done) { abortNative(); done(error); },
279
+ });
280
+ stream.on('error', (error) => complete(error));
281
+ return stream;
282
+ }
283
+
284
+ /** Sniff a bounded prefix, then decode and parse each chunk as it arrives. */
285
+ export function decodeStream(options: DecodeStreamOptions, callback: LoadCallback): Writable {
286
+ const complete = completion(callback);
287
+ const { encoding = {}, ...parserOptions } = options ?? {};
288
+ const opts = flattenOptions(parserOptions);
289
+ const decoderOptions = { defaultEncoding: opts.xmlMode ? 'utf-8' : 'windows-1252', ...encoding };
290
+ const maxBytes = decoderOptions.maxBytes ?? 1024;
291
+ if (!Number.isSafeInteger(maxBytes) || maxBytes < 0) throw new RangeError('encoding.maxBytes must be a non-negative integer');
292
+ const prefixLimit = Math.max(6, maxBytes);
293
+ const prefix: Buffer[] = [];
294
+ let prefixLength = 0;
295
+ let decoder: TextDecoder | undefined;
296
+ let userDefined = false;
297
+ let started = false;
298
+ const parser = stringStream(opts, (error, document) => complete(error, document));
299
+ let downstreamError: Error | undefined;
300
+ parser.on('error', (error) => { downstreamError = error; stream.destroy(error); });
301
+ const writeText = (text: string): Promise<void> => new Promise((resolve, reject) => {
302
+ if (downstreamError) { reject(downstreamError); return; }
303
+ if (!text) { resolve(); return; }
304
+ parser.write(text, (error?: Error | null) => error ? reject(error) : resolve());
305
+ });
306
+ const decode = (bytes: Uint8Array, final = false): string => {
307
+ if (userDefined) {
308
+ let text = '';
309
+ for (const byte of bytes) text += String.fromCharCode(byte < 0x80 ? byte : 0xf700 + byte);
310
+ return text;
311
+ }
312
+ return decoder!.decode(bytes, { stream: !final });
313
+ };
314
+ const start = async (): Promise<void> => {
315
+ const bytes = Buffer.concat(prefix, prefixLength);
316
+ const label = sniffEncoding(bytes, decoderOptions);
317
+ userDefined = label === 'x-user-defined';
318
+ if (!userDefined) decoder = new TextDecoder(label);
319
+ started = true;
320
+ prefix.length = 0;
321
+ await writeText(decode(bytes));
322
+ };
323
+ const accept = async (bytes: Buffer): Promise<void> => {
324
+ if (!started) {
325
+ const count = Math.min(prefixLimit - prefixLength, bytes.length);
326
+ if (count) { prefix.push(Buffer.from(bytes.subarray(0, count))); prefixLength += count; }
327
+ bytes = bytes.subarray(count);
328
+ const declaredEncoding = encodingLabel(decoderOptions.userEncoding) || encodingLabel(decoderOptions.transportLayerEncodingLabel);
329
+ if (prefixLength >= prefixLimit || declaredEncoding && prefixLength >= 3) await start();
330
+ }
331
+ if (started && bytes.length) await writeText(decode(bytes));
332
+ };
333
+ const stream = new Writable({
334
+ write(chunk: Buffer | string, encoding, done) {
335
+ accept(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk, encoding)).then(() => done(), (error: Error) => done(error));
336
+ },
337
+ final(done) {
338
+ (async () => {
339
+ if (!started) await start();
340
+ await writeText(decode(new Uint8Array(), true));
341
+ await new Promise<void>((resolve, reject) => {
342
+ parser.once('error', reject);
343
+ parser.end(() => { parser.removeListener('error', reject); resolve(); });
344
+ });
345
+ })().then(() => done(), (error: Error) => done(error));
346
+ },
347
+ destroy(error, done) { parser.destroy(error ?? undefined); done(error); },
348
+ });
349
+ stream.on('error', (error) => complete(error));
350
+ return stream;
351
+ }
352
+
353
+ /** Load HTML or XML with bounded redirects through Node's built-in transport. */
354
+ export async function fromURL(url: string | URL, options: CheerioRequestOptions = {}): Promise<CheerioAPI> {
355
+ const signal = options.requestOptions?.signal;
356
+ if (!signal) return loadURL(url, options);
357
+ if (typeof signal !== 'object' && typeof signal !== 'function') throw new TypeError('signal must be an AbortSignal or EventEmitter');
358
+ if ('addEventListener' in signal) return loadURL(url, options);
359
+ if (!('once' in signal) || !('removeListener' in signal)) throw new TypeError('signal must be an AbortSignal or EventEmitter');
360
+ const emitter = signal as EventEmitter;
361
+ const controller = new AbortController();
362
+ const abort = () => controller.abort();
363
+ emitter.once('abort', abort);
364
+ try { return await loadURL(url, { ...options, requestOptions: { ...options.requestOptions, signal: controller.signal } }); }
365
+ finally { emitter.removeListener('abort', abort); }
366
+ }
367
+
368
+ async function loadURL(url: string | URL, options: CheerioRequestOptions): Promise<CheerioAPI> {
369
+ const { requestOptions = {}, encoding, ...parserOptions } = options;
370
+ const maxRedirects = requestOptions.maxRedirections ?? 5;
371
+ if (!Number.isInteger(maxRedirects) || maxRedirects < 0) throw new RangeError('maxRedirections must be a non-negative integer');
372
+ const headers = requestHeaders(requestOptions.headers);
373
+ if (!Object.hasOwn(requestOptions, 'headers')) headers.set('accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8');
374
+ let current = new URL(url);
375
+ if (requestOptions.query) {
376
+ if (current.search || current.hash) throw new TypeError('URL must not contain a query or fragment when requestOptions.query is supplied');
377
+ current.search = stringifyQuery(requestOptions.query);
378
+ }
379
+ let method = requestOptions.method ?? 'GET';
380
+ let body = requestOptions.body;
381
+ let response: HTTPResponse;
382
+ const controller = new AbortController();
383
+ const signal = requestOptions.signal ? AbortSignal.any([requestOptions.signal as AbortSignal, controller.signal]) : controller.signal;
384
+ const timeoutError = (kind: 'Headers' | 'Body') => Object.assign(new Error(`${kind} Timeout Error`), {
385
+ name: `${kind}TimeoutError`, code: kind === 'Headers' ? 'UND_ERR_HEADERS_TIMEOUT' : 'UND_ERR_BODY_TIMEOUT',
386
+ });
387
+ for (const timeout of [requestOptions.headersTimeout, requestOptions.bodyTimeout]) {
388
+ if (timeout != null && (!Number.isFinite(timeout) || timeout < 0)) throw new RangeError('HTTP timeouts must be non-negative finite numbers');
389
+ }
390
+ const suppliedDispatcher = requestOptions.dispatcher as { dispatch?: (options: unknown, handler: unknown) => boolean } | undefined;
391
+ if (suppliedDispatcher && typeof suppliedDispatcher.dispatch !== 'function') throw new TypeError('requestOptions.dispatcher must implement dispatch');
392
+ const dispatcher = suppliedDispatcher && {
393
+ dispatch(dispatchOptions: Record<string, unknown>, handler: unknown) {
394
+ // Keep fetch's URL/body/header encoding and forward the remaining Undici
395
+ // transport options to the caller's dispatcher without an npm dependency.
396
+ const { dispatcher: _dispatcher, throwOnError: _throwOnError, maxRedirections: _maxRedirections, query: _query, ...transport } = requestOptions;
397
+ return suppliedDispatcher.dispatch!({ ...transport, ...dispatchOptions }, handler);
398
+ },
399
+ };
400
+ for (let redirects = 0; ; redirects += 1) {
401
+ let timer: ReturnType<typeof setTimeout> | undefined;
402
+ const headersTimeout = requestOptions.headersTimeout ?? 300000;
403
+ if (headersTimeout) { timer = setTimeout(() => controller.abort(timeoutError('Headers')), headersTimeout); timer.unref(); }
404
+ try {
405
+ if (dispatcher) {
406
+ const init: RequestInit & { dispatcher?: unknown; duplex?: 'half' } = { method, headers, body: body as BodyInit | undefined, redirect: 'manual', signal, dispatcher };
407
+ if (body != null && typeof body !== 'string') init.duplex = 'half';
408
+ response = await fetch(current, init);
409
+ } else response = await requestDocument(current, method, headers, body, requestOptions, signal);
410
+ } catch (error) {
411
+ throw signal.aborted ? signal.reason : error;
412
+ } finally { if (timer) clearTimeout(timer); }
413
+ const location = response.headers.get('location');
414
+ if (![300, 301, 302, 303, 307, 308].includes(response.status) || !location) break;
415
+ const rewriteToGet = response.status === 303 && method.toUpperCase() !== 'HEAD' || (response.status === 301 || response.status === 302) && method.toUpperCase() === 'POST';
416
+ if (body instanceof Readable && body.readableDidRead && !rewriteToGet) break;
417
+ await response.body?.cancel();
418
+ if (redirects >= maxRedirects) throw new Error('max redirects reached');
419
+ const next = new URL(location, current);
420
+ headers.delete('host');
421
+ if (response.status === 303) for (const name of [...headers.keys()]) if (name.startsWith('content-')) headers.delete(name);
422
+ if (next.origin !== current.origin) {
423
+ headers.delete('authorization');
424
+ headers.delete('cookie');
425
+ headers.delete('proxy-authorization');
426
+ }
427
+ if (rewriteToGet) {
428
+ method = 'GET';
429
+ body = undefined;
430
+ headers.delete('content-length');
431
+ headers.delete('content-type');
432
+ }
433
+ current = next;
434
+ }
435
+ if (!response.ok) {
436
+ await response.body?.cancel();
437
+ const error = new Error('Response Error') as Error & { statusCode: number; code: string; headers: Record<string, string | string[] | undefined> };
438
+ error.name = 'ResponseError';
439
+ error.statusCode = response.status;
440
+ error.code = 'UND_ERR_RESPONSE';
441
+ error.headers = response.rawHeaders ?? Object.fromEntries(response.headers);
442
+ throw error;
443
+ }
444
+ const contentType = response.headers.get('content-type') ?? 'text/html';
445
+ let parsedType: MIMEType;
446
+ try { parsedType = new MIMEType(contentType); }
447
+ catch {
448
+ await response.body?.cancel();
449
+ throw new Error(`Could not parse MIME type string "${contentType}"`);
450
+ }
451
+ const mime = parsedType.essence;
452
+ const xml = (parsedType.type === 'application' || parsedType.type === 'text') && parsedType.subtype === 'xml' || parsedType.subtype.endsWith('+xml');
453
+ if (mime !== 'text/html' && !xml) {
454
+ await response.body?.cancel();
455
+ throw new RangeError(`The content-type "${mime}" is neither HTML nor XML.`);
456
+ }
457
+ const charset = parsedType.params.get('charset') ?? undefined;
458
+ if (encoding) encoding.transportLayerEncodingLabel = charset;
459
+ const streamOptions: DecodeStreamOptions = {
460
+ xmlMode: xml,
461
+ baseURI: current,
462
+ ...parserOptions,
463
+ encoding: { ...encoding, transportLayerEncodingLabel: charset },
464
+ };
465
+ return new Promise<CheerioAPI>((resolve, reject) => {
466
+ const parser = decodeStream(streamOptions, (error, document) => error ? reject(error) : resolve(document));
467
+ const reader = response.body?.getReader();
468
+ parser.once('error', (error) => { controller.abort(error); void reader?.cancel(error).catch(() => {}); });
469
+ (async () => {
470
+ let received = 0;
471
+ const maxResponseSize = requestOptions.maxResponseSize;
472
+ if (maxResponseSize != null && (typeof maxResponseSize !== 'number' || !Number.isSafeInteger(maxResponseSize) || maxResponseSize < -1)) throw new RangeError('maxResponseSize must be -1 or a non-negative integer');
473
+ while (reader) {
474
+ let timer: ReturnType<typeof setTimeout> | undefined;
475
+ const bodyTimeout = requestOptions.bodyTimeout ?? 300000;
476
+ if (bodyTimeout) { timer = setTimeout(() => controller.abort(timeoutError('Body')), bodyTimeout); timer.unref(); }
477
+ let chunk: ReadableStreamReadResult<Uint8Array>;
478
+ try { chunk = await reader.read(); }
479
+ catch (error) { throw controller.signal.aborted ? controller.signal.reason : error; }
480
+ finally { if (timer) clearTimeout(timer); }
481
+ if (chunk.done) break;
482
+ received += chunk.value.byteLength;
483
+ if (typeof maxResponseSize === 'number' && maxResponseSize >= 0 && received > maxResponseSize) {
484
+ throw Object.assign(new Error('Response content exceeded maxResponseSize'), { name: 'ResponseExceededMaxSizeError', code: 'UND_ERR_RES_EXCEEDED_MAX_SIZE' });
485
+ }
486
+ await new Promise<void>((resolve, reject) => parser.write(Buffer.from(chunk.value.buffer, chunk.value.byteOffset, chunk.value.byteLength), (error?: Error | null) => error ? reject(error) : resolve()));
487
+ }
488
+ parser.end();
489
+ })().catch((error: Error) => { parser.destroy(error); reject(error); });
490
+ });
491
+ }
package/src/native.ts ADDED
@@ -0,0 +1,135 @@
1
+ import { createRequire } from 'node:module';
2
+ import { existsSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { wrapNativeBinding } from './transport.js';
6
+
7
+ export interface NativeNodeRecord {
8
+ id: number;
9
+ type: 'root' | 'tag' | 'script' | 'style' | 'text' | 'comment' | 'directive' | 'cdata';
10
+ name?: string;
11
+ data?: string;
12
+ attribs?: Record<string, string>;
13
+ children: number[];
14
+ parent?: number;
15
+ namespace?: string;
16
+ prefix?: string;
17
+ attribsNamespace?: Record<string, string>;
18
+ attribsPrefix?: Record<string, string>;
19
+ doctypeName?: string;
20
+ publicId?: string;
21
+ systemId?: string;
22
+ 'x-name'?: string;
23
+ 'x-publicId'?: string;
24
+ 'x-systemId'?: string;
25
+ 'x-mode'?: 'quirks' | 'limited-quirks' | 'no-quirks';
26
+ [key: string]: any;
27
+ }
28
+
29
+ export interface NativeDocument {
30
+ readonly nodeSpace?: object;
31
+ retainNode?(id: number): void;
32
+ retainNodeData?(id: number, data: object): void;
33
+ adopt?(source: NativeDocument, ids: number[], detach?: boolean): string;
34
+ op(command: string, argsJson: string): string;
35
+ create?(html: string, contextId?: number, optionsJson?: string): number[];
36
+ select(selector: string, contexts?: number[]): number[];
37
+ text(ids: number[]): string;
38
+ innerHtml(id: number): string;
39
+ outerHtml(ids: number[]): string;
40
+ attr(id: number, name: string): string | undefined | null;
41
+ tagName(id: number): string | undefined | null;
42
+ exportTree(): string;
43
+ rootId(): number;
44
+ parserDiagnostics?(): string;
45
+ }
46
+
47
+ export interface NativeStream {
48
+ write(chunk: string): Promise<void>;
49
+ finish(): Promise<NativeDocument>;
50
+ abort(): void;
51
+ }
52
+
53
+ export interface NativeWorkerPoolInfo {
54
+ threads: number;
55
+ activeBatches: number;
56
+ activeWorkers: number;
57
+ peakActiveWorkers: number;
58
+ }
59
+
60
+ export interface NativeQuery {
61
+ key: string;
62
+ selector: string;
63
+ attr?: string;
64
+ all?: boolean;
65
+ }
66
+
67
+ export interface NativeBinding {
68
+ NativeDocument: new (html: string, isDocument?: boolean, optionsJson?: string) => NativeDocument;
69
+ NativeStream?: new (optionsJson: string, isDocument?: boolean, errorBridge?: (request: string) => string) => NativeStream;
70
+ configureWorkerPool?: (threads: number) => void;
71
+ workerPoolInfo?: () => NativeWorkerPoolInfo;
72
+ extractPlanManyAsync?: (html: string[], planJson: string, threads: number, optionsJson?: string) => Promise<string>;
73
+ parseAsync?: (html: string, isDocument?: boolean, optionsJson?: string) => Promise<NativeDocument>;
74
+ parseManyAsync?: (
75
+ html: string[],
76
+ isDocument: boolean,
77
+ threads: number,
78
+ optionsJson?: string,
79
+ ) => Promise<NativeDocument[]>;
80
+ extractManyAsync?: (
81
+ html: string[],
82
+ queries: NativeQuery[],
83
+ threads: number,
84
+ isDocument?: boolean,
85
+ optionsJson?: string,
86
+ ) => Promise<string>;
87
+ }
88
+
89
+ let cached: NativeBinding | undefined;
90
+ let attempted = false;
91
+ let bindingError: unknown;
92
+
93
+ /** Resolve the native addon bundled with the npm package. */
94
+ export function nativeBinding(): NativeBinding | undefined {
95
+ if (attempted) return cached;
96
+ attempted = true;
97
+
98
+ // This module is bundled into both CommonJS and ESM entry points. A synthetic
99
+ // absolute filename lets createRequire work in both module formats.
100
+ const moduleDirectory = typeof __dirname === 'string' ? __dirname : dirname(fileURLToPath(import.meta.url));
101
+ const require = createRequire(join(moduleDirectory, 'rusteerio-loader.cjs'));
102
+ const directories = [moduleDirectory, dirname(moduleDirectory)];
103
+ const libc = process.platform === 'linux'
104
+ ? ((process.report?.getReport() as { header?: { glibcVersionRuntime?: string } })?.header?.glibcVersionRuntime ? 'gnu' : 'musl')
105
+ : undefined;
106
+ const platform = `${process.platform}-${process.arch}${libc ? `-${libc}` : process.platform === 'win32' ? '-msvc' : ''}`;
107
+ const names = [`rusteerio.${platform}.node`, 'rusteerio.node'];
108
+ const candidates = [...new Set(directories.flatMap((directory) => names.map((name) => join(directory, name))))];
109
+
110
+ for (const candidate of candidates) {
111
+ if (!existsSync(candidate)) continue;
112
+ try {
113
+ const loaded: NativeBinding = require(candidate);
114
+ if (typeof loaded.NativeDocument === 'function') {
115
+ cached = wrapNativeBinding(loaded);
116
+ return cached;
117
+ }
118
+ } catch (error) {
119
+ bindingError ??= error;
120
+ }
121
+ }
122
+ return undefined;
123
+ }
124
+
125
+ export function requireNative(): NativeBinding {
126
+ const binding = nativeBinding();
127
+ if (binding) return binding;
128
+ throw new Error('Rusteerio could not load its Rust native binding. Reinstall rusteerio for this platform or run "npm run build:native" from a source checkout.', { cause: bindingError });
129
+ }
130
+
131
+ /** Useful for installation diagnostics and benchmarks that require native code. */
132
+ export function nativeStatus(): { available: boolean; error?: string } {
133
+ const available = !!nativeBinding();
134
+ return available ? { available } : { available, error: String(bindingError ?? 'Native binding not found') };
135
+ }