Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/ISSUE_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
name: Feature Request / Bug Fix
about: Common template for pr and bugfix
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix inconsistent capitalization and phrasing in the front matter.

The about field uses inconsistent capitalization and abbreviated terms. Standardize to match the title field format.

Apply this diff:

-about: Common template for pr and bugfix
+about: Common template for feature requests and bug fixes
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
about: Common template for pr and bugfix
about: Common template for feature requests and bug fixes
🤖 Prompt for AI Agents
.github/ISSUE_TEMPLATE.md around line 3: the front-matter key "about" uses
lowercase and an abbreviated term; update it to match title-style capitalization
and wording (e.g., "About: Common template for PR and Bugfix") so the front
matter is consistently capitalized and phrased like the title.

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)
3 changes: 0 additions & 3 deletions .husky/pre-commit
Original file line number Diff line number Diff line change
@@ -1,4 +1 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"

pnpm lint-staged
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
24 changes: 0 additions & 24 deletions PR_TEMPLATE.md

This file was deleted.

101 changes: 101 additions & 0 deletions __test__/glob.spec.ts
Original file line number Diff line number Diff line change
@@ -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')))
})
10 changes: 7 additions & 3 deletions __test__/readdir.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
}
Expand All @@ -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)
}
})

Expand Down
44 changes: 44 additions & 0 deletions benchmark/glob.ts
Original file line number Diff line number Diff line change
@@ -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,
})
30 changes: 26 additions & 4 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,33 @@
/* 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 glob(pattern: string, options?: GlobOptions | undefined | null): Promise<unknown>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Strengthen the glob return type.

The return type Promise<unknown> is too loose. It should match the synchronous variant to provide proper type safety.

Apply this diff:

-export declare function glob(pattern: string, options?: GlobOptions | undefined | null): Promise<unknown>
+export declare function glob(pattern: string, options?: GlobOptions | undefined | null): Promise<Array<string> | Array<Dirent>>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export declare function glob(pattern: string, options?: GlobOptions | undefined | null): Promise<unknown>
export declare function glob(pattern: string, options?: GlobOptions | undefined | null): Promise<Array<string> | Array<Dirent>>
🤖 Prompt for AI Agents
In index.d.ts around line 16, the async glob declaration returns
Promise<unknown>, which is too loose; change its return type to match the
synchronous variant (use Promise<string[]>), i.e. replace Promise<unknown> with
Promise<string[]>, keep the existing parameter types unchanged, and run type
checks to ensure callers compile against the stronger type.


export interface GlobOptions {
cwd?: string
withFileTypes?: boolean
exclude?: Array<string>
concurrency?: number
gitIgnore?: boolean
}

export declare function globSync(
pattern: string,
options?: GlobOptions | undefined | null,
): Array<string> | Array<Dirent>

export declare function readdir(path: string, options?: ReaddirOptions | undefined | null): Promise<unknown>

/** * Reads the contents of a directory.
Expand Down
3 changes: 3 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,9 @@ 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
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find benchmark directory structure and files
find . -type d -name benchmark -o -name benchmarks 2>/dev/null | head -20

Repository: CoderSerio/hyper-fs

Length of output: 74


🏁 Script executed:

# Search for tinybench and mitata imports across the codebase
echo "=== tinybench usage ==="
rg -i "tinybench" --type ts --type js --type json 2>/dev/null | head -20

echo ""
echo "=== mitata usage ==="
rg -i "mitata" --type ts --type js --type json 2>/dev/null | head -20

Repository: CoderSerio/hyper-fs

Length of output: 358


🏁 Script executed:

# List all benchmark files
ls -la benchmark/

echo ""
echo "=== Check rm.ts imports ==="
head -20 benchmark/rm.ts

echo ""
echo "=== Confirm tinybench not imported anywhere ==="
rg "from ['\"]\s*tinybench" --type ts --type js 2>/dev/null || echo "No tinybench imports found"

Repository: CoderSerio/hyper-fs

Length of output: 1240


Remove tinybench from devDependencies—only mitata is used.

The codebase uses only mitata for benchmarking (imported in benchmark/glob.ts and benchmark/readdir.ts). tinybench is not imported or referenced anywhere and should be removed from package.json.

🤖 Prompt for AI Agents
In package.json around line 71, devDependencies include an unused "tinybench"
entry; remove the "tinybench" dependency from the devDependencies section (leave
"mitata" intact) and run npm/yarn install or update lockfile so package.json and
package-lock.json/yarn.lock remain consistent.

"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)": [
Expand Down Expand Up @@ -105,5 +107,5 @@
"singleQuote": true,
"arrowParens": "always"
},
"packageManager": "pnpm@9.15.0"
"packageManager": "pnpm@^9 || pnpm@^10"
}
Loading
Loading