feat: add vitest, vite, move to tdd

This commit is contained in:
2026-07-13 07:07:44 +05:00
parent 4704a1d5a0
commit 62862beb06
18 changed files with 2043 additions and 346 deletions
+104
View File
@@ -0,0 +1,104 @@
import { Task } from "@/model/task";
import { IFileSystem, ITaskRepository } from "@/ports";
import { parseTaskFile, serializeTask } from "@/utils/markdown";
import { generateId } from "@/utils/uuid";
export class FsTaskRepository implements ITaskRepository {
constructor(
private fs: IFileSystem,
private fileExtension: string,
) {}
async list(folderPath: string): Promise<Task[]> {
let entries: string[];
try {
entries = await this.fs.readdir(folderPath);
} catch {
return [];
}
const files = entries.filter((name) => name.endsWith(this.fileExtension));
const tasks: Task[] = [];
for (const file of files) {
try {
const fullPath = this.joinPath(folderPath, file);
const content = await this.fs.readFile(fullPath);
const task = parseTaskFile(content);
if (!task.id) continue;
tasks.push(task);
} catch {
// skip unparseable files
}
}
return tasks.sort(
(a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime(),
);
}
async getById(folderPath: string, id: string): Promise<Task | undefined> {
const tasks = await this.list(folderPath);
return tasks.find((t) => t.id === id);
}
async create(
folderPath: string,
task: Omit<Task, "id" | "created" | "updated">,
): Promise<Task> {
const now = new Date().toISOString();
const newTask: Task = {
...task,
id: generateId(),
created: now,
updated: now,
};
await this.fs.mkdir(folderPath);
const filePath = this.taskFilePath(folderPath, newTask);
await this.fs.writeFile(filePath, serializeTask(newTask));
return newTask;
}
async update(folderPath: string, task: Task): Promise<void> {
const existing = await this.getById(folderPath, task.id);
const updatedTask = { ...task, updated: new Date().toISOString() };
const newPath = this.taskFilePath(folderPath, updatedTask);
if (existing) {
const oldPath = this.taskFilePath(folderPath, existing);
if (oldPath !== newPath) {
await this.fs.deleteFile(oldPath).catch(() => {});
}
}
await this.fs.writeFile(newPath, serializeTask(updatedTask));
}
async delete(folderPath: string, id: string): Promise<void> {
const task = await this.getById(folderPath, id);
if (!task) return;
const filePath = this.taskFilePath(folderPath, task);
try {
await this.fs.deleteFile(filePath);
} catch {
// already gone
}
}
private taskFilePath(folderPath: string, task: Task): string {
const safeName = task.title
.toLowerCase()
.replace(/[^a-z0-9\u0400-\u04FF]+/g, "-")
.replace(/^-|-$/g, "")
.slice(0, 80);
return this.joinPath(folderPath, `${safeName}${this.fileExtension}`);
}
private joinPath(...segments: string[]): string {
return segments.join("/").replace(/\\/g, "/");
}
}
+25
View File
@@ -0,0 +1,25 @@
import * as fs from "fs";
import { IFileSystem } from "@/ports";
export class NodeFileSystem implements IFileSystem {
async readFile(path: string): Promise<string> {
return fs.promises.readFile(path, "utf-8");
}
async writeFile(path: string, content: string): Promise<void> {
await fs.promises.writeFile(path, content, "utf-8");
}
async deleteFile(path: string): Promise<void> {
await fs.promises.unlink(path);
}
async readdir(path: string): Promise<string[]> {
const entries = await fs.promises.readdir(path, { withFileTypes: true });
return entries.filter((e) => e.isFile()).map((e) => e.name);
}
async mkdir(path: string): Promise<void> {
await fs.promises.mkdir(path, { recursive: true });
}
}
@@ -1,7 +1,8 @@
import * as path from "path";
import * as vscode from "vscode";
import { IConfigProvider } from "@/ports";
export class ConfigStore {
export class VscodeConfigProvider implements IConfigProvider {
getProjectTaskPath(): string | undefined {
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspaceRoot) return undefined;
@@ -13,8 +14,7 @@ export class ConfigStore {
getGlobalTaskPath(): string | undefined {
const config = vscode.workspace.getConfiguration("projectTasks");
const globalPath = config.get<string>("globalPath");
return globalPath || undefined;
return config.get<string>("globalPath") || undefined;
}
getFileExtension(): string {
-114
View File
@@ -1,114 +0,0 @@
import * as vscode from "vscode";
import * as path from "path";
import * as fs from "fs";
import { Task } from "../model/task";
import { parseTaskFile, serializeTask } from "../utils/markdown";
import { generateId } from "../utils/uuid";
export class TaskStore {
private fileExtension: string;
constructor() {
const config = vscode.workspace.getConfiguration("projectTasks");
this.fileExtension = config.get<string>("fileExtension") ?? ".task.md";
}
getProjectTaskPath(): string | undefined {
const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
if (!workspaceRoot) return undefined;
const config = vscode.workspace.getConfiguration("projectTasks");
const relativePath = config.get<string>("projectPath") ?? ".vscode/tasks";
return path.join(workspaceRoot, relativePath);
}
getGlobalTaskPath(): string | undefined {
const config = vscode.workspace.getConfiguration("projectTasks");
const globalPath = config.get<string>("globalPath");
return globalPath || undefined;
}
async listTasks(folderPath: string): Promise<Task[]> {
try {
await fs.promises.mkdir(folderPath, { recursive: true });
} catch {
return [];
}
const entries = await fs.promises.readdir(folderPath, {
withFileTypes: true,
});
const files = entries.filter(
(e) => e.isFile() && e.name.endsWith(this.fileExtension),
);
const tasks: Task[] = [];
for (const file of files) {
try {
const content = await fs.promises.readFile(
path.join(folderPath, file.name),
"utf-8",
);
tasks.push(parseTaskFile(content));
} catch (err) {
console.error(`Failed to parse task file ${file.name}:`, err);
}
}
return tasks.sort(
(a, b) => new Date(b.updated).getTime() - new Date(a.updated).getTime(),
);
}
async getTask(folderPath: string, taskId: string): Promise<Task | undefined> {
const tasks = await this.listTasks(folderPath);
return tasks.find((t) => t.id === taskId);
}
async createTask(
folderPath: string,
task: Omit<Task, "id" | "created" | "updated">,
): Promise<Task> {
const now = new Date().toISOString();
const newTask: Task = {
...task,
id: generateId(),
created: now,
updated: now,
};
await fs.promises.mkdir(folderPath, { recursive: true });
const filePath = this.taskFilePath(folderPath, newTask);
await fs.promises.writeFile(filePath, serializeTask(newTask), "utf-8");
return newTask;
}
async updateTask(folderPath: string, task: Task): Promise<void> {
const updatedTask = { ...task, updated: new Date().toISOString() };
const filePath = this.taskFilePath(folderPath, updatedTask);
await fs.promises.writeFile(filePath, serializeTask(updatedTask), "utf-8");
}
async deleteTask(folderPath: string, taskId: string): Promise<void> {
const task = await this.getTask(folderPath, taskId);
if (!task) return;
const filePath = this.taskFilePath(folderPath, task);
try {
await fs.promises.unlink(filePath);
} catch (err) {
console.error(`Failed to delete task file:`, err);
}
}
private taskFilePath(folderPath: string, task: Task): string {
const safeName = task.title
.toLowerCase()
.replace(/[^a-z0-9\u0400-\u04FF]+/g, "-")
.replace(/^-|-$/g, "")
.slice(0, 80);
return path.join(folderPath, `${safeName}${this.fileExtension}`);
}
}