diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 00000000..9d10cfe9 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,5 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "useTabs": true, + "ignorePatterns": [] +} diff --git a/AGENTS.md b/AGENTS.md index bd94c09a..fdd16202 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -35,8 +35,8 @@ root symlinks for local Obsidian loading. - `npm install`: install dependencies for local development. - `npm run dev`: watch-mode development build via Vite. - `npm run typecheck`: run `tsc --noEmit`. -- `npm run lint`: run ESLint against TypeScript sources. -- `npm run format:check`: run the configured Biome check. +- `npm run lint`: run oxlint against TypeScript sources (includes the obsidianmd guideline rules via jsPlugins). +- `npm run format:check`: run the configured oxfmt check (`npm run format` to write). - `npm run check:a11y`: run `svelte-check --fail-on-warnings`. - `npm run test`: run Svelte checks and the Vitest suite. - `npm run build`: type-check and produce the production plugin bundle. diff --git a/package-lock.json b/package-lock.json index 95c795c2..bb5878e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "openai": "^6.45.0" }, "devDependencies": { - "@biomejs/biome": "^2.5.2", "@semantic-release/git": "^10.0.1", "@sveltejs/vite-plugin-svelte": "^7.1.2", "@testing-library/jest-dom": "^6.9.1", @@ -24,6 +23,7 @@ "jsdom": "^27.2.0", "obsidian": "1.13.1", "obsidian-e2e": "0.6.0", + "oxfmt": "^0.52.0", "oxlint": "^1.67.0", "semantic-release": "^25.0.5", "svelte": "^5.43.14", @@ -171,181 +171,6 @@ "node": ">=6.9.0" } }, - "node_modules/@biomejs/biome": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.2.tgz", - "integrity": "sha512-VQ3RCqr7JmDIX+w6stWYl+g/3bYofN3q2wDBHUKKc/c7i5QWrFKFBZYCYPWTE6agsUPMIZZe6/CMmVUfUAhkKA==", - "dev": true, - "license": "MIT OR Apache-2.0", - "bin": { - "biome": "bin/biome" - }, - "engines": { - "node": ">=14.21.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/biome" - }, - "optionalDependencies": { - "@biomejs/cli-darwin-arm64": "2.5.2", - "@biomejs/cli-darwin-x64": "2.5.2", - "@biomejs/cli-linux-arm64": "2.5.2", - "@biomejs/cli-linux-arm64-musl": "2.5.2", - "@biomejs/cli-linux-x64": "2.5.2", - "@biomejs/cli-linux-x64-musl": "2.5.2", - "@biomejs/cli-win32-arm64": "2.5.2", - "@biomejs/cli-win32-x64": "2.5.2" - } - }, - "node_modules/@biomejs/cli-darwin-arm64": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.2.tgz", - "integrity": "sha512-e7P3P7EkwFc/KiX2AHw4YDLIBOMfG9CPCAwy52k5Bp0dfhkozx9hf6wCmIr2QeXy2XeccJ3V/Sg+hDmzYEqxSg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-darwin-x64": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.2.tgz", - "integrity": "sha512-ymzMvjC1Jg0b9K0D26ZdARqFQXs7MocfLC5FOCGfkC0Ss+ACUJkX5364ZM5nT4NLZanHRZNVrZEy+Ibwcvux/g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.2.tgz", - "integrity": "sha512-t7sseOmqND57uUWTwlawU6BYj+J06T/9EkydzBhkrgw/FK3QVhjU2wsJR0frljrKZ0/I8A/rYw7284QgqjQfIQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-arm64-musl": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.2.tgz", - "integrity": "sha512-w+ANG0ZvTu9IeEg9QnstoOnk6L0fpwJifW6aHR18+cb5Z39bkANItYjAfMrnvce5tmMK+IQ6nPX7/kQFdam5iw==", - "cpu": [ - "arm64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.2.tgz", - "integrity": "sha512-M/lOZrewzTCRDINbjhQ1gYYru37KlD3kJBQwwKCG0ckz5E9IZwIoJ3X0wBwRXA+yBDIwWUuPBHS67HzJY4dTfA==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "glibc" - ], - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-linux-x64-musl": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.2.tgz", - "integrity": "sha512-VArNLAzND063tF+XY0yPyM+DyahpzOMzOAvb7qs259nhjJWRjvjZdssuA+Rfl+l07+NOesKZ0Xu2yFrXyBMtzw==", - "cpu": [ - "x64" - ], - "dev": true, - "libc": [ - "musl" - ], - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-arm64": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.2.tgz", - "integrity": "sha512-kbjFFKyZlzYnAuw7sRy5qDoFG6zrP40UK08oPQsWK0ct3NMnGSt+Bs1iviEEyEIP57N5MrykGXdO/wRiaR4lww==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, - "node_modules/@biomejs/cli-win32-x64": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.2.tgz", - "integrity": "sha512-4InchVpdVmdkkkgjQqKpgvyu+VPnoF/7RPSw5YATgEVpt2j72wcCAeV5TwaE9ZGJUZWZn7v2CwSAj6CrMJEx8A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT OR Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=14.21.3" - } - }, "node_modules/@codemirror/state": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz", @@ -1317,7 +1142,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1335,7 +1159,6 @@ "os": [ "android" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1353,7 +1176,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1371,7 +1193,6 @@ "os": [ "darwin" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1389,7 +1210,6 @@ "os": [ "freebsd" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1407,7 +1227,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1425,7 +1244,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1446,7 +1264,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1467,7 +1284,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1488,7 +1304,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1509,7 +1324,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1530,7 +1344,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1551,7 +1364,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1572,7 +1384,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1593,7 +1404,6 @@ "os": [ "linux" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1611,7 +1421,6 @@ "os": [ "openharmony" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1629,7 +1438,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1647,7 +1455,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -1665,7 +1472,6 @@ "os": [ "win32" ], - "peer": true, "engines": { "node": "^20.19.0 || >=22.12.0" } @@ -12176,7 +11982,6 @@ "integrity": "sha512-nJlYM35F64zTDMecCNhoHNkf+D/eHv7xcjj9XDSj+bFAVtN93m7v8DQMdHd6nDG6Akf/kEYYHmDUBs2Dz27Sug==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "tinypool": "2.1.0" }, @@ -14723,7 +14528,6 @@ "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": "^20.0.0 || >=22.0.0" } diff --git a/package.json b/package.json index 3e4c1daf..445d6d55 100644 --- a/package.json +++ b/package.json @@ -2,13 +2,20 @@ "name": "podnotes", "version": "2.17.2", "description": "Helps you write notes on podcasts.", + "keywords": [], + "license": "MIT", + "author": "Christian Bager Bach Houmann", + "repository": { + "type": "git", + "url": "https://github.com/chhoumann/PodNotes.git" + }, "main": "main.js", "scripts": { "dev": "vite build --watch --mode development", "build": "npm run typecheck && vite build", "typecheck": "tsc --noEmit", "lint": "oxlint --deny-warnings src tests/e2e scripts", - "format:check": "biome check package.json manifest.json tsconfig.json .oxlintrc.json vite.config.ts vitest.config.ts vitest.e2e.config.ts tests/e2e scripts", + "format:check": "oxfmt --check package.json manifest.json tsconfig.json .oxlintrc.json .oxfmtrc.json vite.config.ts vitest.config.ts vitest.e2e.config.ts tests/e2e scripts", "version": "node version-bump.mjs && git add manifest.json versions.json", "semantic-release": "semantic-release", "test": "npm run check:a11y && vitest", @@ -19,16 +26,14 @@ "stop:e2e-obsidian": "node scripts/stop-obsidian-e2e-instance.mjs", "check:a11y": "svelte-check --fail-on-warnings", "docs:build": "mkdocs build -f docs/mkdocs.yml -d site", - "docs:deploy": "npm run docs:build && npx wrangler pages deploy docs/site --project-name podnotes --branch master" + "docs:deploy": "npm run docs:build && npx wrangler pages deploy docs/site --project-name podnotes --branch master", + "format": "oxfmt package.json manifest.json tsconfig.json .oxlintrc.json .oxfmtrc.json vite.config.ts vitest.config.ts vitest.e2e.config.ts tests/e2e scripts" }, - "keywords": [], - "author": "Christian Bager Bach Houmann", - "license": "MIT", - "engines": { - "node": ">=22.0.0" + "dependencies": { + "fuse.js": "^7.4.2", + "openai": "^6.45.0" }, "devDependencies": { - "@biomejs/biome": "^2.5.2", "@semantic-release/git": "^10.0.1", "@sveltejs/vite-plugin-svelte": "^7.1.2", "@testing-library/jest-dom": "^6.9.1", @@ -39,6 +44,7 @@ "jsdom": "^27.2.0", "obsidian": "1.13.1", "obsidian-e2e": "0.6.0", + "oxfmt": "^0.52.0", "oxlint": "^1.67.0", "semantic-release": "^25.0.5", "svelte": "^5.43.14", @@ -49,10 +55,6 @@ "vite": "^8.1.3", "vitest": "^4.1.10" }, - "repository": { - "type": "git", - "url": "https://github.com/chhoumann/PodNotes.git" - }, "overrides": { "@actions/http-client": { "undici": "^6.27.0" @@ -113,8 +115,7 @@ ] ] }, - "dependencies": { - "fuse.js": "^7.4.2", - "openai": "^6.45.0" + "engines": { + "node": ">=22.0.0" } } diff --git a/scripts/obsidian-e2e-cli.mjs b/scripts/obsidian-e2e-cli.mjs index da67991b..351dec0c 100644 --- a/scripts/obsidian-e2e-cli.mjs +++ b/scripts/obsidian-e2e-cli.mjs @@ -57,9 +57,7 @@ export function parseArgs(argv) { const next = argv[index + 1]; if ( index === 0 && - (next === "--help" || - BOOLEAN_OPTIONS.has(next) || - VALUE_OPTIONS.has(next)) + (next === "--help" || BOOLEAN_OPTIONS.has(next) || VALUE_OPTIONS.has(next)) ) { continue; } @@ -134,14 +132,10 @@ export async function ensureObsidianInstance(options) { function spawnObsidian(options, commandArgs) { return new Promise((resolve) => { - const child = spawn( - options.obsidianBin, - obsidianCommandArgs(options, commandArgs), - { - env: obsidianEnv(options), - stdio: "inherit", - }, - ); + const child = spawn(options.obsidianBin, obsidianCommandArgs(options, commandArgs), { + env: obsidianEnv(options), + stdio: "inherit", + }); child.on("close", (code, signal) => { if (signal) { process.kill(process.pid, signal); @@ -163,9 +157,7 @@ async function main() { return; } - const options = resolveInstanceOptions( - parseInstanceArgs(parsed.instanceArgs), - ); + const options = resolveInstanceOptions(parseInstanceArgs(parsed.instanceArgs)); await reapStaleInstances(options); await ensureObsidianInstance(options); process.exitCode = await spawnObsidian(options, parsed.commandArgs); diff --git a/scripts/obsidian-e2e-cli.test.ts b/scripts/obsidian-e2e-cli.test.ts index 325821f6..4ac917cd 100644 --- a/scripts/obsidian-e2e-cli.test.ts +++ b/scripts/obsidian-e2e-cli.test.ts @@ -1,9 +1,5 @@ import { describe, expect, it } from "vitest"; -import { - obsidianCommandArgs, - obsidianEnv, - parseArgs, -} from "./obsidian-e2e-cli.mjs"; +import { obsidianCommandArgs, obsidianEnv, parseArgs } from "./obsidian-e2e-cli.mjs"; describe("obsidian-e2e-cli", () => { it("defaults to an eval of the vault name when no Obsidian command is provided", () => { @@ -63,11 +59,7 @@ describe("obsidian-e2e-cli", () => { "eval", "code=app.vault.getName()", ]), - ).toEqual([ - "vault=podnotes-worktree-a", - "eval", - "code=app.vault.getName()", - ]); + ).toEqual(["vault=podnotes-worktree-a", "eval", "code=app.vault.getName()"]); }); it("runs Obsidian CLI commands with the isolated HOME", () => { diff --git a/scripts/provision-obsidian-e2e-vault.mjs b/scripts/provision-obsidian-e2e-vault.mjs index 091439c0..8f7c95c4 100644 --- a/scripts/provision-obsidian-e2e-vault.mjs +++ b/scripts/provision-obsidian-e2e-vault.mjs @@ -102,8 +102,7 @@ export const DEFAULT_PODNOTES_DATA = { diarizationApiKey: "", transcript: { path: "transcripts/{{podcast}}/{{title}}.md", - template: - "# {{title}}\n\nPodcast: {{podcast}}\nDate: {{date}}\n\n{{transcript}}", + template: "# {{title}}\n\nPodcast: {{podcast}}\nDate: {{date}}\n\n{{transcript}}", diarization: { enabled: false, provider: "openai", @@ -192,8 +191,7 @@ function toOptionKey(arg) { export function resolveProvisionOptions(rawOptions, cwd = process.cwd()) { const worktreePath = path.resolve(cwd, rawOptions.worktree ?? "."); const vaultName = - rawOptions.vault ?? - `${DEFAULT_VAULT_PREFIX}-${slugify(path.basename(worktreePath))}`; + rawOptions.vault ?? `${DEFAULT_VAULT_PREFIX}-${slugify(path.basename(worktreePath))}`; // Default the vault root to the *worktree* (not cwd) so the provisioned vault // always lives inside the checkout whose plugin it links. Anchoring to cwd // would put `--worktree /other/checkout` vaults under the caller's directory, @@ -203,9 +201,7 @@ export function resolveProvisionOptions(rawOptions, cwd = process.cwd()) { ? path.resolve(cwd, rawOptions.root) : path.join(worktreePath, DEFAULT_ROOT); const vaultPath = path.resolve(rootPath, vaultName); - const dataPath = rawOptions.data - ? path.resolve(cwd, rawOptions.data) - : undefined; + const dataPath = rawOptions.data ? path.resolve(cwd, rawOptions.data) : undefined; return { dataPath, @@ -248,10 +244,7 @@ async function assertRequiredPluginFiles(worktreePath) { async function writeJson(filePath, value) { await fs.mkdir(path.dirname(filePath), { recursive: true }); - await fs.writeFile( - `${filePath}.tmp`, - `${JSON.stringify(value, null, "\t")}\n`, - ); + await fs.writeFile(`${filePath}.tmp`, `${JSON.stringify(value, null, "\t")}\n`); await fs.rename(`${filePath}.tmp`, filePath); } @@ -273,15 +266,11 @@ async function linkPluginFile(sourcePath, destinationPath, force) { } const currentTarget = await fs.readlink(destinationPath); - if ( - path.resolve(path.dirname(destinationPath), currentTarget) === sourcePath - ) { + if (path.resolve(path.dirname(destinationPath), currentTarget) === sourcePath) { return; } - throw new Error( - `${destinationPath} points at ${currentTarget}. Use --force to relink it.`, - ); + throw new Error(`${destinationPath} points at ${currentTarget}. Use --force to relink it.`); } await fs.symlink(sourcePath, destinationPath); @@ -297,9 +286,7 @@ export async function provisionVault(options) { await writeJsonIfMissing(path.join(obsidianPath, "app.json"), {}); await writeJsonIfMissing(path.join(obsidianPath, "appearance.json"), {}); await writeJsonIfMissing(path.join(obsidianPath, "core-plugins.json"), []); - await writeJson(path.join(obsidianPath, "community-plugins.json"), [ - PLUGIN_ID, - ]); + await writeJson(path.join(obsidianPath, "community-plugins.json"), [PLUGIN_ID]); await writeJsonIfMissing(path.join(obsidianPath, "workspace.json"), { main: { id: "podnotes-e2e", type: "split", children: [] }, left: { id: "podnotes-e2e-left", type: "split", children: [] }, diff --git a/scripts/provision-obsidian-e2e-vault.test.ts b/scripts/provision-obsidian-e2e-vault.test.ts index 1ee58521..b95fa449 100644 --- a/scripts/provision-obsidian-e2e-vault.test.ts +++ b/scripts/provision-obsidian-e2e-vault.test.ts @@ -23,14 +23,8 @@ async function makeTempDir(name: string) { // so a seeded worktree never has a styles.css to link. async function seedWorktree(dir: string, label: string) { await fs.mkdir(dir, { recursive: true }); - await fs.writeFile( - path.join(dir, "manifest.json"), - JSON.stringify({ id: "podnotes" }), - ); - await fs.writeFile( - path.join(dir, "main.js"), - `console.log(${JSON.stringify(label)});\n`, - ); + await fs.writeFile(path.join(dir, "manifest.json"), JSON.stringify({ id: "podnotes" })); + await fs.writeFile(path.join(dir, "main.js"), `console.log(${JSON.stringify(label)});\n`); } async function readLinkedTarget(filePath: string) { @@ -39,9 +33,7 @@ async function readLinkedTarget(filePath: string) { afterEach(async () => { await Promise.all( - tempRoots - .splice(0) - .map((dir) => fs.rm(dir, { recursive: true, force: true })), + tempRoots.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })), ); }); @@ -102,37 +94,27 @@ describe("provision-obsidian-e2e-vault", () => { }); const result = await provisionVault(options); - const pluginPath = path.join( - result.vaultPath, - ".obsidian", - "plugins", - "podnotes", - ); + const pluginPath = path.join(result.vaultPath, ".obsidian", "plugins", "podnotes"); await expect( - fs.readFile( - path.join(result.vaultPath, ".obsidian", "community-plugins.json"), - "utf8", - ), + fs.readFile(path.join(result.vaultPath, ".obsidian", "community-plugins.json"), "utf8"), ).resolves.toBe('[\n\t"podnotes"\n]\n'); - await expect( - readLinkedTarget(path.join(pluginPath, "main.js")), - ).resolves.toBe(path.join(worktree, "main.js")); - await expect( - readLinkedTarget(path.join(pluginPath, "manifest.json")), - ).resolves.toBe(path.join(worktree, "manifest.json")); + await expect(readLinkedTarget(path.join(pluginPath, "main.js"))).resolves.toBe( + path.join(worktree, "main.js"), + ); + await expect(readLinkedTarget(path.join(pluginPath, "manifest.json"))).resolves.toBe( + path.join(worktree, "manifest.json"), + ); // CSS is injected into main.js, so no styles.css link is created. - await expect( - fs.lstat(path.join(pluginPath, "styles.css")), - ).rejects.toMatchObject({ code: "ENOENT" }); + await expect(fs.lstat(path.join(pluginPath, "styles.css"))).rejects.toMatchObject({ + code: "ENOENT", + }); const seededData = JSON.parse( await fs.readFile(path.join(pluginPath, "data.json"), "utf8"), ); expect(seededData).toEqual(DEFAULT_PODNOTES_DATA); expect(toShellExports(result)).toContain("PODNOTES_E2E_VAULT='podnotes-a'"); - expect(toShellExports(result)).toContain( - `PODNOTES_E2E_VAULT_PATH='${result.vaultPath}'`, - ); + expect(toShellExports(result)).toContain(`PODNOTES_E2E_VAULT_PATH='${result.vaultPath}'`); }); it("keeps separately provisioned worktrees isolated", async () => { @@ -157,27 +139,11 @@ describe("provision-obsidian-e2e-vault", () => { }), ); - const mainA = path.join( - resultA.vaultPath, - ".obsidian", - "plugins", - "podnotes", - "main.js", - ); - const mainB = path.join( - resultB.vaultPath, - ".obsidian", - "plugins", - "podnotes", - "main.js", - ); + const mainA = path.join(resultA.vaultPath, ".obsidian", "plugins", "podnotes", "main.js"); + const mainB = path.join(resultB.vaultPath, ".obsidian", "plugins", "podnotes", "main.js"); - await expect(readLinkedTarget(mainA)).resolves.toBe( - path.join(worktreeA, "main.js"), - ); - await expect(readLinkedTarget(mainB)).resolves.toBe( - path.join(worktreeB, "main.js"), - ); + await expect(readLinkedTarget(mainA)).resolves.toBe(path.join(worktreeA, "main.js")); + await expect(readLinkedTarget(mainB)).resolves.toBe(path.join(worktreeB, "main.js")); expect(resultA.vaultPath).not.toBe(resultB.vaultPath); }); @@ -198,16 +164,12 @@ describe("provision-obsidian-e2e-vault", () => { const result = await provisionVault(options); const dataPath = path.join(result.pluginPath, "data.json"); // First provision with --data copies the seed verbatim (not DEFAULT_PODNOTES_DATA). - await expect(fs.readFile(dataPath, "utf8")).resolves.toBe( - '{"podNotes":{"seed":true}}\n', - ); + await expect(fs.readFile(dataPath, "utf8")).resolves.toBe('{"podNotes":{"seed":true}}\n'); await fs.writeFile(dataPath, '{"podNotes":{"kept":true}}\n'); await provisionVault(options); - await expect(fs.readFile(dataPath, "utf8")).resolves.toBe( - '{"podNotes":{"kept":true}}\n', - ); + await expect(fs.readFile(dataPath, "utf8")).resolves.toBe('{"podNotes":{"kept":true}}\n'); }); it("fails fast when the worktree has no built plugin artifacts", async () => { @@ -215,9 +177,7 @@ describe("provision-obsidian-e2e-vault", () => { const worktree = await makeTempDir("podnotes-worktree-empty"); await expect( - provisionVault( - resolveProvisionOptions({ root, vault: "podnotes-empty", worktree }), - ), + provisionVault(resolveProvisionOptions({ root, vault: "podnotes-empty", worktree })), ).rejects.toThrow(/missing manifest\.json, main\.js/); }); }); diff --git a/scripts/start-obsidian-e2e-instance.mjs b/scripts/start-obsidian-e2e-instance.mjs index ac776cbc..1b1d0909 100644 --- a/scripts/start-obsidian-e2e-instance.mjs +++ b/scripts/start-obsidian-e2e-instance.mjs @@ -103,14 +103,8 @@ function toOptionKey(arg) { export function resolveInstanceOptions(rawOptions, cwd = process.cwd()) { const provisionOptions = resolveProvisionOptions(rawOptions, cwd); - const profileRoot = path.resolve( - cwd, - rawOptions.profileRoot ?? DEFAULT_PROFILE_ROOT, - ); - const instanceId = stableInstanceId( - provisionOptions.worktreePath, - provisionOptions.vaultName, - ); + const profileRoot = path.resolve(cwd, rawOptions.profileRoot ?? DEFAULT_PROFILE_ROOT); + const instanceId = stableInstanceId(provisionOptions.worktreePath, provisionOptions.vaultName); const instancePath = path.join(profileRoot, instanceId); const obsidianHome = path.join(instancePath, "home"); @@ -220,11 +214,7 @@ async function linkHostKeychains(options) { } function stableVaultId(vaultPath) { - return crypto - .createHash("sha256") - .update(path.resolve(vaultPath)) - .digest("hex") - .slice(0, 16); + return crypto.createHash("sha256").update(path.resolve(vaultPath)).digest("hex").slice(0, 16); } async function writeJson(filePath, value) { @@ -341,10 +331,7 @@ export async function waitForInstanceReady(options) { lastError = `resolved ${actualPath}, expected ${expectedPath}`; } catch (error) { lastError = - error?.stderr?.trim() || - error?.stdout?.trim() || - error?.message || - String(error); + error?.stderr?.trim() || error?.stdout?.trim() || error?.message || String(error); } await sleep(READY_INTERVAL_MS); } @@ -355,11 +342,7 @@ export async function waitForInstanceReady(options) { } export async function trustVaultAndVerifyPodNotes(options) { - await execObsidian(options, [ - `vault=${options.vaultName}`, - "plugins:restrict", - "off", - ]); + await execObsidian(options, [`vault=${options.vaultName}`, "plugins:restrict", "off"]); const deadline = Date.now() + READY_TIMEOUT_MS; let lastError = ""; @@ -374,10 +357,7 @@ export async function trustVaultAndVerifyPodNotes(options) { lastError = stdout.trim(); } catch (error) { lastError = - error?.stderr?.trim() || - error?.stdout?.trim() || - error?.message || - String(error); + error?.stderr?.trim() || error?.stdout?.trim() || error?.message || String(error); } await sleep(READY_INTERVAL_MS); } @@ -390,11 +370,7 @@ export async function trustVaultAndVerifyPodNotes(options) { export async function reloadPodNotes(options) { // Reload the plugin so a reused instance picks up a rebuilt main.js (the // symlink target) instead of running the bundle it loaded earlier. - await execObsidian(options, [ - `vault=${options.vaultName}`, - "plugin:reload", - "id=podnotes", - ]); + await execObsidian(options, [`vault=${options.vaultName}`, "plugin:reload", "id=podnotes"]); } function sleep(ms) { @@ -426,9 +402,7 @@ function shellQuote(value) { // runtime). Logs go to stderr so `--print-env` keeps stdout to `export …` lines. export async function reapStaleInstances(options) { try { - const { reapOrphanedInstances } = await import( - "./stop-obsidian-e2e-instance.mjs" - ); + const { reapOrphanedInstances } = await import("./stop-obsidian-e2e-instance.mjs"); await reapOrphanedInstances({ profileRoot: options.profileRoot, exceptInstancePath: options.instancePath, diff --git a/scripts/start-obsidian-e2e-instance.test.ts b/scripts/start-obsidian-e2e-instance.test.ts index 87f13518..1d51d9ec 100644 --- a/scripts/start-obsidian-e2e-instance.test.ts +++ b/scripts/start-obsidian-e2e-instance.test.ts @@ -19,9 +19,7 @@ async function makeTempDir(name: string) { afterEach(async () => { await Promise.all( - tempRoots - .splice(0) - .map((dir) => fs.rm(dir, { recursive: true, force: true })), + tempRoots.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })), ); }); @@ -67,9 +65,7 @@ describe("start-obsidian-e2e-instance", () => { ); const profile = await prepareObsidianProfile(options); - const registry = JSON.parse( - await fs.readFile(profile.obsidianJsonPath, "utf8"), - ); + const registry = JSON.parse(await fs.readFile(profile.obsidianJsonPath, "utf8")); const vaults = Object.values(registry.vaults) as Array<{ path: string; open: boolean; @@ -77,16 +73,8 @@ describe("start-obsidian-e2e-instance", () => { expect(registry.cli).toBe(true); expect(registry.updateDisabled).toBe(true); - const hostKeychains = path.join( - process.env.HOME ?? "", - "Library", - "Keychains", - ); - const privateKeychains = path.join( - options.obsidianHome, - "Library", - "Keychains", - ); + const hostKeychains = path.join(process.env.HOME ?? "", "Library", "Keychains"); + const privateKeychains = path.join(options.obsidianHome, "Library", "Keychains"); if (await exists(hostKeychains)) { await expect(fs.readlink(privateKeychains)).resolves.toBe(hostKeychains); } else { @@ -95,9 +83,7 @@ describe("start-obsidian-e2e-instance", () => { }); } expect( - options.obsidianHome.startsWith( - path.join(cwd, "profiles", "podnotes-worktree-a-"), - ), + options.obsidianHome.startsWith(path.join(cwd, "profiles", "podnotes-worktree-a-")), ).toBe(true); expect(options.obsidianHome.endsWith("/home")).toBe(true); expect(vaults).toEqual([ @@ -124,12 +110,12 @@ describe("start-obsidian-e2e-instance", () => { }; expect(toInstanceShellExports(base)).not.toContain("OBSIDIAN_BIN="); - expect( - toInstanceShellExports({ ...base, obsidianBin: "obsidian" }), - ).not.toContain("OBSIDIAN_BIN="); - expect( - toInstanceShellExports({ ...base, obsidianBin: "/opt/custom/obsidian" }), - ).toContain("export OBSIDIAN_BIN='/opt/custom/obsidian'"); + expect(toInstanceShellExports({ ...base, obsidianBin: "obsidian" })).not.toContain( + "OBSIDIAN_BIN=", + ); + expect(toInstanceShellExports({ ...base, obsidianBin: "/opt/custom/obsidian" })).toContain( + "export OBSIDIAN_BIN='/opt/custom/obsidian'", + ); }); }); diff --git a/scripts/stop-obsidian-e2e-instance.mjs b/scripts/stop-obsidian-e2e-instance.mjs index c0476127..a6a02f16 100644 --- a/scripts/stop-obsidian-e2e-instance.mjs +++ b/scripts/stop-obsidian-e2e-instance.mjs @@ -4,10 +4,7 @@ import fs from "node:fs/promises"; import path from "node:path"; import process from "node:process"; import { promisify } from "node:util"; -import { - INSTANCE_MARKER_FILE, - resolveInstanceOptions, -} from "./start-obsidian-e2e-instance.mjs"; +import { INSTANCE_MARKER_FILE, resolveInstanceOptions } from "./start-obsidian-e2e-instance.mjs"; const execFileAsync = promisify(execFile); @@ -106,19 +103,13 @@ function stripPrivatePrefix(value) { export function commandMatchesInstance(command, instancePath) { const stripped = stripPrivatePrefix(instancePath); - const variants = new Set([ - instancePath, - stripped, - stripped.replace(/^\//, "/private/"), - ]); + const variants = new Set([instancePath, stripped, stripped.replace(/^\//, "/private/")]); // Seed only on the Obsidian `--user-data-dir=` flag whose value is THIS // instance's profile dir (with a trailing separator). Scoping to the flag, not // a bare substring, means an unrelated process that merely mentions the path — // a log tail, an editor, a grep — is never pulled into the kill set. The // trailing slash stops a sibling whose id shares this one as a leading string. - return [...variants].some((variant) => - command.includes(`--user-data-dir=${variant}/`), - ); + return [...variants].some((variant) => command.includes(`--user-data-dir=${variant}/`)); } export function parsePsOutput(stdout) { @@ -170,9 +161,7 @@ export function collectInstancePids(processes, instancePath, options = {}) { function assertSafeInstancePath(instancePath, profileRoot) { if (!instancePath || !path.isAbsolute(instancePath)) { - throw new Error( - `Refusing to remove non-absolute instance path: ${instancePath}`, - ); + throw new Error(`Refusing to remove non-absolute instance path: ${instancePath}`); } const base = path.basename(instancePath); if (!INSTANCE_DIR_PATTERN.test(base)) { @@ -188,10 +177,7 @@ function assertSafeInstancePath(instancePath, profileRoot) { // Containment guard: when the caller knows the profile root, the dir we remove // must be a direct child of it. Real callers always do, so a bad --profile-root // can never make us rm a path outside the e2e profile tree. - if ( - profileRoot && - path.resolve(path.dirname(instancePath)) !== path.resolve(profileRoot) - ) { + if (profileRoot && path.resolve(path.dirname(instancePath)) !== path.resolve(profileRoot)) { throw new Error( `Refusing to remove ${instancePath}: not a direct child of profile root ${profileRoot}.`, ); @@ -199,11 +185,9 @@ function assertSafeInstancePath(instancePath, profileRoot) { } async function defaultRunPs() { - const { stdout } = await execFileAsync( - "ps", - ["-axww", "-o", "pid=,ppid=,command="], - { maxBuffer: 16 * 1024 * 1024 }, - ); + const { stdout } = await execFileAsync("ps", ["-axww", "-o", "pid=,ppid=,command="], { + maxBuffer: 16 * 1024 * 1024, + }); return stdout; } @@ -274,8 +258,7 @@ export async function stopInstance(instancePath, options = {}) { await sleep(pollMs); survivors = survivors.filter((pid) => pidAlive(pid, kill)); } - const killed = - survivors.length > 0 ? signalPids(survivors, "SIGKILL", kill) : []; + const killed = survivors.length > 0 ? signalPids(survivors, "SIGKILL", kill) : []; await removeDir(instancePath); @@ -320,9 +303,7 @@ async function pathExists(target) { export async function readInstanceMarker(instancePath, deps = {}) { const readFile = deps.readFile ?? ((file) => fs.readFile(file, "utf8")); try { - return JSON.parse( - await readFile(path.join(instancePath, INSTANCE_MARKER_FILE)), - ); + return JSON.parse(await readFile(path.join(instancePath, INSTANCE_MARKER_FILE))); } catch { return null; } @@ -376,8 +357,7 @@ export async function reapOrphanedInstances(options = {}) { let scanned = 0; const except = exceptInstancePath ? path.resolve(exceptInstancePath) : null; for (const entry of entries) { - if (!entry.isDirectory() || !INSTANCE_DIR_PATTERN.test(entry.name)) - continue; + if (!entry.isDirectory() || !INSTANCE_DIR_PATTERN.test(entry.name)) continue; const instancePath = path.join(profileRoot, entry.name); if (except && path.resolve(instancePath) === except) continue; scanned += 1; @@ -393,9 +373,7 @@ export async function reapOrphanedInstances(options = {}) { await stopInstance(instancePath, { ...deps, profileRoot, dryRun: false }); reaped.push(instancePath); } catch (error) { - log( - `Failed to reap ${entry.name}: ${error instanceof Error ? error.message : error}`, - ); + log(`Failed to reap ${entry.name}: ${error instanceof Error ? error.message : error}`); } } diff --git a/scripts/stop-obsidian-e2e-instance.test.ts b/scripts/stop-obsidian-e2e-instance.test.ts index dfc109e9..d7117958 100644 --- a/scripts/stop-obsidian-e2e-instance.test.ts +++ b/scripts/stop-obsidian-e2e-instance.test.ts @@ -23,9 +23,7 @@ async function makeTempDir(name: string) { afterEach(async () => { await Promise.all( - tempRoots - .splice(0) - .map((dir) => fs.rm(dir, { recursive: true, force: true })), + tempRoots.splice(0).map((dir) => fs.rm(dir, { recursive: true, force: true })), ); }); @@ -119,20 +117,17 @@ describe("commandMatchesInstance", () => { }); it("does not match a sibling whose id merely shares this one as a prefix", () => { - expect( - commandMatchesInstance( - `--user-data-dir=${INSTANCE}-extra/home/x`, - INSTANCE, - ), - ).toBe(false); + expect(commandMatchesInstance(`--user-data-dir=${INSTANCE}-extra/home/x`, INSTANCE)).toBe( + false, + ); }); it("does not match a process that mentions the path outside the --user-data-dir flag", () => { // e.g. a log tail or editor referencing a file under the instance dir must // never be pulled into the kill set. - expect( - commandMatchesInstance(`tail -f ${INSTANCE}/home/obsidian.log`, INSTANCE), - ).toBe(false); + expect(commandMatchesInstance(`tail -f ${INSTANCE}/home/obsidian.log`, INSTANCE)).toBe( + false, + ); }); }); @@ -180,9 +175,7 @@ describe("stopInstance", () => { graceMs: 20, }); - const termed = calls - .filter((c) => c.signal === "SIGTERM") - .map((c) => c.pid); + const termed = calls.filter((c) => c.signal === "SIGTERM").map((c) => c.pid); expect(termed).toEqual([100, 101, 102, 103]); expect(calls.some((c) => c.signal === "SIGKILL")).toBe(false); expect(removed).toEqual([INSTANCE]); @@ -199,9 +192,7 @@ describe("stopInstance", () => { pollMs: 1, graceMs: 5, }); - const killed = calls - .filter((c) => c.signal === "SIGKILL") - .map((c) => c.pid); + const killed = calls.filter((c) => c.signal === "SIGKILL").map((c) => c.pid); expect(killed).toEqual([100, 101, 102, 103]); expect(result.killed).toEqual([100, 101, 102, 103]); }); @@ -288,11 +279,7 @@ describe("stopInstance", () => { }); describe("orphan detection", () => { - async function seedInstance( - profileRoot: string, - id: string, - vaultPath: string, - ) { + async function seedInstance(profileRoot: string, id: string, vaultPath: string) { const udd = path.join( profileRoot, id, @@ -311,27 +298,15 @@ describe("orphan detection", () => { it("reads the registered vault paths", async () => { const root = await makeTempDir("podnotes-profiles"); - const instance = await seedInstance( - root, - "podnotes-a-aaaaaaaaaaaa", - "/x/y", - ); + const instance = await seedInstance(root, "podnotes-a-aaaaaaaaaaaa", "/x/y"); await expect(readInstanceVaultPaths(instance)).resolves.toEqual(["/x/y"]); }); it("is orphaned only when every backing vault is gone", async () => { const root = await makeTempDir("podnotes-profiles"); const liveVault = await makeTempDir("live-vault"); - const live = await seedInstance( - root, - "podnotes-live-bbbbbbbbbbbb", - liveVault, - ); - const gone = await seedInstance( - root, - "podnotes-gone-cccccccccccc", - "/does/not/exist", - ); + const live = await seedInstance(root, "podnotes-live-bbbbbbbbbbbb", liveVault); + const gone = await seedInstance(root, "podnotes-gone-cccccccccccc", "/does/not/exist"); await expect(isInstanceOrphaned(live)).resolves.toBe(false); await expect(isInstanceOrphaned(gone)).resolves.toBe(true); @@ -342,22 +317,14 @@ describe("orphan detection", () => { const liveWorktree = await makeTempDir("live-worktree"); // marker worktree alive, but the vault leaf is gone -> NOT orphaned - const spared = await seedInstance( - root, - "podnotes-m1-aaaaaaaaaaaa", - "/vault/leaf/gone", - ); + const spared = await seedInstance(root, "podnotes-m1-aaaaaaaaaaaa", "/vault/leaf/gone"); await fs.writeFile( path.join(spared, "podnotes-e2e-instance.json"), JSON.stringify({ worktreePath: liveWorktree }), ); // marker worktree gone, but the vault still exists -> orphaned (leaked) - const leaked = await seedInstance( - root, - "podnotes-m2-bbbbbbbbbbbb", - liveWorktree, - ); + const leaked = await seedInstance(root, "podnotes-m2-bbbbbbbbbbbb", liveWorktree); await fs.writeFile( path.join(leaked, "podnotes-e2e-instance.json"), JSON.stringify({ worktreePath: "/worktree/removed/on/merge" }), @@ -376,11 +343,7 @@ describe("orphan detection", () => { }); describe("reapOrphanedInstances", () => { - async function seedInstance( - profileRoot: string, - id: string, - vaultPath: string, - ) { + async function seedInstance(profileRoot: string, id: string, vaultPath: string) { const udd = path.join( profileRoot, id, @@ -400,21 +363,9 @@ describe("reapOrphanedInstances", () => { it("reaps only orphaned instances, skipping live ones, the exception, and non-instance dirs", async () => { const root = await makeTempDir("podnotes-profiles"); const liveVault = await makeTempDir("live-vault"); - const live = await seedInstance( - root, - "podnotes-live-bbbbbbbbbbbb", - liveVault, - ); - const orphan = await seedInstance( - root, - "podnotes-gone-cccccccccccc", - "/does/not/exist", - ); - const current = await seedInstance( - root, - "podnotes-current-dddddddddddd", - "/also/gone", - ); + const live = await seedInstance(root, "podnotes-live-bbbbbbbbbbbb", liveVault); + const orphan = await seedInstance(root, "podnotes-gone-cccccccccccc", "/does/not/exist"); + const current = await seedInstance(root, "podnotes-current-dddddddddddd", "/also/gone"); await fs.mkdir(path.join(root, "not-an-instance"), { recursive: true }); const removed: string[] = []; @@ -436,16 +387,8 @@ describe("reapOrphanedInstances", () => { it("continues reaping the rest of the scan when one orphan's teardown fails", async () => { const root = await makeTempDir("podnotes-profiles"); - const bad = await seedInstance( - root, - "podnotes-bad-aaaaaaaaaaaa", - "/gone/1", - ); - const good = await seedInstance( - root, - "podnotes-good-bbbbbbbbbbbb", - "/gone/2", - ); + const bad = await seedInstance(root, "podnotes-bad-aaaaaaaaaaaa", "/gone/1"); + const good = await seedInstance(root, "podnotes-good-bbbbbbbbbbbb", "/gone/2"); const removed: string[] = []; const logs: string[] = []; diff --git a/tests/e2e/harness.ts b/tests/e2e/harness.ts index 78e55e75..9fa74151 100644 --- a/tests/e2e/harness.ts +++ b/tests/e2e/harness.ts @@ -45,11 +45,7 @@ export type PodNotesE2EContext = { sandbox: SandboxApi; }; -const repoRoot = path.resolve( - path.dirname(fileURLToPath(import.meta.url)), - "..", - "..", -); +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", ".."); export function createPodNotesE2EHarness(testName: string) { const state: HarnessState = {}; @@ -123,9 +119,7 @@ export function createPodNotesE2EHarness(testName: string) { if (!state.obsidian) return undefined; return clearVaultRunLockMarker(state.obsidian); }); - await runTeardown("release vault lock", errors, () => - state.lock?.release(), - ); + await runTeardown("release vault lock", errors, () => state.lock?.release()); if (errors.length > 0) { throw errors[0]; @@ -203,9 +197,7 @@ async function flushPodNotesSaves(obsidian: ObsidianClient): Promise { ); } -export async function waitForPodNotesReady( - obsidian: ObsidianClient, -): Promise { +export async function waitForPodNotesReady(obsidian: ObsidianClient): Promise { await obsidian.waitFor( async () => { return await obsidian.dev.evalJson(` @@ -226,10 +218,7 @@ type AsyncEvalEnvelope = | { ok: true; value: T } | { error: { message: string; stack?: string }; ok: false }; -export async function evalJsonAsync( - obsidian: ObsidianClient, - code: string, -): Promise { +export async function evalJsonAsync(obsidian: ObsidianClient, code: string): Promise { const envelope = await obsidian.dev.eval>(` (async () => { const code = ${JSON.stringify(code)}; @@ -262,9 +251,7 @@ export async function evalJsonAsync( return envelope.value; } -export async function openPodNotesView( - obsidian: ObsidianClient, -): Promise { +export async function openPodNotesView(obsidian: ObsidianClient): Promise { const result = await evalJsonAsync<{ activeViewType: string | null; count: number; @@ -305,10 +292,7 @@ async function assertDevVaultSymlinks(vaultPath: string): Promise { await assertSymlinkTarget(pluginDir, "manifest.json"); } -async function assertSymlinkTarget( - pluginDir: string, - fileName: string, -): Promise { +async function assertSymlinkTarget(pluginDir: string, fileName: string): Promise { const linkPath = path.join(pluginDir, fileName); const expected = path.join(repoRoot, fileName); let target: string; diff --git a/tests/e2e/podnotes-runtime.test.ts b/tests/e2e/podnotes-runtime.test.ts index 7feba0a3..a9eb0b72 100644 --- a/tests/e2e/podnotes-runtime.test.ts +++ b/tests/e2e/podnotes-runtime.test.ts @@ -165,10 +165,7 @@ describe("PodNotes runtime", () => { const state = await waitForPlaybackState( obsidian, - (value) => - value.title === episode.title && - value.isPlaying && - value.currentTime === 12, + (value) => value.title === episode.title && value.isPlaying && value.currentTime === 12, ); expect(state).toMatchObject({ @@ -213,10 +210,7 @@ describe("PodNotes runtime", () => { test("captures a linked segment into the active editor", async () => { const { obsidian, plugin, sandbox } = getContext(); const audioPath = await seedAudio(sandbox, "capture-segment-episode.mp3"); - const episode = createLocalEpisode( - "E2E Capture Segment Episode", - audioPath, - ); + const episode = createLocalEpisode("E2E Capture Segment Episode", audioPath); const notePath = sandbox.path("capture-segment-target.md"); await seedRuntimeData(plugin, sandbox, episode, { @@ -229,12 +223,7 @@ describe("PodNotes runtime", () => { await obsidian.command(`${PLUGIN_ID}:capture-segment-10s`).run(); - const expectedLink = `- ${expectedTimestampLink( - "00:01:55-00:02:05", - episode, - 115, - 125, - )}`; + const expectedLink = `- ${expectedTimestampLink("00:01:55-00:02:05", episode, 115, 125)}`; const content = await sandbox.waitForContent( "capture-segment-target.md", (value) => value.includes(expectedLink), @@ -246,12 +235,7 @@ describe("PodNotes runtime", () => { expect(content).toContain("endTime=125"); await obsidian.command(`${PLUGIN_ID}:capture-segment-20s`).run(); - const expected20SecondLink = expectedTimestampLink( - "00:01:45-00:02:05", - episode, - 105, - 125, - ); + const expected20SecondLink = expectedTimestampLink("00:01:45-00:02:05", episode, 105, 125); const contentWithBothSegments = await sandbox.waitForContent( "capture-segment-target.md", (value) => value.includes(expected20SecondLink), @@ -290,9 +274,7 @@ describe("PodNotes runtime", () => { const state = await waitForPlaybackState( obsidian, (value) => - value.title === episode.title && - !value.isPlaying && - value.currentTime === 125, + value.title === episode.title && !value.isPlaying && value.currentTime === 125, ); expect(state).toMatchObject({ @@ -336,10 +318,7 @@ describe("PodNotes runtime", () => { test("previous-track Media Session action captures a timestamp into the active editor", async () => { const { obsidian, plugin, sandbox } = getContext(); const audioPath = await seedAudio(sandbox, "headphone-capture-episode.mp3"); - const episode = createLocalEpisode( - "E2E Headphone Capture Episode", - audioPath, - ); + const episode = createLocalEpisode("E2E Headphone Capture Episode", audioPath); const notePath = sandbox.path("headphone-capture-target.md"); await seedRuntimeData(plugin, sandbox, episode, { @@ -352,10 +331,7 @@ describe("PodNotes runtime", () => { await openMarkdownFile(obsidian, notePath); await setPlayback(obsidian, { currentTime: 95, paused: false }); - const action = await invokeRecordedMediaSessionAction( - obsidian, - "previoustrack", - ); + const action = await invokeRecordedMediaSessionAction(obsidian, "previoustrack"); expect(action).toMatchObject({ action: "previoustrack", ok: true, @@ -376,14 +352,8 @@ describe("PodNotes runtime", () => { test("previous-track Media Session action appends to the episode note without an active editor", async () => { const { obsidian, plugin, sandbox } = getContext(); - const audioPath = await seedAudio( - sandbox, - "headphone-background-episode.mp3", - ); - const episode = createLocalEpisode( - "E2E Background Capture Episode", - audioPath, - ); + const audioPath = await seedAudio(sandbox, "headphone-background-episode.mp3"); + const episode = createLocalEpisode("E2E Background Capture Episode", audioPath); const noteRelativePath = `${episode.title}.md`; await seedRuntimeData(plugin, sandbox, episode, { @@ -405,10 +375,7 @@ describe("PodNotes runtime", () => { `); expect(noEditor).toBe(true); - const action = await invokeRecordedMediaSessionAction( - obsidian, - "previoustrack", - ); + const action = await invokeRecordedMediaSessionAction(obsidian, "previoustrack"); expect(action).toMatchObject({ action: "previoustrack", ok: true, @@ -437,10 +404,7 @@ describe("PodNotes runtime", () => { await waitForPodNotesReady(obsidian); await setVolume(obsidian, 0.42); - await plugin.waitForData( - (data) => data.defaultVolume === 0.42, - WAIT_OPTS, - ); + await plugin.waitForData((data) => data.defaultVolume === 0.42, WAIT_OPTS); await setVolume(obsidian, 1.5); const data = await plugin.waitForData( @@ -454,10 +418,7 @@ describe("PodNotes runtime", () => { }); }); -async function seedAudio( - sandbox: SandboxApi, - fileName: string, -): Promise { +async function seedAudio(sandbox: SandboxApi, fileName: string): Promise { await sandbox.write(fileName, "podnotes e2e audio placeholder", { waitForContent: true, waitOptions: WAIT_OPTS, @@ -900,9 +861,7 @@ async function installMediaSessionRecorder( ); if (!result.ok) { - throw new Error( - result.error ?? "Failed to install Media Session recorder.", - ); + throw new Error(result.error ?? "Failed to install Media Session recorder."); } } diff --git a/tsconfig.json b/tsconfig.json index d63cb195..853e25bf 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -14,13 +14,7 @@ "importHelpers": true, "isolatedModules": true, "verbatimModuleSyntax": true, - "types": [ - "svelte", - "node", - "obsidian", - "vitest/globals", - "@testing-library/jest-dom" - ], + "types": ["svelte", "node", "obsidian", "vitest/globals", "@testing-library/jest-dom"], "skipLibCheck": true, "lib": ["DOM", "ES2020"] }, diff --git a/vite.config.ts b/vite.config.ts index 08a11720..f6fa1671 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -40,9 +40,7 @@ const externalDeps = [ "@lezer/lr", ]; -const builtinExternals = builtinModules.filter( - (moduleName) => !moduleName.startsWith("_"), -); +const builtinExternals = builtinModules.filter((moduleName) => !moduleName.startsWith("_")); const external = [ ...externalDeps,