-
Notifications
You must be signed in to change notification settings - Fork 2.9k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
259 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import type { GlobScanOptions } from "bun"; | ||
const { validateObject } = require("internal/validators"); | ||
|
||
const isWindows = process.platform === "win32"; | ||
|
||
interface GlobOptions { | ||
/** @default process.cwd() */ | ||
cwd?: string; | ||
exclude?: (ent: string) => boolean; | ||
/** | ||
* Should glob return paths as {@link Dirent} objects. `false` for strings. | ||
* @default false */ | ||
withFileTypes?: boolean; | ||
} | ||
|
||
interface ExtendedGlobOptions extends GlobScanOptions { | ||
exclude(ent: string): boolean; | ||
} | ||
|
||
async function* glob(pattern: string | string[], options: GlobOptions): AsyncGenerator<string> { | ||
pattern = validatePattern(pattern); | ||
const globOptions = mapOptions(options); | ||
let it = new Bun.Glob(pattern).scan(globOptions); | ||
const exclude = globOptions.exclude; | ||
|
||
for await (const ent of it) { | ||
if (exclude(ent)) continue; | ||
yield ent; | ||
} | ||
} | ||
|
||
function* globSync(pattern: string | string[], options: GlobOptions): Generator<string> { | ||
pattern = validatePattern(pattern); | ||
const globOptions = mapOptions(options); | ||
const g = new Bun.Glob(pattern); | ||
const exclude = globOptions.exclude; | ||
for (const ent of g.scanSync(globOptions)) { | ||
if (exclude(ent)) continue; | ||
yield ent; | ||
} | ||
} | ||
|
||
function validatePattern(pattern: string | string[]): string { | ||
if ($isArray(pattern)) { | ||
throw new TypeError("fs.glob does not support arrays of patterns yet. Please open an issue on GitHub."); | ||
} | ||
if (typeof pattern !== "string") { | ||
throw $ERR_INVALID_ARG_TYPE("pattern", "string", pattern); | ||
} | ||
return isWindows ? pattern.replaceAll("/", "\\") : pattern; | ||
} | ||
|
||
function mapOptions(options: GlobOptions): ExtendedGlobOptions { | ||
// forcing callers to pass a default object prevents internal glob functions | ||
// from becoming megamorphic | ||
$assert(!$isUndefinedOrNull(options) && typeof options === "object", "wrapper methods must pass an options object."); | ||
validateObject(options, "options"); | ||
|
||
const exclude = options.exclude ?? no; | ||
if (typeof exclude !== "function") { | ||
throw $ERR_INVALID_ARG_TYPE("options.exclude", "function", exclude); | ||
} | ||
|
||
if (options.withFileTypes) { | ||
throw new TypeError("fs.glob does not support options.withFileTypes yet. Please open an issue on GitHub."); | ||
} | ||
|
||
return { | ||
// NOTE: this is subtly different from Glob's default behavior. | ||
// `process.cwd()` may be overridden by JS code, but native code will used the | ||
// cached `getcwd` on BunProcess. | ||
cwd: options?.cwd ?? process.cwd(), | ||
// https://github.com/nodejs/node/blob/a9546024975d0bfb0a8ae47da323b10fb5cbb88b/lib/internal/fs/glob.js#L655 | ||
followSymlinks: true, | ||
exclude, | ||
}; | ||
} | ||
|
||
// `var` avoids TDZ checks. | ||
var no = _ => false; | ||
|
||
export default { glob, globSync }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
/** | ||
* @note `fs.glob` et. al. are powered by {@link Bun.Glob}, which is extensively | ||
* tested elsewhere. These tests check API compatibility with Node.js. | ||
*/ | ||
import fs from "node:fs"; | ||
import { describe, beforeAll, afterAll, it, expect } from "bun:test"; | ||
import { isWindows, tempDirWithFiles } from "harness"; | ||
|
||
let tmp: string; | ||
beforeAll(() => { | ||
tmp = tempDirWithFiles("fs-glob", { | ||
"foo.txt": "foo", | ||
a: { | ||
"bar.txt": "bar", | ||
"baz.js": "baz", | ||
}, | ||
}); | ||
}); | ||
|
||
afterAll(() => { | ||
return fs.promises.rmdir(tmp, { recursive: true }); | ||
}); | ||
|
||
describe("fs.glob", () => { | ||
it("has a length of 3", () => { | ||
expect(fs).toHaveProperty("glob"); | ||
expect(typeof fs.glob).toEqual("function"); | ||
expect(fs.glob).toHaveLength(3); | ||
}); | ||
|
||
it("is named 'glob'", () => { | ||
expect(fs.glob.name).toEqual("glob"); | ||
}); | ||
|
||
it("when successful, passes paths to the callback", done => { | ||
fs.glob("*.txt", { cwd: tmp }, (err, paths) => { | ||
expect(err).toBeNull(); | ||
expect(paths.sort()).toStrictEqual(["foo.txt"]); | ||
done(); | ||
}); | ||
}); | ||
|
||
it("can filter out files", done => { | ||
const exclude = (path: string) => path.endsWith(".js"); | ||
fs.glob("a/*", { cwd: tmp, exclude }, (err, paths) => { | ||
if (err) done(err); | ||
if (isWindows) { | ||
expect(paths).toStrictEqual(["a\\bar.txt"]); | ||
} else { | ||
expect(paths).toStrictEqual(["a/bar.txt"]); | ||
} | ||
done(); | ||
}); | ||
}); | ||
|
||
describe("invalid arguments", () => { | ||
it("throws if no callback is provided", () => { | ||
expect(() => fs.glob("*.txt")).toThrow(TypeError); | ||
expect(() => fs.glob("*.txt", undefined)).toThrow(TypeError); | ||
expect(() => fs.glob("*.txt", { cwd: tmp })).toThrow(TypeError); | ||
expect(() => fs.glob("*.txt", { cwd: tmp }, undefined)).toThrow(TypeError); | ||
}); | ||
}); | ||
}); // </fs.glob> | ||
|
||
describe("fs.globSync", () => { | ||
it("has a length of 2", () => { | ||
expect(fs).toHaveProperty("globSync"); | ||
expect(typeof fs.globSync).toBe("function"); | ||
expect(fs.globSync).toHaveLength(2); | ||
}); | ||
|
||
it("is named 'globSync'", () => { | ||
expect(fs.globSync.name).toEqual("globSync"); | ||
}); | ||
|
||
it.each([ | ||
["*.txt", ["foo.txt"]], | ||
["a/**", isWindows ? ["a\\bar.txt", "a\\baz.js"] : ["a/bar.txt", "a/baz.js"]], | ||
])("fs.glob(%p, { cwd: /tmp/fs-glob }) === %p", (pattern, expected) => { | ||
expect(fs.globSync(pattern, { cwd: tmp }).sort()).toStrictEqual(expected); | ||
}); | ||
|
||
describe("when process.cwd() is set", () => { | ||
let oldProcessCwd: () => string; | ||
beforeAll(() => { | ||
oldProcessCwd = process.cwd; | ||
process.cwd = () => tmp; | ||
}); | ||
afterAll(() => { | ||
process.cwd = oldProcessCwd; | ||
}); | ||
|
||
it("respects the new cwd", () => { | ||
expect(fs.globSync("*.txt")).toStrictEqual(["foo.txt"]); | ||
}); | ||
}); | ||
|
||
it("can filter out files", () => { | ||
const exclude = (path: string) => path.endsWith(".js"); | ||
const expected = isWindows ? ["a\\bar.txt"] : ["a/bar.txt"]; | ||
expect(fs.globSync("a/*", { cwd: tmp, exclude })).toStrictEqual(expected); | ||
}); | ||
|
||
describe("invalid arguments", () => { | ||
// TODO: GlobSet | ||
it("does not support arrays of patterns yet", () => { | ||
expect(() => fs.globSync(["*.txt"])).toThrow(TypeError); | ||
}); | ||
}); | ||
}); // </fs.globSync> | ||
|
||
describe("fs.promises.glob", () => { | ||
it("has a length of 2", () => { | ||
expect(fs.promises).toHaveProperty("glob"); | ||
expect(typeof fs.promises.glob).toBe("function"); | ||
expect(fs.promises.glob).toHaveLength(2); | ||
}); | ||
|
||
it("is named 'glob'", () => { | ||
expect(fs.promises.glob.name).toEqual("glob"); | ||
}); | ||
|
||
it("returns an AsyncIterable over matched paths", async () => { | ||
const iter = fs.promises.glob("*.txt", { cwd: tmp }); | ||
// FIXME: .toHaveProperty does not support symbol keys | ||
expect(iter[Symbol.asyncIterator]).toBeDefined(); | ||
for await (const path of iter) { | ||
expect(path).toMatch(/\.txt$/); | ||
} | ||
}); | ||
}); // </fs.promises.glob> |