From 09d4f7c3ac65ce9f1c5de4ae98f4c317be14207f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carbon=20=E7=A2=B3=E8=8B=AF?= <2779066456@qq.com> Date: Tue, 9 Dec 2025 00:13:59 +0800 Subject: [PATCH 1/4] refactor(type): update the type Dirent --- __test__/readdir.spec.ts | 10 +++++-- index.d.ts | 15 +++++++--- index.js | 1 + src/glob.rs | 13 +++++++++ src/lib.rs | 3 ++ src/readdir.rs | 14 +++------- src/types.rs | 59 ++++++++++++++++++++++++++++++++++++++++ src/utils.rs | 22 +++++++++++++++ 8 files changed, 120 insertions(+), 17 deletions(-) create mode 100644 src/glob.rs create mode 100644 src/types.rs create mode 100644 src/utils.rs diff --git a/__test__/readdir.spec.ts b/__test__/readdir.spec.ts index b4206a2..fc56da4 100644 --- a/__test__/readdir.spec.ts +++ b/__test__/readdir.spec.ts @@ -25,7 +25,9 @@ test('sync: should return Dirent objects when withFileTypes is true', (t) => { const first = files[0] if (typeof first === 'object') { t.is(typeof first.name, 'string') - t.is(typeof first.isDir, 'boolean') + // Dirent in Node.js (and our implementation) uses methods, not properties for type checking + t.is(typeof first.isDirectory, 'function') + t.is(typeof first.isFile, 'function') } else { t.fail('Should return objects when withFileTypes is true') } @@ -34,12 +36,14 @@ test('sync: should return Dirent objects when withFileTypes is true', (t) => { t.truthy(packageJson, 'Result should contain package.json') if (typeof packageJson !== 'string' && packageJson) { - t.is(packageJson.isDir, false) + t.is(packageJson.isFile(), true) + t.is(packageJson.isDirectory(), false) } const srcDir = files.find((f) => typeof f !== 'string' && f.name === 'src') if (srcDir && typeof srcDir !== 'string') { - t.is(srcDir.isDir, true, 'src should be identified as a directory') + t.is(srcDir.isDirectory(), true, 'src should be identified as a directory') + t.is(srcDir.isFile(), false) } }) diff --git a/index.d.ts b/index.d.ts index 9e51a57..543b302 100644 --- a/index.d.ts +++ b/index.d.ts @@ -1,9 +1,16 @@ /* auto-generated by NAPI-RS */ /* eslint-disable */ -export interface Dirent { - name: string - parentPath: string - isDir: boolean +export declare class Dirent { + readonly name: string + readonly parentPath: string + isFile(): boolean + isDirectory(): boolean + isSymbolicLink(): boolean + isBlockDevice(): boolean + isCharacterDevice(): boolean + isFIFO(): boolean + isSocket(): boolean + get path(): string } export declare function readdir(path: string, options?: ReaddirOptions | undefined | null): Promise diff --git a/index.js b/index.js index 8e0cb2d..e93dac3 100644 --- a/index.js +++ b/index.js @@ -572,6 +572,7 @@ if (!nativeBinding) { } module.exports = nativeBinding +module.exports.Dirent = nativeBinding.Dirent module.exports.readdir = nativeBinding.readdir module.exports.readdirSync = nativeBinding.readdirSync module.exports.rm = nativeBinding.rm diff --git a/src/glob.rs b/src/glob.rs new file mode 100644 index 0000000..53a28f7 --- /dev/null +++ b/src/glob.rs @@ -0,0 +1,13 @@ +// function glob(pattern: string | readonly string[]): NodeJS.AsyncIterator; +// function glob( +// pattern: string | readonly string[], +// options: GlobOptionsWithFileTypes, +// ): NodeJS.AsyncIterator; +// function glob( +// pattern: string | readonly string[], +// options: GlobOptionsWithoutFileTypes, +// ): NodeJS.AsyncIterator; +// function glob( +// pattern: string | readonly string[], +// options: GlobOptions, +// ): NodeJS.AsyncIterator; diff --git a/src/lib.rs b/src/lib.rs index 8757011..11bcf35 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -12,7 +12,10 @@ // define modules pub mod readdir; pub mod rm; +pub mod types; +pub mod utils; //export modules pub use readdir::*; pub use rm::*; +pub use types::*; diff --git a/src/readdir.rs b/src/readdir.rs index 0089e15..7eeb68c 100644 --- a/src/readdir.rs +++ b/src/readdir.rs @@ -1,3 +1,5 @@ +use crate::types::Dirent; +use crate::utils::get_file_type_id; use jwalk::{Parallelism, WalkDir}; use napi::bindgen_prelude::*; use napi::Task; @@ -30,14 +32,6 @@ pub struct ReaddirOptions { pub with_file_types: Option, } -#[napi(object)] // Similar to fs.Dirent -#[derive(Clone)] -pub struct Dirent { - pub name: String, - pub parent_path: String, - pub is_dir: bool, -} - // #[napi] // marco: expose the function to Node fn ls( path_str: String, @@ -89,7 +83,7 @@ fn ls( list.push(Dirent { name: name_str.to_string(), parent_path: parent_path_val.clone(), - is_dir: entry.file_type().map(|t| t.is_dir()).unwrap_or(false), + file_type: entry.file_type().map(|t| get_file_type_id(&t)).unwrap_or(0), }); } else if let Some(ref mut list) = result_files { list.push(name_str.to_string()); @@ -128,7 +122,7 @@ fn ls( Dirent { name: e.file_name().to_string_lossy().to_string(), parent_path: parent, - is_dir: e.file_type().is_dir(), + file_type: get_file_type_id(&e.file_type()), } }) .collect(); diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..2087d2d --- /dev/null +++ b/src/types.rs @@ -0,0 +1,59 @@ +#![allow(dead_code)] + +use napi_derive::napi; + +#[napi] +#[derive(Clone)] +pub struct Dirent { + #[napi(readonly)] + pub name: String, + #[napi(readonly, js_name = "parentPath")] + pub parent_path: String, + // We store type info internally + // 1: file, 2: dir, 3: symlink, 4: block, 5: char, 6: fifo, 7: socket, 0: unknown + pub(crate) file_type: u8, +} + +#[napi] +impl Dirent { + #[napi(js_name = "isFile")] + pub fn is_file(&self) -> bool { + self.file_type == 1 + } + + #[napi(js_name = "isDirectory")] + pub fn is_directory(&self) -> bool { + self.file_type == 2 + } + + #[napi(js_name = "isSymbolicLink")] + pub fn is_symbolic_link(&self) -> bool { + self.file_type == 3 + } + + #[napi(js_name = "isBlockDevice")] + pub fn is_block_device(&self) -> bool { + self.file_type == 4 + } + + #[napi(js_name = "isCharacterDevice")] + pub fn is_character_device(&self) -> bool { + self.file_type == 5 + } + + #[napi(js_name = "isFIFO")] + pub fn is_fifo(&self) -> bool { + self.file_type == 6 + } + + #[napi(js_name = "isSocket")] + pub fn is_socket(&self) -> bool { + self.file_type == 7 + } + + // Deprecated alias + #[napi(getter)] + pub fn path(&self) -> String { + self.parent_path.clone() + } +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..cf17bd7 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,22 @@ +#[cfg(unix)] +use std::os::unix::fs::FileTypeExt; + +pub fn get_file_type_id(ft: &std::fs::FileType) -> u8 { + if ft.is_file() { + 1 + } else if ft.is_dir() { + 2 + } else if ft.is_symlink() { + 3 + } else if cfg!(unix) && ft.is_block_device() { + 4 + } else if cfg!(unix) && ft.is_char_device() { + 5 + } else if cfg!(unix) && ft.is_fifo() { + 6 + } else if cfg!(unix) && ft.is_socket() { + 7 + } else { + 0 + } +} From 32fa367891447dea472066eadbd6972118aad05f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carbon=20=E7=A2=B3=E8=8B=AF?= <2779066456@qq.com> Date: Tue, 9 Dec 2025 02:40:27 +0800 Subject: [PATCH 2/4] =?UTF-8?q?feat(glob):=20=F0=9F=A6=8A=20add=20glob=20f?= =?UTF-8?q?unction?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.toml | 1 + __test__/glob.spec.ts | 101 +++++++++++++++++++++ benchmark/glob.ts | 44 ++++++++++ index.d.ts | 15 ++++ index.js | 2 + package.json | 8 +- pnpm-lock.yaml | 59 +++++++++++++ src/glob.rs | 199 +++++++++++++++++++++++++++++++++++++++--- src/lib.rs | 2 + 9 files changed, 415 insertions(+), 16 deletions(-) create mode 100644 __test__/glob.spec.ts create mode 100644 benchmark/glob.ts diff --git a/Cargo.toml b/Cargo.toml index 8d235ca..4ce7d4c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ version = "0.1.0" crate-type = ["cdylib"] [dependencies] +ignore = "0.4.25" jwalk = "0.8.1" napi = "3.0.0" napi-derive = "3.4" diff --git a/__test__/glob.spec.ts b/__test__/glob.spec.ts new file mode 100644 index 0000000..aa4e4b3 --- /dev/null +++ b/__test__/glob.spec.ts @@ -0,0 +1,101 @@ +import test from 'ava' +import { globSync, glob } from '../index.js' +import { join } from 'path' + +const CWD = process.cwd() + +test('globSync: should find files in current directory', (t) => { + const files = globSync('*.json', { cwd: CWD }) + t.true(files.length > 0) + t.true(files.some((f) => f.endsWith('package.json'))) +}) + +test('globSync: should match files in subdirectories', (t) => { + const files = globSync('src/*.rs', { cwd: CWD }) + t.true(files.length > 0) + t.true(files.some((f) => f.endsWith('lib.rs'))) +}) + +test('globSync: should return Dirent objects when withFileTypes is true', (t) => { + const files = globSync('src/*.rs', { cwd: CWD, withFileTypes: true }) + t.true(files.length > 0) + + const first = files[0] + if (typeof first === 'object') { + t.is(typeof first.isFile, 'function') + t.true(first.isFile()) + t.is(typeof first.name, 'string') + t.true(first.name.endsWith('.rs')) + t.is(typeof first.parentPath, 'string') + } else { + t.fail('Should return objects') + } +}) + +test('globSync: should support exclude option', (t) => { + // Should match multiple .rs files normally + const allFiles = globSync('src/*.rs', { cwd: CWD }) + t.true(allFiles.some((f) => f.endsWith('lib.rs'))) + + // Exclude lib.rs + const filteredFiles = globSync('src/*.rs', { cwd: CWD, exclude: ['lib.rs'] }) + t.true(filteredFiles.length > 0) + t.false( + filteredFiles.some((f) => f.endsWith('lib.rs')), + 'Should exclude lib.rs', + ) + t.true(filteredFiles.length < allFiles.length) +}) + +test('globSync: should respect git_ignore (default: true)', (t) => { + // 'target' directory is usually gitignored in Rust projects + // Note: This test assumes 'target' directory exists and is ignored. + // If running in a fresh clone without build, target might not exist. + // We can skip if target doesn't exist, or just check node_modules which is definitely ignored? + // node_modules is ignored by default in many setups but strict gitignore check depends on .gitignore file. + + // Let's assume 'target' exists because we built the project + const ignoredFiles = globSync('target/**/*.d', { cwd: CWD }) + // Should be empty or very few if ignored (actually cargo ignores target/) + // But wait, standard_filters includes .ignore and .gitignore. + + // If we force git_ignore: false, we should see files + const includedFiles = globSync('target/**/*.d', { cwd: CWD, git_ignore: false }) + + if (includedFiles.length > 0) { + t.true(ignoredFiles.length < includedFiles.length, 'Should find fewer files when respecting gitignore') + } else { + t.pass('Target directory empty or not present, skipping git_ignore comparison') + } +}) + +test('globSync: concurrency option should not crash', (t) => { + const files = globSync('src/**/*.rs', { cwd: CWD, concurrency: 2 }) + t.true(files.length > 0) +}) + +test('async: should work basically', async (t) => { + const files = await glob('*.json', { cwd: CWD }) + t.true(files.length > 0) + t.true(files.some((f) => f.endsWith('package.json'))) +}) + +test('async: withFileTypes', async (t) => { + const files = await glob('src/*.rs', { cwd: CWD, withFileTypes: true }) + t.true(files.length > 0) + const first = files[0] + t.is(typeof first, 'object') + t.true(first.isFile()) +}) + +test('async: should return empty array for no matches', async (t) => { + const files = await glob('non_existent_*.xyz', { cwd: CWD }) + t.true(Array.isArray(files)) + t.is(files.length, 0) +}) + +test('async: recursive match', async (t) => { + const files = await glob('**/*.rs', { cwd: CWD }) + t.true(files.length > 0) + t.true(files.some((f) => f.includes('src/lib.rs'))) +}) diff --git a/benchmark/glob.ts b/benchmark/glob.ts new file mode 100644 index 0000000..9e3d55c --- /dev/null +++ b/benchmark/glob.ts @@ -0,0 +1,44 @@ +import { run, bench, group } from 'mitata' +import { globSync as hyperGlobSync } from '../index.js' +import { globSync as nodeGlobSync } from 'glob' +import fastGlob from 'fast-glob' + +const cwd = process.cwd() + +// Patterns to test +const patternSimple = 'src/*.rs' +const patternRecursive = '**/*.rs' +const patternDeep = 'node_modules/**/*.json' + +console.log(`Benchmarking glob in: ${cwd}`) + +// 1. Simple Flat Glob +group('Glob (Simple: src/*.rs)', () => { + bench('node-glob', () => nodeGlobSync(patternSimple, { cwd })) + bench('fast-glob', () => fastGlob.sync(patternSimple, { cwd })) + bench('hyper-fs', () => hyperGlobSync(patternSimple, { cwd })).baseline() +}) + +// 2. Recursive Glob +group('Glob (Recursive: **/*.rs)', () => { + bench('node-glob', () => nodeGlobSync(patternRecursive, { cwd })) + bench('fast-glob', () => fastGlob.sync(patternRecursive, { cwd })) + bench('hyper-fs', () => hyperGlobSync(patternRecursive, { cwd })).baseline() +}) + +// 3. Deep Recursive (if node_modules exists) +// This is a stress test +group('Glob (Deep: node_modules/**/*.json)', () => { + // Only run if node_modules exists to avoid empty result bias + const hasNodeModules = fastGlob.sync('node_modules').length > 0 + if (hasNodeModules) { + bench('node-glob', () => nodeGlobSync(patternDeep, { cwd })) + bench('fast-glob', () => fastGlob.sync(patternDeep, { cwd })) + bench('hyper-fs', () => hyperGlobSync(patternDeep, { cwd })).baseline() + bench('hyper-fs (8 threads)', () => hyperGlobSync(patternDeep, { cwd, concurrency: 8 })) + } +}) + +await run({ + colors: true, +}) diff --git a/index.d.ts b/index.d.ts index 543b302..29a74b2 100644 --- a/index.d.ts +++ b/index.d.ts @@ -13,6 +13,21 @@ export declare class Dirent { get path(): string } +export declare function glob(pattern: string, options?: GlobOptions | undefined | null): Promise + +export interface GlobOptions { + cwd?: string + withFileTypes?: boolean + exclude?: Array + concurrency?: number + gitIgnore?: boolean +} + +export declare function globSync( + pattern: string, + options?: GlobOptions | undefined | null, +): Array | Array + export declare function readdir(path: string, options?: ReaddirOptions | undefined | null): Promise /** * Reads the contents of a directory. diff --git a/index.js b/index.js index e93dac3..93357ea 100644 --- a/index.js +++ b/index.js @@ -573,6 +573,8 @@ if (!nativeBinding) { module.exports = nativeBinding module.exports.Dirent = nativeBinding.Dirent +module.exports.glob = nativeBinding.glob +module.exports.globSync = nativeBinding.globSync module.exports.readdir = nativeBinding.readdir module.exports.readdirSync = nativeBinding.readdirSync module.exports.rm = nativeBinding.rm diff --git a/package.json b/package.json index d5af444..11daa31 100644 --- a/package.json +++ b/package.json @@ -64,14 +64,16 @@ "@types/node": "^20.0.0", "ava": "^6.4.1", "chalk": "^5.6.2", + "fast-glob": "^3.3.3", + "glob": "^13.0.0", "husky": "^9.1.7", "lint-staged": "^16.1.6", + "mitata": "^1.0.34", "npm-run-all2": "^8.0.4", "oxlint": "^1.14.0", "prettier": "^3.6.2", "tinybench": "^5.0.1", - "typescript": "^5.9.2", - "mitata": "^1.0.34" + "typescript": "^5.9.2" }, "lint-staged": { "*.@(js|ts|tsx)": [ @@ -105,5 +107,5 @@ "singleQuote": true, "arrowParens": "always" }, - "packageManager": "pnpm@9.15.0" + "packageManager": "pnpm@^9 || pnpm@^10" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 11f820c..d49c3af 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -34,6 +34,12 @@ importers: chalk: specifier: ^5.6.2 version: 5.6.2 + fast-glob: + specifier: ^3.3.3 + version: 3.3.3 + glob: + specifier: ^13.0.0 + version: 13.0.0 husky: specifier: ^9.1.7 version: 9.1.7 @@ -222,6 +228,16 @@ packages: '@types/node': optional: true + '@isaacs/balanced-match@4.0.1': + resolution: + { integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ== } + engines: { node: 20 || >=22 } + + '@isaacs/brace-expansion@5.0.0': + resolution: + { integrity: sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA== } + engines: { node: 20 || >=22 } + '@isaacs/cliui@8.0.2': resolution: { integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== } @@ -1288,6 +1304,11 @@ packages: { integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg== } hasBin: true + glob@13.0.0: + resolution: + { integrity: sha512-tvZgpqk6fz4BaNZ66ZsRaZnbHvP/jG3uKJvAZOwEVUL4RTA5nJeeLYfyN9/VA8NX/V3IBG+hkeuGpKjvELkVhA== } + engines: { node: 20 || >=22 } + globby@14.1.0: resolution: { integrity: sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA== } @@ -1444,6 +1465,11 @@ packages: resolution: { integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== } + lru-cache@11.2.4: + resolution: + { integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg== } + engines: { node: 20 || >=22 } + matcher@5.0.0: resolution: { integrity: sha512-s2EMBOWtXFc8dgqvoAzKJXxNHibcdJMV0gwqKUaw9E2JBJuGUK7DrNKrA6g/i+v72TT16+6sVm5mS3thaMLQUw== } @@ -1479,6 +1505,11 @@ packages: { integrity: sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA== } engines: { node: '>=18' } + minimatch@10.1.1: + resolution: + { integrity: sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ== } + engines: { node: 20 || >=22 } + minimatch@9.0.5: resolution: { integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow== } @@ -1598,6 +1629,11 @@ packages: { integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== } engines: { node: '>=16 || 14 >=14.18' } + path-scurry@2.0.1: + resolution: + { integrity: sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA== } + engines: { node: 20 || >=22 } + path-type@6.0.0: resolution: { integrity: sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ== } @@ -2046,6 +2082,12 @@ snapshots: optionalDependencies: '@types/node': 20.19.25 + '@isaacs/balanced-match@4.0.1': {} + + '@isaacs/brace-expansion@5.0.0': + dependencies: + '@isaacs/balanced-match': 4.0.1 + '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -2814,6 +2856,12 @@ snapshots: package-json-from-dist: 1.0.1 path-scurry: 1.11.1 + glob@13.0.0: + dependencies: + minimatch: 10.1.1 + minipass: 7.1.2 + path-scurry: 2.0.1 + globby@14.1.0: dependencies: '@sindresorhus/merge-streams': 2.3.0 @@ -2926,6 +2974,8 @@ snapshots: lru-cache@10.4.3: {} + lru-cache@11.2.4: {} + matcher@5.0.0: dependencies: escape-string-regexp: 5.0.0 @@ -2949,6 +2999,10 @@ snapshots: mimic-function@5.0.1: {} + minimatch@10.1.1: + dependencies: + '@isaacs/brace-expansion': 5.0.0 + minimatch@9.0.5: dependencies: brace-expansion: 2.0.2 @@ -3027,6 +3081,11 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-scurry@2.0.1: + dependencies: + lru-cache: 11.2.4 + minipass: 7.1.2 + path-type@6.0.0: {} picomatch@2.3.1: {} diff --git a/src/glob.rs b/src/glob.rs index 53a28f7..c1dd8bf 100644 --- a/src/glob.rs +++ b/src/glob.rs @@ -1,13 +1,186 @@ -// function glob(pattern: string | readonly string[]): NodeJS.AsyncIterator; -// function glob( -// pattern: string | readonly string[], -// options: GlobOptionsWithFileTypes, -// ): NodeJS.AsyncIterator; -// function glob( -// pattern: string | readonly string[], -// options: GlobOptionsWithoutFileTypes, -// ): NodeJS.AsyncIterator; -// function glob( -// pattern: string | readonly string[], -// options: GlobOptions, -// ): NodeJS.AsyncIterator; +use crate::types::Dirent; +use crate::utils::get_file_type_id; +use ignore::{overrides::OverrideBuilder, WalkBuilder}; +use napi::bindgen_prelude::*; +use napi_derive::napi; +use std::path::Path; +use std::sync::{Arc, Mutex}; + +#[napi(object)] +#[derive(Clone)] +pub struct GlobOptions { + pub cwd: Option, + pub with_file_types: Option, + pub exclude: Option>, + pub concurrency: Option, + pub git_ignore: Option, +} + +#[napi(js_name = "globSync")] +pub fn glob_sync( + pattern: String, + options: Option, +) -> Result, Vec>> { + let opts = options.unwrap_or(GlobOptions { + cwd: None, + with_file_types: None, + exclude: None, + concurrency: None, + git_ignore: None, + }); + + let cwd = opts.cwd.unwrap_or_else(|| ".".to_string()); + let with_file_types = opts.with_file_types.unwrap_or(false); + let concurrency = opts.concurrency.unwrap_or(4) as usize; + + // 1. Build match rules (Matcher) + // ignore crate handles glob patterns via override + let mut override_builder = OverrideBuilder::new(&cwd); + override_builder + .add(&pattern) + .map_err(|e| Error::from_reason(e.to_string()))?; + + if let Some(excludes) = opts.exclude { + for ex in excludes { + // ignore crate exclusions usually start with !, or use builder.add_ignore + // For simplicity here, we assume exclude is also a glob pattern, prepend ! + override_builder + .add(&format!("!{}", ex)) + .map_err(|e| Error::from_reason(e.to_string()))?; + } + } + + let overrides = override_builder + .build() + .map_err(|e| Error::from_reason(e.to_string()))?; + + // 2. Build parallel walker (Walker) + let mut builder = WalkBuilder::new(&cwd); + builder + .overrides(overrides) // Apply glob patterns + .standard_filters(opts.git_ignore.unwrap_or(true)) // Automatically handle .gitignore, .ignore etc + .threads(concurrency); // Core: Enable multithreading with one line! + + // 3. Collect results + // Since it's multithreaded, we need a thread-safe container to store results + // We use two vectors to avoid enum overhead in the lock if possible, but Mutex> is easier + // Let's use an enum wrapper or just two mutexes? Or just one mutex with enum? + // Let's stick to the structure: + let result_strings = Arc::new(Mutex::new(Vec::new())); + let result_dirents = Arc::new(Mutex::new(Vec::new())); + + let result_strings_clone = result_strings.clone(); + let result_dirents_clone = result_dirents.clone(); + + let root_path = Path::new(&cwd).to_path_buf(); + + // 4. Start parallel traversal + builder.build_parallel().run(move || { + let result_strings = result_strings_clone.clone(); + let result_dirents = result_dirents_clone.clone(); + let root = root_path.clone(); + + Box::new(move |entry| { + match entry { + Ok(entry) => { + // WalkBuilder's overrides already help us include or exclude + // However, ignore crate returns directories too if they match. + // Usually globs like "**/*.js" only match files. + // But "src/*" matches both. + // Let's keep logic: if it matches, we keep it. + // But typically glob returns files. + // If the user wants directories, pattern usually ends with /. + // Standard glob behavior varies. + // For now, let's include everything that matches the pattern overrides. + + if entry.depth() == 0 { + return ignore::WalkState::Continue; + } + + if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) { + return ignore::WalkState::Continue; + } + + let path = entry.path(); + // Make path relative to cwd if possible, similar to node-glob + let relative_path = path.strip_prefix(&root).unwrap_or(path); + let relative_path_str = relative_path.to_string_lossy().to_string(); + + if with_file_types { + let mut lock = result_dirents.lock().unwrap(); + + // Convert to Dirent + let parent_path = relative_path + .parent() + .unwrap_or(Path::new("")) + .to_string_lossy() + .to_string(); + let name = relative_path + .file_name() + .unwrap_or_default() + .to_string_lossy() + .to_string(); + + // entry.file_type() is usually cached and efficient + let file_type = if let Some(ft) = entry.file_type() { + get_file_type_id(&ft) + } else { + 0 // Unknown + }; + + lock.push(Dirent { + name, + parent_path, + file_type, + }); + } else { + let mut lock = result_strings.lock().unwrap(); + lock.push(relative_path_str); + } + } + Err(_) => { + // Handle errors or ignore permission errors + } + } + ignore::WalkState::Continue + }) + }); + + if with_file_types { + let final_results = Arc::try_unwrap(result_dirents) + .map_err(|_| Error::from_reason("Lock error"))? + .into_inner() + .map_err(|_| Error::from_reason("Mutex error"))?; + Ok(Either::B(final_results)) + } else { + let final_results = Arc::try_unwrap(result_strings) + .map_err(|_| Error::from_reason("Lock error"))? + .into_inner() + .map_err(|_| Error::from_reason("Mutex error"))?; + Ok(Either::A(final_results)) + } +} + +// Async version task +pub struct GlobTask { + pub pattern: String, + pub options: Option, +} + +impl Task for GlobTask { + type Output = Either, Vec>; + type JsValue = Either, Vec>; + + fn compute(&mut self) -> Result { + glob_sync(self.pattern.clone(), self.options.clone()) + } + + fn resolve(&mut self, _env: Env, output: Self::Output) -> Result { + Ok(output) + } +} + +#[napi(js_name = "glob")] +pub fn glob(pattern: String, options: Option) -> AsyncTask { + AsyncTask::new(GlobTask { pattern, options }) +} diff --git a/src/lib.rs b/src/lib.rs index 11bcf35..700f911 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,12 +10,14 @@ #![deny(clippy::all)] // define modules +pub mod glob; pub mod readdir; pub mod rm; pub mod types; pub mod utils; //export modules +pub use glob::*; pub use readdir::*; pub use rm::*; pub use types::*; From 23e48a516bb66c27b3ccb791fdf1d3d7b19b8eaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carbon=20=E7=A2=B3=E8=8B=AF?= <2779066456@qq.com> Date: Tue, 9 Dec 2025 02:46:48 +0800 Subject: [PATCH 3/4] chore: remove some useless comments and modify precommit file --- .husky/pre-commit | 3 --- src/glob.rs | 11 ++--------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index fab6428..cb2c84d 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -1,4 +1 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - pnpm lint-staged diff --git a/src/glob.rs b/src/glob.rs index c1dd8bf..0b4e26b 100644 --- a/src/glob.rs +++ b/src/glob.rs @@ -33,7 +33,7 @@ pub fn glob_sync( let with_file_types = opts.with_file_types.unwrap_or(false); let concurrency = opts.concurrency.unwrap_or(4) as usize; - // 1. Build match rules (Matcher) + // Build match rules (Matcher) // ignore crate handles glob patterns via override let mut override_builder = OverrideBuilder::new(&cwd); override_builder @@ -54,18 +54,13 @@ pub fn glob_sync( .build() .map_err(|e| Error::from_reason(e.to_string()))?; - // 2. Build parallel walker (Walker) let mut builder = WalkBuilder::new(&cwd); builder .overrides(overrides) // Apply glob patterns .standard_filters(opts.git_ignore.unwrap_or(true)) // Automatically handle .gitignore, .ignore etc .threads(concurrency); // Core: Enable multithreading with one line! - // 3. Collect results - // Since it's multithreaded, we need a thread-safe container to store results // We use two vectors to avoid enum overhead in the lock if possible, but Mutex> is easier - // Let's use an enum wrapper or just two mutexes? Or just one mutex with enum? - // Let's stick to the structure: let result_strings = Arc::new(Mutex::new(Vec::new())); let result_dirents = Arc::new(Mutex::new(Vec::new())); @@ -74,7 +69,6 @@ pub fn glob_sync( let root_path = Path::new(&cwd).to_path_buf(); - // 4. Start parallel traversal builder.build_parallel().run(move || { let result_strings = result_strings_clone.clone(); let result_dirents = result_dirents_clone.clone(); @@ -121,7 +115,6 @@ pub fn glob_sync( .to_string_lossy() .to_string(); - // entry.file_type() is usually cached and efficient let file_type = if let Some(ft) = entry.file_type() { get_file_type_id(&ft) } else { @@ -161,7 +154,7 @@ pub fn glob_sync( } } -// Async version task +// ===== Async version ===== pub struct GlobTask { pub pattern: String, pub options: Option, From 2ffbe15d7cd78e416fca95d22c626074789a9cbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carbon=20=E7=A2=B3=E8=8B=AF?= <2779066456@qq.com> Date: Tue, 9 Dec 2025 11:43:53 +0800 Subject: [PATCH 4/4] docs: add github pr template --- .github/ISSUE_TEMPLATE.md | 35 +++++++++++++++++++++++++++++++++++ PR_TEMPLATE.md | 24 ------------------------ src/types.rs | 1 - 3 files changed, 35 insertions(+), 25 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE.md delete mode 100644 PR_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..e548126 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,35 @@ +--- +name: Feature Request / Bug Fix +about: Common template for pr and bugfix +title: '[FEAT/BUG]' +--- + +## Type of Change + +- [ ] ๐Ÿš€ New feature (non-breaking change which adds functionality) +- [ ] ๐Ÿ› Bug fix (non-breaking change which fixes an issue) +- [ ] โš ๏ธ Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] โšก Performance improvement +- [ ] ๐Ÿ“ Documentation update +- [ ] ๐Ÿ”ง Refactoring (no functional changes, no api changes) +- [ ] ๐Ÿงช Tests + +## Description + +[Please describe the background, possible causes, how to reproduce the issue (code snippets or repo links are appreciated), and any necessary solutions. For feature requests, explain the motivation and use case.] + +## Environment + +- **OS:** [e.g. macOS, Windows, Linux] +- **Node.js Version:** [e.g. v18.16.0] +- **hyper-fs Version:** [e.g. 0.0.1] + +## Related Issues: + +[List the issue numbers related to this issue, e.g. #123] + +## Checklist + +- [ ] I have searched existing issues to ensure this is not a duplicate +- [ ] I have provided a minimal reproduction (for bugs) +- [ ] I am willing to submit a PR (Optional) diff --git a/PR_TEMPLATE.md b/PR_TEMPLATE.md deleted file mode 100644 index 8741969..0000000 --- a/PR_TEMPLATE.md +++ /dev/null @@ -1,24 +0,0 @@ -## Description - -This PR optimizes/implements.... - -## Changes - -- **Feature 1**: - - Desc1... - - Desc2... - -- **Feature 2**: - - Desc1... - - Desc2... - -## Benchmarks - -_(Benchmarks show that...)_ - -## Checklist - -- [ ] Code compiles and passes linter (`cargo check`, `npm run lint`) -- [ ] Added/Updated tests in `__test__/xxx.spec.ts` -- [ ] Verified performance improvements with `benchmark/xxx.ts` -- [ ] Updated type definitions diff --git a/src/types.rs b/src/types.rs index 2087d2d..3f64497 100644 --- a/src/types.rs +++ b/src/types.rs @@ -9,7 +9,6 @@ pub struct Dirent { pub name: String, #[napi(readonly, js_name = "parentPath")] pub parent_path: String, - // We store type info internally // 1: file, 2: dir, 3: symlink, 4: block, 5: char, 6: fifo, 7: socket, 0: unknown pub(crate) file_type: u8, }