refactor: replace jest & esbuild with vite and vitest

This commit is contained in:
Jacobtread 2026-03-26 15:36:04 +13:00
parent 2229612d6f
commit 31197666d7
22 changed files with 1255 additions and 4712 deletions

View file

@ -1,32 +0,0 @@
/* eslint-disable */
const process = require("process");
const builtins = require("builtin-modules");
const prod = process.argv[2] === "production";
module.exports = {
platform: "browser",
bundle: true,
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtins,
],
format: "cjs",
target: "es2018",
sourcemap: prod ? false : "inline",
treeShaking: true,
loader: { ".ttf": "dataurl" },
};

View file

@ -1,11 +0,0 @@
const { pathsToModuleNameMapper, JestConfigWithTsJest } = require("ts-jest");
const { compilerOptions } = require("./tsconfig.json");
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
moduleFileExtensions: ["ts", "js", "json", "node"],
moduleDirectories: ["node_modules", "<rootDir>"],
extensionsToTreatAsEsm: [".ts"],
moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths),
};

5621
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -3,33 +3,31 @@
"version": "1.16.0",
"description": "Obsidian plugin for time tracking",
"main": "main.js",
"type": "module",
"scripts": {
"dev": "node scripts/dev.js",
"build": "node scripts/build.js",
"version": "node scripts/version-bump.mjs && git add manifest.json versions.json",
"version": "node scripts/version-bump.js && git add manifest.json versions.json",
"lint": "eslint src",
"format": "prettier -w src",
"test": "jest --coverage"
"test": "vitest run --coverage"
},
"keywords": [],
"author": "Jacobtread",
"license": "MIT",
"devDependencies": {
"@types/jest": "^30.0.0",
"@types/node": "^16.11.6",
"@types/node": "^20.19.37",
"@types/pdfmake": "^0.3.2",
"@typescript-eslint/eslint-plugin": "8.57.2",
"@typescript-eslint/parser": "8.57.2",
"builtin-modules": "^3.3.0",
"chokidar": "^5.0.0",
"esbuild": "^0.27.4",
"@vitest/coverage-v8": "^4.1.1",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-perfectionist": "^5.7.0",
"jest": "^30.3.0",
"obsidian": "latest",
"prettier": "^3.8.1",
"ts-jest": "^29.4.6",
"typescript": "5.9.2"
"typescript": "5.9.2",
"vite": "^8.0.2",
"vitest": "^4.1.1"
},
"dependencies": {
"moment": "^2.30.1",

View file

@ -1,57 +1,37 @@
const path = require("path");
const fs = require('fs/promises');
const esbuild = require("esbuild");
import path from "path";
import fs from "fs/promises";
import { build } from "vite";
import { fileURLToPath } from "url";
const esbuildConfig = require('../esbuild.config');
async function build() {
const rootPath = path.join(__dirname, "../");
async function buildPlugin() {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootPath = path.resolve(__dirname, "../");
const manifestPath = path.join(rootPath, "manifest.json");
const outputPath = path.join(rootPath, "dist");
const srcPath = path.join(rootPath, "src");
// Setup output path
const outputExists = await dirExists(outputPath);
if (!outputExists) {
fs.mkdir(outputPath, { recursive: true });
}
await ensureDir(outputPath);
// Perform a build
await esbuild.build({
...esbuildConfig,
entryPoints:
[
path.join(srcPath, "main.ts"),
path.join(srcPath, 'styles.css'),
],
bundle: true,
outdir: path.join(outputPath),
await build({
configFile: path.resolve(rootPath, "vite.config.js"),
});
// Copy files from outside build
for (const filePath of [manifestPath]) {
const filename = path.basename(filePath);
const destPath = path.join(outputPath, filename);
try {
await fs.copyFile(filePath, destPath);
} catch (e) {
console.error(`❌ Failed to copy ${filename}:`, e.message);
}
}
}
async function dirExists(path) {
const destManifest = path.join(outputPath, "manifest.json");
try {
const stat = await fs.stat(path);
return stat.isDirectory();
} catch (err) {
if (err.code === 'ENOENT') return false;
throw err;
await fs.copyFile(manifestPath, destManifest);
console.info(`Copied manifest.json`);
} catch (e) {
console.error(`Failed to copy manifest.json:`, e.message);
}
}
async function ensureDir(dir) {
try {
await fs.mkdir(dir, { recursive: true });
} catch {}
}
build().catch((err) => {
buildPlugin().catch((err) => {
console.error(err);
process.exit(1);
});
});

View file

@ -1,100 +1,77 @@
const path = require("path");
const fs = require('fs/promises');
const esbuild = require("esbuild");
const chokidar = require("chokidar");
const esbuildConfig = require('../esbuild.config');
import path from "path";
import fs from "fs/promises";
import { build } from "vite";
import { fileURLToPath } from "url";
async function dev() {
const rootPath = path.join(__dirname, "../");
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const rootPath = path.resolve(__dirname, "../");
const manifestPath = path.join(rootPath, "manifest.json");
const outputPath = path.join(rootPath, "dist");
const srcPath = path.join(rootPath, "src");
const manifestContents = await fs.readFile(manifestPath);
const manifestContents = await fs.readFile(manifestPath, "utf-8");
const manifest = JSON.parse(manifestContents);
const vaultPath = path.join(rootPath, "test-vault");
const pluginPath = path.join(vaultPath, ".obsidian", "plugins", manifest.id);
const pluginPath = path.join(
vaultPath,
".obsidian",
"plugins",
manifest.id
);
// Setup path to the plugin
const pluginPathExists = await dirExists(pluginPath);
if (!pluginPathExists) {
fs.mkdir(pluginPath, { recursive: true });
}
// Setup output path
const outputExists = await dirExists(outputPath);
if (!outputExists) {
fs.mkdir(outputPath, { recursive: true });
}
await ensureDir(pluginPath);
await ensureDir(outputPath);
const inputFiles = [
path.join(outputPath, 'main.js'),
path.join(outputPath, 'styles.css'),
path.join(outputPath, "main.js"),
path.join(outputPath, "styles.css"),
manifestPath,
];
// Setup esbuild
const ctx = await esbuild.context({
...esbuildConfig,
entryPoints:
[
path.join(srcPath, "main.ts"),
path.join(srcPath, 'styles.css')
],
bundle: true,
outdir: path.join(outputPath),
build({
mode: "development",
configFile: path.resolve(rootPath, "vite.config.js"),
build: {
watch: {},
},
plugins: [
{
name: "build-finish-copy",
closeBundle() {
console.info("Build finished copying files");
copyFiles();
},
},
],
});
// Perform an initial rebuild
await ctx.rebuild();
// Copy initial files
for (const filePath of inputFiles) {
const filename = path.basename(filePath);
const destPath = path.join(pluginPath, filename);
try {
await fs.copyFile(filePath, destPath);
} catch (e) {
console.error(`❌ Failed to copy ${filename}:`, e.message);
const copyFiles = async () => {
for (const filePath of inputFiles) {
const destPath = path.join(pluginPath, path.basename(filePath));
try {
await fs.copyFile(filePath, destPath);
console.info(`✓ Updated ${path.basename(filePath)}`);
} catch (e) {
console.error(
`Failed to copy ${path.basename(filePath)}:`,
e.message
);
}
}
};
}
// Setup watchers for changed files
const watcher = chokidar.watch(inputFiles, { ignoreInitial: true });
watcher.on("change", async (changedPath) => {
const filename = path.basename(changedPath);
const destPath = path.join(pluginPath, filename);
try {
await fs.copyFile(changedPath, destPath);
console.log(`🔁 Updated ${filename}`);
} catch (e) {
console.error(`❌ Failed to copy ${filename}:`, e.message);
}
});
// Start esbuild watching for changes and re-building
await ctx.watch();
console.log("🚀 Dev server started. Watching for changes...");
console.info("🚀 Dev server started. Watching for changes...");
}
async function dirExists(path) {
async function ensureDir(dir) {
try {
const stat = await fs.stat(path);
return stat.isDirectory();
} catch (err) {
if (err.code === 'ENOENT') return false;
throw err;
}
await fs.mkdir(dir, { recursive: true });
} catch {}
}
dev().catch((err) => {
console.error(err);
process.exit(1);
});
});

View file

@ -1,4 +1,5 @@
import type { TFile, Vault } from "obsidian";
import { vi } from "vitest";
function createTFile(vault: Vault, path: string): TFile {
const nameIndex = path.lastIndexOf("/");
@ -30,30 +31,30 @@ export class MockVault {
this._files = initialFiles;
}
getMarkdownFiles = jest.fn(() => {
getMarkdownFiles = vi.fn(() => {
return Object.keys(this._files).map((path) =>
createTFile(this.asVault(), path)
);
});
process = jest.fn(async (file: TFile, func: (data: string) => string) => {
process = vi.fn(async (file: TFile, func: (data: string) => string) => {
const content = await this.read(file);
const data = func(content);
await this.write(file, data);
return data;
});
read = jest.fn(async (file: TFile) => {
read = vi.fn(async (file: TFile) => {
this._cache[file.path] = this._files[file.path];
return this._files[file.path] ?? "";
});
write = jest.fn(async (file: TFile, data: string) => {
write = vi.fn(async (file: TFile, data: string) => {
this._files[file.path] = data;
this._cache[file.path] = data;
});
cachedRead = jest.fn(async (file: TFile) => {
cachedRead = vi.fn(async (file: TFile) => {
if (this._cache[file.path]) {
return this._cache[file.path];
}

View file

@ -3,6 +3,7 @@ import fs from "node:fs/promises";
import { MockVault } from "./__mocks__/obsidian";
import { stopAllTimekeeps } from "./stopAllTimekeeps";
import { expect, it, describe } from "vitest";
describe("stopAllTimekeeps", () => {
it("should stop nothing when no markdown files", async () => {

View file

@ -3,6 +3,7 @@ import fs from "node:fs/promises";
import { MockVault } from "./__mocks__/obsidian";
import { stopFileTimekeeps } from "./stopFileTimekeeps";
import { expect, it, describe } from "vitest";
describe("stopFileTimekeeps", () => {
it("nothing should change if the file has no timekeeps", async () => {

View file

@ -5,6 +5,7 @@ import {
TimekeepSettings,
legacySettingsCompatibility,
} from "./settings";
import { expect, test, describe } from "vitest";
describe("legacy settings compatibility conversion", () => {
test("Empty setting", () => {

View file

@ -2,6 +2,7 @@ import moment from "moment";
import { withEntry, createEntry, withSubEntry } from "./create";
import { stripEntryRuntimeData, stripEntriesRuntimeData } from "./schema";
import { expect, it, test, describe } from "vitest";
describe("createEntry", () => {
it("creating entry should use current time", () => {

View file

@ -8,6 +8,7 @@ import {
replaceTimekeepCodeblock,
extractTimekeepCodeblocks,
} from "./parser";
import { expect, it, test, describe } from "vitest";
/**
* Generates a code block surrounding the provided JSON
@ -184,7 +185,7 @@ describe("loading timekeep", () => {
it("should tolerate a timekeep with leading or trailing whitespaces", () => {
const input = `
\`\`\`timekeep
\`\`\`
\`\`\`
`;
// Start not code fences
replaceTimekeepCodeblock({ entries: [] }, input, 1, 2);

View file

@ -7,6 +7,7 @@ import {
getEntryDuration,
getTotalDuration,
} from "./queries";
import { expect, it, test, describe } from "vitest";
describe("getEntryById", () => {
it("find top level entry", async () => {

View file

@ -1,10 +1,14 @@
import moment from "moment";
import { v4 as uuid } from "uuid";
import { TIMEKEEP } from "@/timekeep/schema";
import { expect, it, describe, vi } from "vitest";
jest.mock("uuid", () => ({
v4: jest.fn(() => "mocked-uuid"),
}));
vi.mock(import("uuid"), async (importOriginal) => {
return {
...(await importOriginal()),
v4: vi.fn(() => "mocked-uuid"),
};
});
describe("schema transform", () => {
it("transforms input with an added id", () => {

View file

@ -4,7 +4,7 @@ import {
defaultSettings,
TimekeepSettings,
} from "@/settings";
import { expect, it, describe } from "vitest";
import { getEntriesSorted } from "./sort";
describe("getEntriesSorted", () => {

View file

@ -1,5 +1,6 @@
import { stripEntriesRuntimeData } from "./schema";
import { startNewEntry, startNewNestedEntry } from "./start";
import { expect, it, describe } from "vitest";
describe("startNewEntry", () => {
it("starting a new entry should stop any running entries", async () => {

View file

@ -6,6 +6,7 @@ import {
setEntryCollapsed,
stopRunningEntries,
} from "./update";
import { expect, it, describe } from "vitest";
describe("updateEntry", () => {
it("updating existing entry should succeed", async () => {

View file

@ -1,4 +1,5 @@
import { NameSegmentType, parseNameSegments } from "./name";
import { expect, it, test, describe } from "vitest";
describe("parseNameSegments", () => {
it("should parse plain text without modification", () => {

View file

@ -13,6 +13,8 @@ import {
formatEditableTimestamp,
} from "./time";
import { expect, it, test, describe } from "vitest";
it("should format time", () => {
const input = moment("2024-03-31T02:34:45.413Z").utc();
const expected = "24-03-31 02:34:45";

58
vite.config.js Normal file
View file

@ -0,0 +1,58 @@
import { defineConfig } from "vite";
import { builtinModules } from "module";
import path from "path";
import { fileURLToPath } from "url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
export default defineConfig((env) => ({
build: {
outDir: "dist",
sourcemap: env.mode === "production" ? false : "inline",
target: "es2018",
minify: true,
lib: {
entry: "src/main.ts",
formats: ["cjs"],
fileName: () => "main.js",
},
rolldownOptions: {
input: {
main: path.resolve(__dirname, "src/main.ts"),
styles: path.resolve(__dirname, "src/styles.css"),
},
external: [
"obsidian",
"electron",
"@codemirror/autocomplete",
"@codemirror/collab",
"@codemirror/commands",
"@codemirror/language",
"@codemirror/lint",
"@codemirror/search",
"@codemirror/state",
"@codemirror/view",
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
...builtinModules,
],
output: {
exports: "auto",
},
},
//
cssCodeSplit: true,
// Inline all assets (.ttf ...etc)
assetsInlineLimit: Infinity,
},
resolve: {
alias: {
"@": path.resolve(__dirname, "src"),
},
},
test: {},
}));