@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 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 };