@oh-my-pi/pi-natives 8.12.4
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/README.md +63 -0
- package/package.json +41 -0
- package/src/grep/filters.ts +77 -0
- package/src/grep/index.ts +314 -0
- package/src/grep/types.ts +82 -0
- package/src/grep/worker.ts +205 -0
- package/src/image/index.ts +124 -0
- package/src/image/types.ts +52 -0
- package/src/image/worker.ts +152 -0
- package/src/index.ts +140 -0
- package/src/pool.ts +264 -0
- package/src/wasix.ts +1745 -0
- package/wasm/pi_natives.d.ts +88 -0
- package/wasm/pi_natives.js +656 -0
- package/wasm/pi_natives_bg.wasm +0 -0
- package/wasm/pi_natives_bg.wasm.d.ts +22 -0
package/README.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# @oh-my-pi/pi-natives
|
|
2
|
+
|
|
3
|
+
Native Rust functionality compiled to WebAssembly via wasm-bindgen.
|
|
4
|
+
|
|
5
|
+
## What's Inside
|
|
6
|
+
|
|
7
|
+
- **Grep**: Regex-based search powered by ripgrep's engine (WASM handles matching, JS handles I/O + gitignore-aware file walking)
|
|
8
|
+
- **Find**: Glob-based file/directory discovery with gitignore support (pure TypeScript via `globPaths`)
|
|
9
|
+
- **Image**: Image processing via photon-rs (resize, format conversion)
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { grep, find, PhotonImage, resize, SamplingFilter } from "@oh-my-pi/pi-natives";
|
|
15
|
+
|
|
16
|
+
// Grep for a pattern
|
|
17
|
+
const results = await grep({
|
|
18
|
+
pattern: "TODO",
|
|
19
|
+
path: "/path/to/project",
|
|
20
|
+
glob: "*.ts",
|
|
21
|
+
context: 2,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Find files
|
|
25
|
+
const files = await find({
|
|
26
|
+
pattern: "*.rs",
|
|
27
|
+
path: "/path/to/project",
|
|
28
|
+
fileType: "file",
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Image processing
|
|
32
|
+
using image = await PhotonImage.new_from_byteslice(bytes);
|
|
33
|
+
using resized = await resize(image, 800, 600, SamplingFilter.Lanczos3);
|
|
34
|
+
const pngBytes = await resized.get_bytes();
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Building
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Build WASM from workspace root (requires Rust + wasm-pack)
|
|
41
|
+
bun run build:wasm
|
|
42
|
+
|
|
43
|
+
# Type check
|
|
44
|
+
bun run check
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Architecture
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
crates/pi-natives/ # Rust source (workspace member)
|
|
51
|
+
src/lib.rs # Grep/search + wasm-bindgen bindings
|
|
52
|
+
src/image.rs # Image processing (photon-rs)
|
|
53
|
+
Cargo.toml # Rust dependencies
|
|
54
|
+
wasm/ # Generated WASM output
|
|
55
|
+
pi_natives.wasm # Compiled WASM module
|
|
56
|
+
pi_natives.js # wasm-bindgen generated JS glue
|
|
57
|
+
pi_natives.d.ts # TypeScript definitions
|
|
58
|
+
src/ # TypeScript wrappers
|
|
59
|
+
index.ts # Public API
|
|
60
|
+
grep/ # Grep with worker pool
|
|
61
|
+
image/ # Async image processing via worker
|
|
62
|
+
pool.ts # Generic worker pool infrastructure
|
|
63
|
+
```
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oh-my-pi/pi-natives",
|
|
3
|
+
"version": "8.12.4",
|
|
4
|
+
"description": "Native Rust functionality compiled to WebAssembly via wasm-bindgen",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"import": "./src/index.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src",
|
|
16
|
+
"wasm"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build:wasm": "bun scripts/build-wasm.ts",
|
|
20
|
+
"check": "biome check . && tsgo -p tsconfig.json",
|
|
21
|
+
"fix": "biome check --write --unsafe .",
|
|
22
|
+
"test": "bun test",
|
|
23
|
+
"bench": "bun bench/grep.ts"
|
|
24
|
+
},
|
|
25
|
+
"author": "Can Bölük",
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/can1357/oh-my-pi.git",
|
|
30
|
+
"directory": "packages/natives"
|
|
31
|
+
},
|
|
32
|
+
"dependencies": {
|
|
33
|
+
"@oh-my-pi/pi-utils": "8.12.4"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^25.0.10"
|
|
37
|
+
},
|
|
38
|
+
"engines": {
|
|
39
|
+
"bun": ">=1.3.7"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
|
|
3
|
+
export interface TypeFilter {
|
|
4
|
+
extensions?: string[];
|
|
5
|
+
names?: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const TYPE_ALIASES: Record<string, TypeFilter> = {
|
|
9
|
+
js: { extensions: ["js", "jsx", "mjs", "cjs"] },
|
|
10
|
+
javascript: { extensions: ["js", "jsx", "mjs", "cjs"] },
|
|
11
|
+
ts: { extensions: ["ts", "tsx", "mts", "cts"] },
|
|
12
|
+
typescript: { extensions: ["ts", "tsx", "mts", "cts"] },
|
|
13
|
+
json: { extensions: ["json", "jsonc", "json5"] },
|
|
14
|
+
yaml: { extensions: ["yaml", "yml"] },
|
|
15
|
+
yml: { extensions: ["yaml", "yml"] },
|
|
16
|
+
toml: { extensions: ["toml"] },
|
|
17
|
+
md: { extensions: ["md", "markdown", "mdx"] },
|
|
18
|
+
markdown: { extensions: ["md", "markdown", "mdx"] },
|
|
19
|
+
py: { extensions: ["py", "pyi"] },
|
|
20
|
+
python: { extensions: ["py", "pyi"] },
|
|
21
|
+
rs: { extensions: ["rs"] },
|
|
22
|
+
rust: { extensions: ["rs"] },
|
|
23
|
+
go: { extensions: ["go"] },
|
|
24
|
+
java: { extensions: ["java"] },
|
|
25
|
+
kt: { extensions: ["kt", "kts"] },
|
|
26
|
+
kotlin: { extensions: ["kt", "kts"] },
|
|
27
|
+
c: { extensions: ["c", "h"] },
|
|
28
|
+
cpp: { extensions: ["cpp", "cc", "cxx", "hpp", "hxx", "hh"] },
|
|
29
|
+
cxx: { extensions: ["cpp", "cc", "cxx", "hpp", "hxx", "hh"] },
|
|
30
|
+
cs: { extensions: ["cs", "csx"] },
|
|
31
|
+
csharp: { extensions: ["cs", "csx"] },
|
|
32
|
+
php: { extensions: ["php", "phtml"] },
|
|
33
|
+
rb: { extensions: ["rb", "rake", "gemspec"] },
|
|
34
|
+
ruby: { extensions: ["rb", "rake", "gemspec"] },
|
|
35
|
+
sh: { extensions: ["sh", "bash", "zsh", "fish"] },
|
|
36
|
+
bash: { extensions: ["sh", "bash", "zsh"] },
|
|
37
|
+
zsh: { extensions: ["zsh"] },
|
|
38
|
+
fish: { extensions: ["fish"] },
|
|
39
|
+
html: { extensions: ["html", "htm"] },
|
|
40
|
+
css: { extensions: ["css"] },
|
|
41
|
+
scss: { extensions: ["scss"] },
|
|
42
|
+
sass: { extensions: ["sass"] },
|
|
43
|
+
less: { extensions: ["less"] },
|
|
44
|
+
xml: { extensions: ["xml"] },
|
|
45
|
+
docker: { names: ["dockerfile"] },
|
|
46
|
+
dockerfile: { names: ["dockerfile"] },
|
|
47
|
+
make: { names: ["makefile"] },
|
|
48
|
+
makefile: { names: ["makefile"] },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function buildGlobPattern(glob?: string): string {
|
|
52
|
+
const trimmed = glob?.trim();
|
|
53
|
+
if (!trimmed) return "**/*";
|
|
54
|
+
const normalized = trimmed.replace(/\\/g, "/");
|
|
55
|
+
if (normalized.includes("/") || normalized.startsWith("**/")) return normalized;
|
|
56
|
+
return `**/${normalized}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function resolveTypeFilter(type?: string): TypeFilter | undefined {
|
|
60
|
+
if (!type) return undefined;
|
|
61
|
+
const trimmed = type.trim();
|
|
62
|
+
if (!trimmed) return undefined;
|
|
63
|
+
const normalized = trimmed.toLowerCase();
|
|
64
|
+
const withoutDot = normalized.startsWith(".") ? normalized.slice(1) : normalized;
|
|
65
|
+
return TYPE_ALIASES[withoutDot] ?? { extensions: [withoutDot] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function matchesTypeFilter(filePath: string, filter?: TypeFilter): boolean {
|
|
69
|
+
if (!filter) return true;
|
|
70
|
+
const baseName = path.basename(filePath).toLowerCase();
|
|
71
|
+
if (filter.names?.some(name => name.toLowerCase() === baseName)) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
const ext = path.extname(baseName).slice(1).toLowerCase();
|
|
75
|
+
if (!ext) return false;
|
|
76
|
+
return filter.extensions?.includes(ext) ?? false;
|
|
77
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native ripgrep wrapper using wasm-bindgen.
|
|
3
|
+
*
|
|
4
|
+
* JS handles filesystem operations (directory walking, file reading).
|
|
5
|
+
* WASM handles pure regex matching using ripgrep's engine.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as fs from "node:fs/promises";
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import { globPaths } from "@oh-my-pi/pi-utils";
|
|
11
|
+
import {
|
|
12
|
+
CompiledPattern as WasmCompiledPattern,
|
|
13
|
+
has_match as wasmHasMatch,
|
|
14
|
+
search as wasmSearch,
|
|
15
|
+
} from "../../wasm/pi_natives";
|
|
16
|
+
import { WorkerPool } from "../pool";
|
|
17
|
+
import { buildGlobPattern, matchesTypeFilter, resolveTypeFilter } from "./filters";
|
|
18
|
+
import type {
|
|
19
|
+
ContextLine,
|
|
20
|
+
GrepMatch,
|
|
21
|
+
GrepOptions,
|
|
22
|
+
GrepResult,
|
|
23
|
+
GrepSummary,
|
|
24
|
+
WasmSearchResult,
|
|
25
|
+
WorkerRequest,
|
|
26
|
+
WorkerResponse,
|
|
27
|
+
} from "./types";
|
|
28
|
+
|
|
29
|
+
export type { ContextLine, GrepMatch, GrepOptions, GrepResult, GrepSummary };
|
|
30
|
+
|
|
31
|
+
// =============================================================================
|
|
32
|
+
// File Walking
|
|
33
|
+
// =============================================================================
|
|
34
|
+
|
|
35
|
+
function filterUndefined<T extends Record<string, unknown>>(obj: T): T {
|
|
36
|
+
const result = {} as T;
|
|
37
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
38
|
+
if (value !== undefined) {
|
|
39
|
+
(result as Record<string, unknown>)[key] = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// =============================================================================
|
|
46
|
+
// Grep Implementation
|
|
47
|
+
// =============================================================================
|
|
48
|
+
|
|
49
|
+
const GREP_WORKERS = (() => {
|
|
50
|
+
const val = process.env.OMP_GREP_WORKERS;
|
|
51
|
+
if (val === undefined) return true;
|
|
52
|
+
const n = Number.parseInt(val, 10);
|
|
53
|
+
return Number.isNaN(n) || n > 0;
|
|
54
|
+
})();
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Search files for a regex pattern (direct, single-threaded).
|
|
58
|
+
*/
|
|
59
|
+
async function grepDirect(options: GrepOptions, onMatch?: (match: GrepMatch) => void): Promise<GrepResult> {
|
|
60
|
+
const searchPath = path.resolve(options.path);
|
|
61
|
+
const outputMode = options.mode ?? "content";
|
|
62
|
+
const wasmMode = outputMode === "content" ? "content" : "count";
|
|
63
|
+
|
|
64
|
+
const stat = await fs.stat(searchPath);
|
|
65
|
+
const isFile = stat.isFile();
|
|
66
|
+
|
|
67
|
+
using compiledPattern = new WasmCompiledPattern(
|
|
68
|
+
filterUndefined({
|
|
69
|
+
pattern: options.pattern,
|
|
70
|
+
ignoreCase: options.ignoreCase,
|
|
71
|
+
multiline: options.multiline,
|
|
72
|
+
context: options.context,
|
|
73
|
+
maxColumns: options.maxColumns,
|
|
74
|
+
mode: wasmMode,
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
const typeFilter = resolveTypeFilter(options.type);
|
|
79
|
+
const globPattern = buildGlobPattern(options.glob);
|
|
80
|
+
|
|
81
|
+
const matches: GrepMatch[] = [];
|
|
82
|
+
let totalMatches = 0;
|
|
83
|
+
let filesWithMatches = 0;
|
|
84
|
+
let filesSearched = 0;
|
|
85
|
+
let limitReached = false;
|
|
86
|
+
const maxCount = options.maxCount;
|
|
87
|
+
const globalOffset = options.offset ?? 0;
|
|
88
|
+
|
|
89
|
+
if (isFile) {
|
|
90
|
+
if (typeFilter && !matchesTypeFilter(searchPath, typeFilter)) {
|
|
91
|
+
return {
|
|
92
|
+
matches,
|
|
93
|
+
totalMatches,
|
|
94
|
+
filesWithMatches,
|
|
95
|
+
filesSearched,
|
|
96
|
+
limitReached: limitReached || undefined,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const content = Bun.mmap(searchPath);
|
|
101
|
+
filesSearched = 1;
|
|
102
|
+
|
|
103
|
+
const result = compiledPattern.search_bytes(
|
|
104
|
+
content,
|
|
105
|
+
maxCount,
|
|
106
|
+
globalOffset > 0 ? globalOffset : undefined,
|
|
107
|
+
) as WasmSearchResult;
|
|
108
|
+
|
|
109
|
+
if (result.error) {
|
|
110
|
+
throw new Error(result.error);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (result.matchCount > 0) {
|
|
114
|
+
filesWithMatches = 1;
|
|
115
|
+
totalMatches = result.matchCount;
|
|
116
|
+
|
|
117
|
+
if (outputMode === "content") {
|
|
118
|
+
for (const m of result.matches) {
|
|
119
|
+
const match: GrepMatch = {
|
|
120
|
+
path: searchPath,
|
|
121
|
+
lineNumber: m.lineNumber,
|
|
122
|
+
line: m.line,
|
|
123
|
+
contextBefore: m.contextBefore?.length ? m.contextBefore : undefined,
|
|
124
|
+
contextAfter: m.contextAfter?.length ? m.contextAfter : undefined,
|
|
125
|
+
truncated: m.truncated || undefined,
|
|
126
|
+
};
|
|
127
|
+
matches.push(match);
|
|
128
|
+
onMatch?.(match);
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
const match: GrepMatch = {
|
|
132
|
+
path: searchPath,
|
|
133
|
+
lineNumber: 0,
|
|
134
|
+
line: "",
|
|
135
|
+
matchCount: result.matchCount,
|
|
136
|
+
};
|
|
137
|
+
matches.push(match);
|
|
138
|
+
onMatch?.(match);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
limitReached = result.limitReached || (maxCount !== undefined && totalMatches >= maxCount);
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
const paths = await globPaths(globPattern, {
|
|
145
|
+
cwd: searchPath,
|
|
146
|
+
dot: options.hidden ?? true,
|
|
147
|
+
onlyFiles: true,
|
|
148
|
+
gitignore: true,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
for (const relativePath of paths) {
|
|
152
|
+
if (limitReached) break;
|
|
153
|
+
if (typeFilter && !matchesTypeFilter(relativePath, typeFilter)) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const normalizedPath = relativePath.replace(/\\/g, "/");
|
|
158
|
+
const fullPath = path.join(searchPath, normalizedPath);
|
|
159
|
+
|
|
160
|
+
let content: Uint8Array;
|
|
161
|
+
try {
|
|
162
|
+
content = Bun.mmap(fullPath);
|
|
163
|
+
} catch {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
filesSearched++;
|
|
168
|
+
|
|
169
|
+
if (!compiledPattern.has_match_bytes(content)) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const fileOffset = globalOffset > 0 ? Math.max(globalOffset - totalMatches, 0) : 0;
|
|
174
|
+
const remaining = maxCount !== undefined ? Math.max(maxCount - totalMatches, 0) : undefined;
|
|
175
|
+
if (remaining === 0) {
|
|
176
|
+
limitReached = true;
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
const result = compiledPattern.search_bytes(
|
|
180
|
+
content,
|
|
181
|
+
remaining,
|
|
182
|
+
fileOffset > 0 ? fileOffset : undefined,
|
|
183
|
+
) as WasmSearchResult;
|
|
184
|
+
|
|
185
|
+
if (result.error) {
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (result.matchCount > 0) {
|
|
190
|
+
filesWithMatches++;
|
|
191
|
+
totalMatches += result.matchCount;
|
|
192
|
+
|
|
193
|
+
if (outputMode === "content") {
|
|
194
|
+
for (const m of result.matches) {
|
|
195
|
+
const match: GrepMatch = {
|
|
196
|
+
path: normalizedPath,
|
|
197
|
+
lineNumber: m.lineNumber,
|
|
198
|
+
line: m.line,
|
|
199
|
+
contextBefore: m.contextBefore?.length ? m.contextBefore : undefined,
|
|
200
|
+
contextAfter: m.contextAfter?.length ? m.contextAfter : undefined,
|
|
201
|
+
truncated: m.truncated || undefined,
|
|
202
|
+
};
|
|
203
|
+
matches.push(match);
|
|
204
|
+
onMatch?.(match);
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
const match: GrepMatch = {
|
|
208
|
+
path: normalizedPath,
|
|
209
|
+
lineNumber: 0,
|
|
210
|
+
line: "",
|
|
211
|
+
matchCount: result.matchCount,
|
|
212
|
+
};
|
|
213
|
+
matches.push(match);
|
|
214
|
+
onMatch?.(match);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (result.limitReached || (maxCount !== undefined && totalMatches >= maxCount)) {
|
|
218
|
+
limitReached = true;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
matches,
|
|
226
|
+
totalMatches,
|
|
227
|
+
filesWithMatches,
|
|
228
|
+
filesSearched,
|
|
229
|
+
limitReached: limitReached || undefined,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// =============================================================================
|
|
234
|
+
// Content Search (lower-level API)
|
|
235
|
+
// =============================================================================
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Search a single file's content for a pattern.
|
|
239
|
+
* Lower-level API for when you already have file content.
|
|
240
|
+
*/
|
|
241
|
+
export function searchContent(
|
|
242
|
+
content: string,
|
|
243
|
+
options: {
|
|
244
|
+
pattern: string;
|
|
245
|
+
ignoreCase?: boolean;
|
|
246
|
+
multiline?: boolean;
|
|
247
|
+
maxCount?: number;
|
|
248
|
+
offset?: number;
|
|
249
|
+
context?: number;
|
|
250
|
+
maxColumns?: number;
|
|
251
|
+
mode?: "content" | "count";
|
|
252
|
+
},
|
|
253
|
+
): WasmSearchResult {
|
|
254
|
+
return wasmSearch(content, filterUndefined(options)) as WasmSearchResult;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Quick check if content contains a pattern match.
|
|
259
|
+
*/
|
|
260
|
+
export function hasMatch(
|
|
261
|
+
content: string,
|
|
262
|
+
pattern: string,
|
|
263
|
+
options?: { ignoreCase?: boolean; multiline?: boolean },
|
|
264
|
+
): boolean {
|
|
265
|
+
return wasmHasMatch(content, pattern, options?.ignoreCase ?? false, options?.multiline ?? false);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// =============================================================================
|
|
269
|
+
// Public API
|
|
270
|
+
// =============================================================================
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Search files for a regex pattern.
|
|
274
|
+
*
|
|
275
|
+
* Uses worker pool by default. Set `OMP_GREP_WORKERS=0` to disable.
|
|
276
|
+
*/
|
|
277
|
+
export async function grep(options: GrepOptions, onMatch?: (match: GrepMatch) => void): Promise<GrepResult> {
|
|
278
|
+
if (GREP_WORKERS) {
|
|
279
|
+
return await grepPoolInternal(options);
|
|
280
|
+
}
|
|
281
|
+
return await grepDirect(options, onMatch);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Search files using worker pool (always, ignores OMP_GREP_WORKERS).
|
|
286
|
+
*/
|
|
287
|
+
export async function grepPool(options: GrepOptions): Promise<GrepResult> {
|
|
288
|
+
return await grepPoolInternal(options);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// =============================================================================
|
|
292
|
+
// Worker Pool
|
|
293
|
+
// =============================================================================
|
|
294
|
+
|
|
295
|
+
const pool = new WorkerPool<WorkerRequest, WorkerResponse>({
|
|
296
|
+
workerUrl: new URL("./worker.ts", import.meta.url).href,
|
|
297
|
+
maxWorkers: 4,
|
|
298
|
+
idleTimeoutMs: 30_000,
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
async function grepPoolInternal(request: GrepOptions): Promise<GrepResult> {
|
|
302
|
+
const response = await pool.request<Extract<WorkerResponse, { type: "result" }>>({
|
|
303
|
+
type: "grep",
|
|
304
|
+
request,
|
|
305
|
+
});
|
|
306
|
+
return response.result;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Terminate all grep workers. */
|
|
310
|
+
export function terminate(): void {
|
|
311
|
+
pool.terminate();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export { grepDirect };
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/** Options for searching files. */
|
|
2
|
+
export interface GrepOptions {
|
|
3
|
+
/** Regex pattern to search for */
|
|
4
|
+
pattern: string;
|
|
5
|
+
/** Directory or file to search */
|
|
6
|
+
path: string;
|
|
7
|
+
/** Glob filter for filenames (e.g., "*.ts") */
|
|
8
|
+
glob?: string;
|
|
9
|
+
/** Filter by file type (e.g., "js", "py", "rust") */
|
|
10
|
+
type?: string;
|
|
11
|
+
/** Case-insensitive search */
|
|
12
|
+
ignoreCase?: boolean;
|
|
13
|
+
/** Enable multiline matching */
|
|
14
|
+
multiline?: boolean;
|
|
15
|
+
/** Include hidden files (default: true) */
|
|
16
|
+
hidden?: boolean;
|
|
17
|
+
/** Maximum number of matches to return */
|
|
18
|
+
maxCount?: number;
|
|
19
|
+
/** Skip first N matches */
|
|
20
|
+
offset?: number;
|
|
21
|
+
/** Lines of context before/after matches */
|
|
22
|
+
context?: number;
|
|
23
|
+
/** Truncate lines longer than this (characters) */
|
|
24
|
+
maxColumns?: number;
|
|
25
|
+
/** Output mode */
|
|
26
|
+
mode?: "content" | "filesWithMatches" | "count";
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface ContextLine {
|
|
30
|
+
lineNumber: number;
|
|
31
|
+
line: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface GrepMatch {
|
|
35
|
+
path: string;
|
|
36
|
+
lineNumber: number;
|
|
37
|
+
line: string;
|
|
38
|
+
contextBefore?: ContextLine[];
|
|
39
|
+
contextAfter?: ContextLine[];
|
|
40
|
+
truncated?: boolean;
|
|
41
|
+
matchCount?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface GrepSummary {
|
|
45
|
+
totalMatches: number;
|
|
46
|
+
filesWithMatches: number;
|
|
47
|
+
filesSearched: number;
|
|
48
|
+
limitReached?: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface GrepResult extends GrepSummary {
|
|
52
|
+
matches: GrepMatch[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** WASM match result from the compiled pattern. */
|
|
56
|
+
export interface WasmMatch {
|
|
57
|
+
lineNumber: number;
|
|
58
|
+
line: string;
|
|
59
|
+
contextBefore: ContextLine[];
|
|
60
|
+
contextAfter: ContextLine[];
|
|
61
|
+
truncated: boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** WASM search result. */
|
|
65
|
+
export interface WasmSearchResult {
|
|
66
|
+
matches: WasmMatch[];
|
|
67
|
+
matchCount: number;
|
|
68
|
+
limitReached: boolean;
|
|
69
|
+
error?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Message types from main thread to worker. */
|
|
73
|
+
export type WorkerRequest =
|
|
74
|
+
| { type: "init"; id: number }
|
|
75
|
+
| { type: "grep"; id: number; request: GrepOptions }
|
|
76
|
+
| { type: "destroy" };
|
|
77
|
+
|
|
78
|
+
/** Message types from worker to main thread. */
|
|
79
|
+
export type WorkerResponse =
|
|
80
|
+
| { type: "ready"; id: number }
|
|
81
|
+
| { type: "result"; id: number; result: GrepResult }
|
|
82
|
+
| { type: "error"; id: number; error: string };
|