Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
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
2 changes: 1 addition & 1 deletion extensions/collections/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@
"shortid": "catalog:",
"tmp": "catalog:",
"ts-v-gen": "catalog:",
"turbowalk": "catalog:",
"turbowalk": "workspace:*",
"typescript": "catalog:",
"vortex-api": "workspace:*"
}
Expand Down
2 changes: 1 addition & 1 deletion extensions/extension-dashlet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"react-i18next": "catalog:",
"redux": "catalog:",
"redux-thunk": "catalog:",
"turbowalk": "catalog:",
"turbowalk": "workspace:*",
"typescript": "catalog:",
"vortex-api": "workspace:*"
}
Expand Down
2 changes: 1 addition & 1 deletion extensions/gamebryo-savegame-management/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
"redux": "catalog:",
"redux-act": "catalog:",
"redux-thunk": "catalog:",
"turbowalk": "catalog:",
"turbowalk": "workspace:*",
"vortex-api": "workspace:*",
"vortex-parse-ini": "catalog:"
},
Expand Down
2 changes: 1 addition & 1 deletion extensions/games/game-stardewvalley/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"redux-act": "catalog:",
"relaxed-json": "catalog:",
"semver": "catalog:",
"turbowalk": "catalog:"
"turbowalk": "workspace:*"
},
"devDependencies": {
"@nexusmods/nexus-api": "catalog:",
Expand Down
2 changes: 1 addition & 1 deletion extensions/gamestore-origin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
"@types/libxmljs": "catalog:",
"bluebird": "catalog:",
"copyfiles": "catalog:",
"turbowalk": "catalog:",
"turbowalk": "workspace:*",
"typescript": "catalog:",
"vortex-api": "workspace:*",
"winapi-bindings": "catalog:",
Expand Down
2 changes: 1 addition & 1 deletion extensions/gamestore-xbox/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"copyfiles": "catalog:",
"redux": "catalog:",
"redux-thunk": "catalog:",
"turbowalk": "catalog:",
"turbowalk": "workspace:*",
"typescript": "catalog:",
"vortex-api": "workspace:*",
"winapi-bindings": "catalog:",
Expand Down
2 changes: 1 addition & 1 deletion extensions/mod-content/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"react": "catalog:",
"react-bootstrap": "catalog:",
"redux": "catalog:",
"turbowalk": "catalog:",
"turbowalk": "workspace:*",
"vortex-api": "workspace:*"
}
}
2 changes: 1 addition & 1 deletion extensions/mod-dependency-manager/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"reselect": "catalog:",
"semver": "catalog:",
"shortid": "catalog:",
"turbowalk": "catalog:",
"turbowalk": "workspace:*",
"vortex-api": "workspace:*"
}
}
2 changes: 1 addition & 1 deletion extensions/mod-report/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"bs58": "catalog:",
"copyfiles": "catalog:",
"electron": "catalog:",
"turbowalk": "catalog:",
"turbowalk": "workspace:*",
"typescript": "catalog:",
"vortex-api": "workspace:*"
}
Expand Down
2 changes: 1 addition & 1 deletion extensions/new-file-monitor/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"bluebird": "catalog:",
"copyfiles": "catalog:",
"redux": "catalog:",
"turbowalk": "catalog:",
"turbowalk": "workspace:*",
"typescript": "catalog:",
"vortex-api": "workspace:*"
}
Expand Down
2 changes: 1 addition & 1 deletion extensions/script-extender-error-check/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"i18next": "catalog:",
"react": "catalog:",
"react-bootstrap": "catalog:",
"turbowalk": "catalog:",
"turbowalk": "workspace:*",
"vortex-api": "workspace:*"
}
}
2 changes: 1 addition & 1 deletion extensions/test-setup/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"copyfiles": "catalog:",
"i18next": "catalog:",
"react-bootstrap": "catalog:",
"turbowalk": "catalog:",
"turbowalk": "workspace:*",
"vortex-api": "workspace:*",
"winapi-bindings": "catalog:"
}
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,18 @@
"prepare": "husky",
"preinstall": "npx only-allow pnpm",
"start": "pnpm -F @vortex/main run start",
"build": "pnpm run typecheck && pnpm --filter \"@vortex/*\" --filter \"@nexusmods/*\" -r run build",
"build": "pnpm run typecheck && pnpm --filter \"@vortex/*\" --filter \"@nexusmods/*\" --filter \"./packages/**\" --filter \"!@vortex/e2e\" --filter \"!vortex-api\" -r run build",
"build:all": "pnpm run build && pnpm run build:extensions && pnpm run build:assets",
"build:extensions": "pnpm run api && pnpm run typecheck:extensions && pnpm --filter \"./extensions/**\" run build",
"build:assets": "npx tsx scripts/download-duckdb-extensions.ts && node ./scripts/dependency-report.mjs && node ./InstallAssets.mjs ./src/main/out && pnpm sass --style compressed --silence-deprecation=import ./src/stylesheets/loadingScreen.scss ./src/main/out/assets/css/loadingScreen.css && pnpm tailwindcss -i ./src/stylesheets/tailwind-v4.css -o ./src/main/out/assets/css/tailwind-v4.css -m",
"dist": "pnpm run typecheck && pnpm --filter \"@vortex/*\" --filter \"@nexusmods/*\" -r run dist",
"dist": "pnpm run typecheck && pnpm --filter \"@vortex/*\" --filter \"@nexusmods/*\" --filter \"./packages/**\" --filter \"!@vortex/e2e\" --filter \"!vortex-api\" -r run dist",
"dist:all": "pnpm run dist && pnpm run dist:extensions && pnpm run dist:assets",
"dist:extensions": "pnpm run api && pnpm --filter \"./extensions/**\" run dist",
"dist:assets": "npx tsx scripts/download-duckdb-extensions.ts && node ./scripts/dependency-report.mjs && node ./InstallAssets.mjs ./src/main/dist && pnpm sass --style compressed --silence-deprecation=import ./src/stylesheets/loadingScreen.scss ./src/main/dist/assets/css/loadingScreen.css && pnpm tailwindcss -i ./src/stylesheets/tailwind-v4.css -o ./src/main/dist/assets/css/tailwind-v4.css -m",
"package": "pnpm run dist:all && pnpm -F @vortex/main run package",
"package:nosign": "pnpm run dist:all && pnpm -F @vortex/main run package:nosign",
"package:local": "pnpm -F @vortex/main run prepare:win && pnpm run package:nosign",
"typecheck": "pnpm -F @vortex/shared run build && pnpm --filter \"@vortex/*\" --filter \"@nexusmods/*\" -r run typecheck",
"typecheck": "pnpm -F @vortex/shared run build && pnpm --filter \"./packages/**\" --filter \"!@vortex/e2e\" --filter \"!vortex-api\" -r run build && pnpm --filter \"@vortex/*\" --filter \"@nexusmods/*\" --filter \"./packages/**\" --filter \"!@vortex/e2e\" --filter \"!vortex-api\" -r run typecheck",
"typecheck:extensions": "pnpm --filter \"./extensions/**\" run typecheck",
"lint": "pnpm -r run lint",
"lint:quiet": "pnpm -r run lint:quiet",
Expand Down
18 changes: 18 additions & 0 deletions packages/turbowalk/package.json
Comment thread
halgari marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"name": "turbowalk",
"version": "4.0.0",
"description": "Fast directory walking using koffi FFI on Windows, fs on Linux",
"main": "src/index.ts",
"types": "src/index.ts",
"private": true,
"type": "commonjs",
"scripts": {
"build": "pnpm tsdown"
},
"devDependencies": {
"bluebird": "catalog:",
"@types/bluebird": "catalog:",
"koffi": "^2.9.0",
"tsdown": "catalog:"
}
}
146 changes: 146 additions & 0 deletions packages/turbowalk/src/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import * as fs from "fs";
import * as os from "os";
import * as path from "path";

import { describe, it, expect, beforeAll, afterAll } from "vitest";

import turbowalk, { type IEntry } from "./index";

let tmpDir: string;

beforeAll(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "turbowalk-test-"));

// Create a directory tree:
// root/
// file1.txt (10 bytes)
// file2.dat (20 bytes)
// .hidden (0 bytes)
// subdir/
// nested.txt (5 bytes)
// emptydir/
// .hiddendir/
// inside.txt (1 byte)
fs.writeFileSync(path.join(tmpDir, "file1.txt"), "0123456789");
fs.writeFileSync(path.join(tmpDir, "file2.dat"), "01234567890123456789");
fs.writeFileSync(path.join(tmpDir, ".hidden"), "");
fs.mkdirSync(path.join(tmpDir, "subdir"));
fs.writeFileSync(path.join(tmpDir, "subdir", "nested.txt"), "hello");
fs.mkdirSync(path.join(tmpDir, "emptydir"));
fs.mkdirSync(path.join(tmpDir, ".hiddendir"));
fs.writeFileSync(path.join(tmpDir, ".hiddendir", "inside.txt"), "x");
});

afterAll(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});

function collect(dir: string, options?: Parameters<typeof turbowalk>[2]): Promise<IEntry[]> {
const all: IEntry[] = [];
return turbowalk(
dir,
(entries) => {
all.push(...entries);
},
options,
).then(() => all);
}

describe("turbowalk", () => {
it("walks all files and directories recursively", async () => {
const entries = await collect(tmpDir, { skipHidden: false });
const names = entries.map((e) => path.basename(e.filePath)).sort();
expect(names).toEqual([
".hidden",
".hiddendir",
"emptydir",
"file1.txt",
"file2.dat",
"inside.txt",
"nested.txt",
"subdir",
]);
});

it("skips hidden files and directories by default", async () => {
const entries = await collect(tmpDir);
const names = entries.map((e) => path.basename(e.filePath)).sort();
// .hidden, .hiddendir, and inside.txt (inside .hiddendir) should be excluded
expect(names).toEqual(["emptydir", "file1.txt", "file2.dat", "nested.txt", "subdir"]);
});

it("reports correct isDirectory flag", async () => {
const entries = await collect(tmpDir, { skipHidden: false });
const dirs = entries
.filter((e) => e.isDirectory)
.map((e) => path.basename(e.filePath))
.sort();
const files = entries
.filter((e) => !e.isDirectory)
.map((e) => path.basename(e.filePath))
.sort();
expect(dirs).toEqual([".hiddendir", "emptydir", "subdir"]);
expect(files).toEqual([".hidden", "file1.txt", "file2.dat", "inside.txt", "nested.txt"]);
});

it("reports correct file sizes", async () => {
const entries = await collect(tmpDir, { skipHidden: false });
const file1 = entries.find((e) => path.basename(e.filePath) === "file1.txt");
const file2 = entries.find((e) => path.basename(e.filePath) === "file2.dat");
expect(file1?.size).toBe(10);
expect(file2?.size).toBe(20);
});

it("reports mtime as unix seconds", async () => {
const entries = await collect(tmpDir, { skipHidden: false });
const file1 = entries.find((e) => path.basename(e.filePath) === "file1.txt");
expect(file1?.mtime).toBeGreaterThan(0);
// Should be within a few seconds of now
const now = Math.floor(Date.now() / 1000);
expect(file1!.mtime).toBeGreaterThan(now - 60);
expect(file1!.mtime).toBeLessThanOrEqual(now + 1);
});

it("respects recurse: false", async () => {
const entries = await collect(tmpDir, { recurse: false, skipHidden: false });
const names = entries.map((e) => path.basename(e.filePath)).sort();
// Should only include top-level entries, not nested.txt or inside.txt
expect(names).toEqual([
".hidden",
".hiddendir",
"emptydir",
"file1.txt",
"file2.dat",
"subdir",
]);
});

it("handles non-existent directory gracefully", async () => {
const entries = await collect(path.join(tmpDir, "nonexistent"));
expect(entries).toEqual([]);
});

it("calls progress callback with batched entries", async () => {
const calls: number[] = [];
await turbowalk(
tmpDir,
(entries) => {
calls.push(entries.length);
},
{ skipHidden: false },
);
// At least one call should have been made
expect(calls.length).toBeGreaterThan(0);
// Total entries across all calls should match
const total = calls.reduce((a, b) => a + b, 0);
expect(total).toBe(8); // 5 files + 3 dirs
});

it("returns full absolute paths", async () => {
const entries = await collect(tmpDir, { skipHidden: false });
for (const entry of entries) {
expect(path.isAbsolute(entry.filePath)).toBe(true);
expect(entry.filePath.startsWith(tmpDir)).toBe(true);
}
});
});
Loading
Loading