Skip to content
Merged
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
5 changes: 5 additions & 0 deletions .changeset/free-views-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@webiny/stdlib": patch
---

Add GlobTool to the available tools - replaces fast-glob
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ The package is ESM-only and ships three subpath exports. Because each is a separ
| `HashFolderTool` / `HashFolderToolFeature` | Deterministic SHA-256 hash of a folder's contents — [docs](src/node/features/HashFolderTool/README.md) |
| `ProcessEnvFeature` | `Env` implementation backed by `process.env` — [docs](src/node/features/ProcessEnv/README.md) |
| `WorkspaceTool` / `WorkspaceToolFeature` | Discover workspaces from root `package.json` (replaces `get-yarn-workspaces`) — [docs](src/node/features/WorkspaceTool/README.md) |
| `GlobTool` / `GlobToolFeature` | Find files and directories by glob pattern — [docs](src/node/features/GlobTool/README.md) |

---

Expand Down
2 changes: 2 additions & 0 deletions __tests__/node/DirectoryTool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
DirectoryToolFeature,
createDirectoryTool
} from "../../src/node/features/DirectoryTool/index.js";
import { GlobToolFeature } from "../../src/node/features/GlobTool/index.js";
import { PinoLoggerConfig, PinoLoggerFeature } from "../../src/node/features/PinoLogger/index.js";

function makeContainer(): Container {
Expand All @@ -16,6 +17,7 @@ function makeContainer(): Container {
getConfig: () => ({ logLevel: "error" as const, transport: "json" as const })
});
PinoLoggerFeature.register(container);
GlobToolFeature.register(container);
DirectoryToolFeature.register(container);
return container;
}
Expand Down
2 changes: 2 additions & 0 deletions __tests__/node/FileTool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
DirectoryToolFeature,
createDirectoryTool
} from "../../src/node/features/DirectoryTool/index.js";
import { GlobToolFeature } from "../../src/node/features/GlobTool/index.js";
import { PinoLoggerConfig, PinoLoggerFeature } from "../../src/node/features/PinoLogger/index.js";

function makeContainer(): Container {
Expand All @@ -20,6 +21,7 @@ function makeContainer(): Container {
getConfig: () => ({ logLevel: "error" as const, transport: "json" as const })
});
PinoLoggerFeature.register(container);
GlobToolFeature.register(container);
DirectoryToolFeature.register(container);
FileToolFeature.register(container);
return container;
Expand Down
156 changes: 156 additions & 0 deletions __tests__/node/GlobTool.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { mkdirSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { tmpdir } from "node:os";
import { Container } from "@webiny/di";
import {
GlobTool,
GlobToolFeature,
createGlobTool
} from "../../src/node/features/GlobTool/index.js";

function makeContainer(): Container {
const container = new Container();
GlobToolFeature.register(container);
return container;
}

describe("GlobTool", () => {
let tmpDir: string;
let tool: GlobTool.Interface;

beforeEach(() => {
tmpDir = join(tmpdir(), `wby-glob-test-${Date.now()}`);
mkdirSync(tmpDir, { recursive: true });
tool = makeContainer().resolve(GlobTool);
});

afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true });
});

describe("findFiles", () => {
it("returns files matching a pattern", () => {
writeFileSync(join(tmpDir, "a.ts"), "");
writeFileSync(join(tmpDir, "b.ts"), "");
writeFileSync(join(tmpDir, "c.json"), "{}");

const result = tool.findFiles("**/*.ts", { cwd: tmpDir });
expect(result.sort()).toEqual(["a.ts", "b.ts"]);
});

it("returns empty array when nothing matches", () => {
writeFileSync(join(tmpDir, "a.txt"), "");
const result = tool.findFiles("**/*.ts", { cwd: tmpDir });
expect(result).toEqual([]);
});

it("supports nested directory patterns", () => {
mkdirSync(join(tmpDir, "src", "lib"), { recursive: true });
writeFileSync(join(tmpDir, "src", "index.ts"), "");
writeFileSync(join(tmpDir, "src", "lib", "utils.ts"), "");

const result = tool.findFiles("src/**/*.ts", { cwd: tmpDir });
expect(result.sort()).toEqual(["src/index.ts", "src/lib/utils.ts"]);
});

it("respects ignore option", () => {
mkdirSync(join(tmpDir, "node_modules"), { recursive: true });
writeFileSync(join(tmpDir, "index.ts"), "");
writeFileSync(join(tmpDir, "node_modules", "dep.ts"), "");

const result = tool.findFiles("**/*.ts", {
cwd: tmpDir,
ignore: ["node_modules/**"]
});
expect(result).toEqual(["index.ts"]);
});

it("finds dot files when dot option is set", () => {
writeFileSync(join(tmpDir, ".hidden.ts"), "");
writeFileSync(join(tmpDir, "visible.ts"), "");

const withDot = tool.findFiles("**/*.ts", { cwd: tmpDir, dot: true });
expect(withDot.sort()).toEqual([".hidden.ts", "visible.ts"]);

const withoutDot = tool.findFiles("**/*.ts", { cwd: tmpDir, dot: false });
expect(withoutDot).toEqual(["visible.ts"]);
});

it("accepts an array of patterns", () => {
writeFileSync(join(tmpDir, "a.ts"), "");
writeFileSync(join(tmpDir, "b.json"), "{}");
writeFileSync(join(tmpDir, "c.txt"), "");

const result = tool.findFiles(["**/*.ts", "**/*.json"], { cwd: tmpDir });
expect(result.sort()).toEqual(["a.ts", "b.json"]);
});
});

describe("findDirectories", () => {
it("returns directories matching a pattern", () => {
mkdirSync(join(tmpDir, "src"), { recursive: true });
mkdirSync(join(tmpDir, "lib"), { recursive: true });
writeFileSync(join(tmpDir, "file.ts"), "");

const result = tool.findDirectories("*", { cwd: tmpDir });
expect(result.sort()).toEqual(["lib", "src"]);
});

it("returns empty array when no directories match", () => {
writeFileSync(join(tmpDir, "file.ts"), "");
const result = tool.findDirectories("nonexistent*", { cwd: tmpDir });
expect(result).toEqual([]);
});

it("finds nested directories", () => {
mkdirSync(join(tmpDir, "a", "b", "c"), { recursive: true });

const result = tool.findDirectories("**/*", { cwd: tmpDir });
expect(result.sort()).toEqual(["a", "a/b", "a/b/c"]);
});
});

describe("findAll", () => {
it("returns all matching entries", () => {
writeFileSync(join(tmpDir, "a.ts"), "");
writeFileSync(join(tmpDir, "b.ts"), "");

const result = tool.findAll("**/*.ts", { cwd: tmpDir });
expect(result.sort()).toEqual(["a.ts", "b.ts"]);
});

it("returns empty array when nothing matches", () => {
const result = tool.findAll("nonexistent*", { cwd: tmpDir });
expect(result).toEqual([]);
});

it("can include both files and directories with onlyFiles: false", () => {
mkdirSync(join(tmpDir, "subdir"), { recursive: true });
writeFileSync(join(tmpDir, "file.ts"), "");

const result = tool.findAll("*", { cwd: tmpDir, onlyFiles: false });
expect(result.sort()).toEqual(["file.ts", "subdir"]);
});
});
});

describe("createGlobTool", () => {
let tmpDir: string;

beforeEach(() => {
tmpDir = join(tmpdir(), `wby-glob-factory-test-${Date.now()}`);
mkdirSync(tmpDir, { recursive: true });
});

afterEach(() => {
rmSync(tmpDir, { recursive: true, force: true });
});

it("creates a working tool without DI", () => {
const tool = createGlobTool();
writeFileSync(join(tmpDir, "test.ts"), "");
const result = tool.findFiles("**/*.ts", { cwd: tmpDir });
expect(result).toEqual(["test.ts"]);
});
});
2 changes: 2 additions & 0 deletions __tests__/node/JsonFileTool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "../../src/node/features/JsonFileTool/index.js";
import { FileTool, FileToolFeature } from "../../src/node/features/FileTool/index.js";
import { DirectoryToolFeature } from "../../src/node/features/DirectoryTool/index.js";
import { GlobToolFeature } from "../../src/node/features/GlobTool/index.js";
import { PinoLoggerConfig, PinoLoggerFeature } from "../../src/node/features/PinoLogger/index.js";

function makeContainer(): Container {
Expand All @@ -19,6 +20,7 @@ function makeContainer(): Container {
getConfig: () => ({ logLevel: "error" as const, transport: "json" as const })
});
PinoLoggerFeature.register(container);
GlobToolFeature.register(container);
DirectoryToolFeature.register(container);
FileToolFeature.register(container);
JsonFileToolFeature.register(container);
Expand Down
2 changes: 2 additions & 0 deletions __tests__/node/PackageJsonFileTool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "../../src/node/features/PackageJsonFileTool/index.js";
import { FileToolFeature } from "../../src/node/features/FileTool/index.js";
import { DirectoryToolFeature } from "../../src/node/features/DirectoryTool/index.js";
import { GlobToolFeature } from "../../src/node/features/GlobTool/index.js";
import { PinoLoggerConfig, PinoLoggerFeature } from "../../src/node/features/PinoLogger/index.js";

function makeContainer(): Container {
Expand All @@ -19,6 +20,7 @@ function makeContainer(): Container {
getConfig: () => ({ logLevel: "error" as const, transport: "json" as const })
});
PinoLoggerFeature.register(container);
GlobToolFeature.register(container);
DirectoryToolFeature.register(container);
FileToolFeature.register(container);
PackageJsonFileToolFeature.register(container);
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,11 @@
"@webiny/di": "^1.0.2",
"bson-objectid": "^2.0.4",
"dot-prop": "^10.2.0",
"fast-glob": "^3.3.3",
"nanoid": "^6.0.0",
"nanoid-dictionary": "^5.0.0",
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"tinyglobby": "^0.2.17",
"type-fest": "^5.8.0",
"zod": "^4.4.3"
},
Expand Down
4 changes: 4 additions & 0 deletions src/common/features/Logger/ConsoleLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,7 @@ export const ConsoleLogger = Logger.createImplementation({
implementation: ConsoleLoggerImpl,
dependencies: [[ConsoleLoggerConfig, { optional: true }]]
});

export const createConsoleLogger = (configProvider?: ConsoleLoggerConfig.Interface) => {
return new ConsoleLoggerImpl(configProvider);
};
2 changes: 1 addition & 1 deletion src/common/features/Logger/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export { Logger } from "./abstractions/Logger.js";
export { ConsoleLoggerConfig } from "./abstractions/ConsoleLoggerConfig.js";
export { ConsoleLoggerFeature } from "./feature.js";
export { ConsoleLogger } from "./ConsoleLogger.js";
export { ConsoleLogger, createConsoleLogger } from "./ConsoleLogger.js";
19 changes: 13 additions & 6 deletions src/node/features/DirectoryTool/DirectoryTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,16 @@ import {
constants
} from "node:fs";
import { dirname } from "node:path";
import fg from "fast-glob";
import { DirectoryTool as DirectoryToolAbstraction } from "./abstractions/DirectoryTool.js";
import type { GlobOptions } from "./abstractions/DirectoryTool.js";
import { Logger, ConsoleLogger } from "~/common/index.js";
import { Logger, ConsoleLogger } from "~/common/features/Logger/index.js";
import { createGlobTool, GlobTool } from "../GlobTool/index.js";

class DirectoryToolImpl implements DirectoryToolAbstraction.Interface {
public constructor(private readonly logger: Logger.Interface) {}
public constructor(
private readonly logger: Logger.Interface,
private readonly globTool: GlobTool.Interface
) {}

public exists(path: string): boolean {
return existsSync(path);
Expand Down Expand Up @@ -77,7 +80,7 @@ class DirectoryToolImpl implements DirectoryToolAbstraction.Interface {
if (!existsSync(cwd)) {
return [];
}
return fg.sync(pattern, {
return this.globTool.findAll(pattern, {
cwd,
dot: options?.dot,
ignore: options?.ignore,
Expand All @@ -90,15 +93,19 @@ class DirectoryToolImpl implements DirectoryToolAbstraction.Interface {

export const DirectoryTool = DirectoryToolAbstraction.createImplementation({
implementation: DirectoryToolImpl,
dependencies: [Logger]
dependencies: [Logger, GlobTool]
});

export interface CreateDirectoryToolParams {
logger?: Logger.Interface;
glob?: GlobTool.Interface;
}

export function createDirectoryTool(
params?: CreateDirectoryToolParams
): DirectoryToolAbstraction.Interface {
return new DirectoryToolImpl(params?.logger ?? new ConsoleLogger());
return new DirectoryToolImpl(
params?.logger ?? new ConsoleLogger(),
params?.glob || createGlobTool()
);
}
45 changes: 45 additions & 0 deletions src/node/features/GlobTool/GlobTool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { GlobTool as Abstraction } from "./abstractions/index.js";
import { globSync } from "tinyglobby";

class GlobToolImpl implements Abstraction.Interface {
public findAll(patterns: string | string[], options?: Abstraction.AllOptions): string[] {
return this.stripTrailingSlashes(globSync(patterns, options));
}

public findFiles(patterns: string | string[], options?: Abstraction.FilesOptions): string[] {
return globSync(patterns, {
...options,
onlyFiles: true
});
}

public findDirectories(
patterns: string | string[],
options?: Abstraction.DirectoriesOptions
): string[] {
return this.stripTrailingSlashes(
globSync(patterns, {
...options,
onlyDirectories: true
})
);
}

private stripTrailingSlashes(paths: string[]): string[] {
return paths.map(function (p) {
if (p.at(-1) !== "/") {
return p;
}
return p.slice(0, -1);
});
}
}

export const GlobTool = Abstraction.createImplementation({
implementation: GlobToolImpl,
dependencies: []
});

export const createGlobTool = (): Abstraction.Interface => {
return new GlobToolImpl();
};
Loading