Compare commits

..

8 Commits

Author SHA1 Message Date
vorotamoroz
72033472f3 add dependency explicitly 2026-06-17 10:36:38 +01:00
vorotamoroz
93dc03e86f remove unused dependencies, update some dependencies 2026-06-17 10:05:45 +01:00
vorotamoroz
dae8443fe8 update more deps 2026-06-17 06:35:14 +01:00
vorotamoroz
88a8bcbd5a barrel node specific modules to summarise the warnings 2026-06-17 06:15:25 +01:00
vorotamoroz
4a5283543d for automatic review 2026-06-17 06:10:30 +01:00
vorotamoroz
7895336189 revert deno test 2026-06-17 06:08:40 +01:00
vorotamoroz
2d5cdccf7d for automatic review 2026-06-17 05:51:01 +01:00
vorotamoroz
497fd04081 fix global references 2026-06-17 05:29:45 +01:00
53 changed files with 2104 additions and 1390 deletions

2195
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -66,7 +66,8 @@
"devDependencies": {
"@dword-design/eslint-plugin-import-alias": "^8.1.8",
"@eslint/js": "^9.39.3",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@playwright/test": "^1.58.2",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"@tsconfig/svelte": "^5.0.8",
"@types/deno": "^2.5.0",
"@types/diff-match-patch": "^1.0.36",
@@ -81,7 +82,6 @@
"@types/pouchdb-mapreduce": "^6.1.10",
"@types/pouchdb-replication": "^6.4.7",
"@types/transform-pouch": "^1.0.6",
"@typescript-eslint/eslint-plugin": "8.56.1",
"@typescript-eslint/parser": "8.56.1",
"@vitest/browser": "^4.1.8",
"@vitest/browser-playwright": "^4.1.8",
@@ -92,10 +92,9 @@
"esbuild-svelte": "^0.9.4",
"eslint": "^9.39.3",
"eslint-plugin-obsidianmd": "^0.3.0",
"eslint-plugin-svelte": "^3.15.0",
"eslint-plugin-svelte": "^3.19.0",
"events": "^3.3.0",
"globals": "^14.0.0",
"@playwright/test": "^1.58.2",
"playwright": "^1.58.2",
"postcss": "^8.5.6",
"pouchdb-adapter-http": "^9.0.0",
@@ -111,7 +110,7 @@
"pouchdb-utils": "^9.0.0",
"prettier": "3.8.1",
"rollup-plugin-copy": "^3.5.0",
"svelte": "5.41.1",
"svelte": "5.56.3",
"svelte-check": "^4.6.0",
"svelte-eslint-parser": "^1.8.0",
"svelte-preprocess": "^6.0.3",
@@ -121,10 +120,12 @@
"tsx": "^4.21.0",
"typescript": "5.9.3",
"typescript-eslint": "^8.61.0",
"vite": "^7.3.1",
"vite": "^8.0.16",
"vitest": "^4.1.8",
"webdriverio": "^9.27.0",
"yaml": "^2.8.2"
"yaml": "^2.8.2",
"@emnapi/core": "1.11.1",
"@emnapi/runtime": "1.11.1"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.808.0",
@@ -139,9 +140,9 @@
"diff-match-patch": "^1.0.5",
"fflate": "^0.8.2",
"idb": "^8.0.3",
"markdown-it": "^14.1.1",
"minimatch": "^10.2.2",
"obsidian": "^1.12.3",
"markdown-it": "^14.2.0",
"minimatch": "^10.2.5",
"obsidian": "^1.13.1",
"octagonal-wheels": "^0.1.46",
"qrcode-generator": "^1.4.4",
"xxhash-wasm-102": "npm:xxhash-wasm@^1.0.2"

View File

@@ -1,7 +1,7 @@
import * as path from "path";
import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
import type { IConversionAdapter } from "@lib/serviceModules/adapters";
import type { NodeFile, NodeFolder } from "./NodeTypes";
import { path } from "../node-compat";
/**
* Conversion adapter implementation for Node.js

View File

@@ -1,5 +1,3 @@
import * as fs from "fs/promises";
import * as path from "path";
import type { FilePath, UXStat } from "@lib/common/types";
import type { IFileSystemAdapter } from "@lib/serviceModules/adapters";
import { NodePathAdapter } from "./NodePathAdapter";
@@ -8,6 +6,7 @@ import { NodeConversionAdapter } from "./NodeConversionAdapter";
import { NodeStorageAdapter } from "./NodeStorageAdapter";
import { NodeVaultAdapter } from "./NodeVaultAdapter";
import type { NodeFile, NodeFolder, NodeStat } from "./NodeTypes";
import { fsPromises as fs, path } from "../node-compat";
/**
* Complete file system adapter implementation for Node.js

View File

@@ -1,7 +1,7 @@
import * as path from "path";
import type { FilePath } from "@lib/common/types";
import type { IPathAdapter } from "@lib/serviceModules/adapters";
import type { NodeFile } from "./NodeTypes";
import { path } from "../node-compat";
/**
* Path adapter implementation for Node.js

View File

@@ -1,8 +1,7 @@
import * as fs from "fs/promises";
import * as path from "path";
import type { UXDataWriteOptions } from "@lib/common/types";
import type { IStorageAdapter } from "@lib/serviceModules/adapters";
import type { NodeStat } from "./NodeTypes";
import { fsPromises as fs, path } from "../node-compat";
/**
* Storage adapter implementation for Node.js

View File

@@ -1,8 +1,7 @@
import * as fs from "fs/promises";
import * as path from "path";
import type { UXDataWriteOptions } from "@lib/common/types";
import type { IVaultAdapter } from "@lib/serviceModules/adapters";
import type { NodeFile, NodeFolder, NodeStat } from "./NodeTypes";
import type { NodeFile, NodeFolder } from "./NodeTypes";
import { fsPromises as fs, path } from "../node-compat";
/**
* Vault adapter implementation for Node.js

View File

@@ -2,14 +2,15 @@ import type { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
import { P2P_DEFAULT_SETTINGS } from "@lib/common/types";
import type { ServiceContext } from "@lib/services/base/ServiceBase";
import { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
import { addP2PEventHandlers } from "@lib/replication/trystero/addP2PEventHandlers";
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
type CLIP2PPeer = {
peerId: string;
name: string;
};
function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
return new Promise((resolve) => compatGlobal.setTimeout(resolve, ms));
}
export function parseTimeoutSeconds(value: string, commandName: string): number {

View File

@@ -1,5 +1,3 @@
import * as fs from "fs/promises";
import * as path from "path";
import { decodeSettingsFromSetupURI } from "@lib/API/processSetting";
import { configURIBase } from "@lib/common/models/shared.const";
import {
@@ -18,6 +16,8 @@ import { promptForPassphrase, readStdinAsUtf8, toArrayBuffer, toDatabaseRelative
import { collectPeers, openP2PHost, parseTimeoutSeconds, syncWithPeer } from "./p2p";
import { performFullScan } from "@lib/serviceFeatures/offlineScanner";
import { UnresolvedErrorManager } from "@lib/services/base/UnresolvedErrorManager";
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
import { fsPromises as fs, path } from "../node-compat";
function redactConnectionString(uri: string): string {
return uri.replace(/\/\/([^@/]+)@/u, "//***@");
@@ -150,11 +150,11 @@ export async function runCommand(options: CLIOptions, context: CLICommandContext
);
}
}
pollTimer = setTimeout(poll, currentIntervalMs);
pollTimer = compatGlobal.setTimeout(poll, currentIntervalMs);
};
let pollTimer: ReturnType<typeof setTimeout> = setTimeout(poll, currentIntervalMs);
let pollTimer = compatGlobal.setTimeout(poll, currentIntervalMs);
core.services.appLifecycle.onUnload.addHandler(async () => {
clearTimeout(pollTimer);
compatGlobal.clearTimeout(pollTimer);
return true;
});
} else {

View File

@@ -1,5 +1,4 @@
import * as path from "path";
import * as readline from "node:readline/promises";
import { path, readline } from "../node-compat";
export function toArrayBuffer(data: Buffer): ArrayBuffer {
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength) as ArrayBuffer;

View File

@@ -1,4 +1,5 @@
#!/usr/bin/env node
// eslint-disable -- This is the entry point for the CLI application.
import * as polyfill from "werift";
import { main } from "./main";

View File

@@ -1,10 +1,3 @@
/**
* Self-hosted LiveSync CLI
* Command-line version of Self-hosted LiveSync plugin for syncing vaults without Obsidian
*/
import * as fs from "fs/promises";
import * as path from "path";
import { NodeServiceContext, NodeServiceHub } from "./services/NodeServiceHub";
import { configureNodeLocalStorage, ensureGlobalNodeLocalStorage } from "./services/NodeLocalStorage";
import { LiveSyncBaseCore } from "@/LiveSyncBaseCore";
@@ -27,6 +20,7 @@ import { getPathFromUXFileInfo } from "@lib/common/typeUtils";
import { stripAllPrefixes } from "@lib/string_and_binary/path";
import { IgnoreRules } from "./serviceModules/IgnoreRules";
import { useP2PReplicatorFeature } from "@lib/replication/trystero/useP2PReplicatorFeature";
import { fsPromises as fs, path, fs as fsSync } from "./node-compat";
const SETTINGS_FILE = ".livesync/settings.json";
ensureGlobalNodeLocalStorage();
@@ -238,8 +232,8 @@ async function createDefaultSettingsFile(options: CLIOptions) {
const targetPath = options.settingsPath
? path.resolve(options.settingsPath)
: options.commandArgs[0]
? path.resolve(options.commandArgs[0])
: path.resolve(process.cwd(), "data.json");
? path.resolve(options.commandArgs[0])
: path.resolve(process.cwd(), "data.json");
if (!options.force) {
try {
@@ -329,8 +323,8 @@ export async function main() {
options.command === "mirror" && options.commandArgs[0]
? path.resolve(options.commandArgs[0])
: options.vaultPath
? path.resolve(options.vaultPath)
: databasePath!;
? path.resolve(options.vaultPath)
: databasePath!;
// Check if vault directory exists
try {
@@ -485,8 +479,8 @@ export async function main() {
}
};
process.on("SIGINT", () => shutdown("SIGINT"));
process.on("SIGTERM", () => shutdown("SIGTERM"));
process.on("SIGINT", () => void shutdown("SIGINT"));
process.on("SIGTERM", () => void shutdown("SIGTERM"));
// Save the settings file before any lifecycle events can mutate and persist them.
// suspendAllSync and other lifecycle hooks clobber sync settings in memory, and
@@ -499,8 +493,8 @@ export async function main() {
if (settingsBackup) {
const tmpPath = settingsPath + ".tmp";
try {
require("fs").writeFileSync(tmpPath, settingsBackup, "utf-8");
require("fs").renameSync(tmpPath, settingsPath);
fsSync.writeFileSync(tmpPath, settingsBackup, "utf-8");
fsSync.renameSync(tmpPath, settingsPath);
} catch (err) {
console.error("[Settings] Failed to restore settings on exit:", err);
}
@@ -563,7 +557,7 @@ export async function main() {
if (options.command === "daemon" && result) {
// Keep the process running
await new Promise(() => {});
await new Promise(() => { });
} else {
await core.services.control.onUnload();
}

View File

@@ -11,11 +11,9 @@ import type {
} from "@lib/managers/adapters";
import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager";
import type { NodeFile, NodeFolder } from "@/apps/cli/adapters/NodeTypes";
import type { Stats } from "fs";
import * as fs from "fs/promises";
import * as path from "path";
import { watch as chokidarWatch, type FSWatcher } from "chokidar";
import type { IgnoreRules } from "@/apps/cli/serviceModules/IgnoreRules";
import { fsPromises as fs, path, type Stats } from "../node-compat";
/**
* CLI-specific type guard adapter
@@ -101,7 +99,7 @@ class CLIWatchAdapter implements IStorageEventWatchAdapter {
private basePath: string,
private ignoreRules?: IgnoreRules,
private watchEnabled: boolean = false
) {}
) { }
private _toNodeFile(filePath: string, stats: Stats | undefined): NodeFile {
return {

View File

@@ -0,0 +1,13 @@
/* eslint-disable obsidianmd/no-nodejs-builtins */
import * as nodeFs from "node:fs";
import * as nodeFsPromises from "node:fs/promises";
import * as nodePath from "node:path";
import * as nodeReadlinePromises from "node:readline/promises";
import type { Stats } from "node:fs";
export {
nodeFs as fs,
nodeFsPromises as fsPromises,
nodePath as path,
nodeReadlinePromises as readline,
type Stats,
};

View File

@@ -40,7 +40,7 @@
},
"dependencies": {
"chokidar": "^4.0.0",
"minimatch": "^10.2.2",
"minimatch": "^10.2.5",
"octagonal-wheels": "^0.1.46",
"pouchdb-adapter-http": "^9.0.0",
"pouchdb-adapter-leveldb": "^9.0.0",
@@ -55,9 +55,9 @@
"werift": "^0.23.0"
},
"devDependencies": {
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"typescript": "5.9.3",
"vite": "^7.3.1",
"vite": "^8.0.16",
"vitest": "^4.1.8"
}
}

View File

@@ -1,7 +1,5 @@
import * as fs from "fs/promises";
import * as path from "path";
import { minimatch } from "minimatch";
import { fsPromises as fs, path } from "../node-compat";
/**
* Loads and evaluates ignore rules from `.livesync/ignore` inside the vault.

View File

@@ -7,8 +7,7 @@ import type { InjectableDatabaseEventService } from "@lib/services/implements/in
import type { IVaultService } from "@lib/services/base/IService";
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
import { createInstanceLogFunction } from "@lib/services/lib/logUtils";
import * as nodeFs from "node:fs";
import * as nodePath from "node:path";
import { fs as nodeFs, path as nodePath } from "../node-compat";
const NODE_KV_TYPED_KEY = "__nodeKvType";
const NODE_KV_VALUES_KEY = "values";

View File

@@ -1,5 +1,4 @@
import * as nodeFs from "node:fs";
import * as nodePath from "node:path";
import { fs as nodeFs, path as nodePath } from "../node-compat";
type LocalStorageShape = {
getItem(key: string): string | null;

View File

@@ -1,6 +1,5 @@
import type { AppLifecycleService, AppLifecycleServiceDependencies } from "@lib/services/base/AppLifecycleService";
import { ServiceContext } from "@lib/services/base/ServiceBase";
import * as nodePath from "node:path";
import { ConfigServiceBrowserCompat } from "@lib/services/implements/browser/ConfigServiceBrowserCompat";
import { SvelteDialogManagerBase, type ComponentHasResult } from "@lib/services/implements/base/SvelteDialog";
import { UIService } from "@lib/services/implements/base/UIService";
@@ -25,6 +24,7 @@ import { NodeKeyValueDBService } from "./NodeKeyValueDBService";
import { NodeSettingService } from "./NodeSettingService";
import { DatabaseService } from "@lib/services/base/DatabaseService";
import type { ObsidianLiveSyncSettings } from "@lib/common/types";
import { path as nodePath } from "../node-compat";
export class NodeServiceContext extends ServiceContext {
databasePath: string;

View File

@@ -77,7 +77,9 @@ export class BackgroundCliProcess {
if (this.combined.includes(needle)) return;
const status = await Promise.race([
this.child.status.then((s) => ({ type: "status" as const, status: s })),
new Promise<{ type: "tick" }>((resolve) => setTimeout(() => resolve({ type: "tick" }), 100)),
new Promise<{ type: "tick" }>((resolve) =>
setTimeout(() => resolve({ type: "tick" }), 100)
),
]);
if (status.type === "status") {
throw new Error(

View File

@@ -132,7 +132,7 @@ Deno.test("CLI file operations: push / cat / ls / info / rm / resolve / cat-rev
assertEquals(data.path, REMOTE_PATH, "info .path mismatch");
assertEquals(data.filename, REMOTE_PATH.split("/").at(-1), "info .filename mismatch");
assert(typeof data.size === "number" && data.size >= 0, `info .size invalid: ${data.size}`);
assert(typeof data.chunks === "number" && (data.chunks as number) >= 1, `info .chunks invalid: ${data.chunks}`);
assert(typeof data.chunks === "number" && (data.chunks) >= 1, `info .chunks invalid: ${data.chunks}`);
assertEquals(data.conflicts, "N/A", "info .conflicts should be N/A");
console.log("[PASS] info output format matched");
});

View File

@@ -1,11 +1,12 @@
import { LiveSyncWebApp } from "./main";
import { VaultHistoryStore, type VaultHistoryItem } from "./vaultSelector";
import { compatGlobal, _activeDocument } from "@lib/common/coreEnvFunctions.ts";
const historyStore = new VaultHistoryStore();
let app: LiveSyncWebApp | null = null;
function getRequiredElement<T extends HTMLElement>(id: string): T {
const element = document.getElementById(id);
const element = _activeDocument.getElementById(id);
if (!element) {
throw new Error(`Missing element: #${id}`);
}
@@ -22,7 +23,7 @@ function setBusyState(isBusy: boolean): void {
const pickNewBtn = getRequiredElement<HTMLButtonElement>("pick-new-vault");
pickNewBtn.disabled = isBusy;
const historyButtons = document.querySelectorAll<HTMLButtonElement>(".vault-item button");
const historyButtons = _activeDocument.querySelectorAll<HTMLButtonElement>(".vault-item button");
historyButtons.forEach((button) => {
button.disabled = isBusy;
});
@@ -45,24 +46,24 @@ async function renderHistoryList(): Promise<VaultHistoryItem[]> {
emptyEl.classList.toggle("is-hidden", items.length > 0);
for (const item of items) {
const row = document.createElement("div");
const row = _activeDocument.createElement("div");
row.className = "vault-item";
const info = document.createElement("div");
const info = _activeDocument.createElement("div");
info.className = "vault-item-info";
const name = document.createElement("div");
const name = _activeDocument.createElement("div");
name.className = "vault-item-name";
name.textContent = item.name;
const meta = document.createElement("div");
const meta = _activeDocument.createElement("div");
meta.className = "vault-item-meta";
const label = item.id === lastUsedId ? "Last used" : "Used";
meta.textContent = `${label}: ${formatLastUsed(item.lastUsedAt)}`;
info.append(name, meta);
const useButton = document.createElement("button");
const useButton = _activeDocument.createElement("button");
useButton.type = "button";
useButton.textContent = "Use this vault";
useButton.addEventListener("click", () => {
@@ -120,7 +121,7 @@ async function initializeVaultSelector(): Promise<void> {
await renderHistoryList();
}
window.addEventListener("load", async () => {
compatGlobal.addEventListener("load", async () => {
try {
await initializeVaultSelector();
} catch (error) {
@@ -129,11 +130,11 @@ window.addEventListener("load", async () => {
}
});
window.addEventListener("beforeunload", () => {
compatGlobal.addEventListener("beforeunload", () => {
void app?.shutdown();
});
(window as any).livesyncApp = {
(compatGlobal as any).livesyncApp = {
getApp: () => app,
historyStore,
};

View File

@@ -19,6 +19,7 @@ import { SetupManager } from "@/modules/features/SetupManager";
import { useSetupManagerHandlersFeature } from "@/serviceFeatures/setupObsidian/setupManagerHandlers";
import { useP2PReplicatorCommands } from "@lib/replication/trystero/useP2PReplicatorCommands";
import { useP2PReplicatorFeature } from "@lib/replication/trystero/useP2PReplicatorFeature";
import { compatGlobal, _activeDocument } from "@lib/common/coreEnvFunctions.ts";
const SETTINGS_DIR = ".livesync";
const SETTINGS_FILE = "settings.json";
@@ -91,7 +92,7 @@ class LiveSyncWebApp {
console.log("[Settings] Loaded from .livesync/settings.json");
return { ...DEFAULT_SETTINGS, ...data } as ObsidianLiveSyncSettings;
}
} catch (error) {
} catch {
console.log("[Settings] Failed to load, using defaults");
}
return DEFAULT_SETTINGS as ObsidianLiveSyncSettings;
@@ -102,8 +103,8 @@ class LiveSyncWebApp {
console.log("[AppLifecycle] Restart requested");
await this.shutdown();
await this.initialize();
setTimeout(() => {
window.location.reload();
compatGlobal.setTimeout(() => {
compatGlobal.location.reload();
}, 1000);
});
@@ -169,7 +170,7 @@ class LiveSyncWebApp {
const file = await fileHandle.getFile();
const text = await file.text();
return JSON.parse(text);
} catch (error) {
} catch {
// File doesn't exist yet
return null;
}
@@ -235,7 +236,7 @@ class LiveSyncWebApp {
}
private showError(message: string) {
const statusEl = document.getElementById("status");
const statusEl = _activeDocument.getElementById("status");
if (statusEl) {
statusEl.className = "error";
statusEl.textContent = `Error: ${message}`;
@@ -243,7 +244,7 @@ class LiveSyncWebApp {
}
private showWarning(message: string) {
const statusEl = document.getElementById("status");
const statusEl = _activeDocument.getElementById("status");
if (statusEl) {
statusEl.className = "warning";
statusEl.textContent = `Warning: ${message}`;
@@ -251,7 +252,7 @@ class LiveSyncWebApp {
}
private showSuccess(message: string) {
const statusEl = document.getElementById("status");
const statusEl = _activeDocument.getElementById("status");
if (statusEl) {
statusEl.className = "success";
statusEl.textContent = message;

View File

@@ -11,6 +11,7 @@ import type {
} from "@lib/managers/adapters";
import type { FileEventItemSentinel } from "@lib/managers/StorageEventManager";
import type { FSAPIFile, FSAPIFolder } from "@/apps/webapp/adapters/FSAPITypes";
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
/**
* FileSystem API-specific type guard adapter
@@ -149,14 +150,14 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
async beginWatch(handlers: IStorageEventWatchHandlers): Promise<void> {
// Use FileSystemObserver if available (Chrome 124+)
if (typeof (window as any).FileSystemObserver === "undefined") {
if (typeof (compatGlobal as any).FileSystemObserver === "undefined") {
console.log("[FSAPIWatchAdapter] FileSystemObserver not available, file watching disabled");
console.log("[FSAPIWatchAdapter] Consider using Chrome 124+ for real-time file watching");
return Promise.resolve();
}
try {
const FileSystemObserver = (window as any).FileSystemObserver;
const FileSystemObserver = (compatGlobal as any).FileSystemObserver;
this.observer = new FileSystemObserver(async (records: any[]) => {
for (const record of records) {
@@ -181,7 +182,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
if (changedHandle && changedHandle.kind === "file") {
const file = await changedHandle.getFile();
const fileInfo = {
path: relativePath as any,
path: relativePath,
stat: {
size: file.size,
mtime: file.lastModified,
@@ -199,7 +200,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
}
} else if (type === "disappeared") {
const fileInfo = {
path: relativePath as any,
path: relativePath,
stat: {
size: 0,
mtime: Date.now(),
@@ -216,7 +217,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
if (changedHandle && changedHandle.kind === "file") {
const file = await changedHandle.getFile();
const fileInfo = {
path: relativePath as any,
path: relativePath,
stat: {
size: file.size,
mtime: file.lastModified,

View File

@@ -16,11 +16,11 @@
},
"devDependencies": {
"@playwright/test": "^1.58.2",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"playwright": "^1.58.2",
"svelte": "5.41.1",
"svelte": "5.56.3",
"typescript": "5.9.3",
"vite": "^7.3.1",
"vite-plugin-istanbul": "^8.0.0"
"vite": "^8.0.16",
"vite-plugin-istanbul": "^9.0.1"
}
}

View File

@@ -12,6 +12,7 @@
import { LiveSyncWebApp } from "./main";
import type { ObsidianLiveSyncSettings } from "@lib/common/types";
import type { FilePathWithPrefix } from "@lib/common/types";
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
// --------------------------------------------------------------------------
// Internal state one app instance per page / browser context
@@ -41,7 +42,7 @@ async function waitForIdle(core: any, timeoutMs = 60_000): Promise<void> {
(core.services?.fileProcessing?.processing?.value ?? 0) +
(core.services?.replication?.storageApplyingCount?.value ?? 0);
if (q === 0) return;
await new Promise<void>((r) => setTimeout(r, 300));
await new Promise<void>((r) => compatGlobal.setTimeout(r, 300));
}
throw new Error(`waitForIdle timed out after ${timeoutMs} ms`);
}
@@ -116,7 +117,7 @@ export interface LiveSyncTestAPI {
const livesyncTest: LiveSyncTestAPI = {
async init(vaultName: string, settings: Partial<ObsidianLiveSyncSettings>): Promise<void> {
// Clean up any stale OPFS data from previous runs.
const opfsRoot = await navigator.storage.getDirectory();
const opfsRoot = await compatGlobal.navigator.storage.getDirectory();
try {
await opfsRoot.removeEntry(vaultName, { recursive: true });
} catch {
@@ -200,4 +201,4 @@ const livesyncTest: LiveSyncTestAPI = {
};
// Expose on window for Playwright page.evaluate() calls.
(window as any).livesyncTest = livesyncTest;
(compatGlobal as any).livesyncTest = livesyncTest;

View File

@@ -21,7 +21,7 @@
"noFallthroughCasesInSwitch": true,
/* Path mapping */
"baseUrl": ".",
// "baseUrl": ".",
"paths": {
"@/*": ["../../*"],
"@lib/*": ["../../lib/src/*"]

View File

@@ -1,3 +1,5 @@
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
const HANDLE_DB_NAME = "livesync-webapp-handles";
const HANDLE_STORE_NAME = "handles";
const LAST_USED_KEY = "meta:lastUsedVaultId";
@@ -89,7 +91,7 @@ export class VaultHistoryStore {
async getVaultHistory(): Promise<VaultHistoryItem[]> {
return this.withStore("readonly", async (store) => {
const keys = (await this.requestAsPromise(store.getAllKeys())) as IDBValidKey[];
const keys = (await this.requestAsPromise(store.getAllKeys()));
const values = (await this.requestAsPromise(store.getAll())) as unknown[];
const items: VaultHistoryItem[] = [];
for (let i = 0; i < keys.length; i++) {
@@ -170,7 +172,7 @@ export class VaultHistoryStore {
}
async pickNewVault(): Promise<FileSystemDirectoryHandle> {
const picker = (window as any).showDirectoryPicker;
const picker = (compatGlobal as any).showDirectoryPicker;
if (typeof picker !== "function") {
throw new Error("FileSystem API showDirectoryPicker is not supported in this browser");
}

View File

@@ -15,13 +15,13 @@
"octagonal-wheels": "^0.1.46"
},
"devDependencies": {
"eslint-plugin-svelte": "^3.15.0",
"@sveltejs/vite-plugin-svelte": "^6.2.4",
"eslint-plugin-svelte": "^3.19.0",
"@sveltejs/vite-plugin-svelte": "^7.1.2",
"@tsconfig/svelte": "^5.0.8",
"svelte": "5.41.1",
"svelte": "5.56.3",
"svelte-check": "^4.6.0",
"typescript": "5.9.3",
"vite": "^7.3.1"
"vite": "^8.0.16"
},
"imports": {
"../../src/worker/bgWorker.ts": "../../src/worker/bgWorker.mock.ts",

View File

@@ -17,7 +17,9 @@ import {
type PeerStatus,
type PluginShim,
} from "@lib/replication/trystero/P2PReplicatorPaneCommon";
import { P2PLogCollector, type P2PReplicatorBase, useP2PReplicator } from "@lib/replication/trystero/P2PReplicatorCore";
import { useP2PReplicator } from "@lib/replication/trystero/P2PReplicatorCore";
import { P2PLogCollector } from "@lib/replication/trystero/P2PLogCollector";
import type { P2PReplicatorBase } from "@lib/replication/trystero/P2PReplicatorBase.ts";
import type { SimpleStore } from "octagonal-wheels/databases/SimpleStoreBase";
import { reactiveSource } from "octagonal-wheels/dataobject/reactive_v2";
import { EVENT_SETTING_SAVED } from "@lib/events/coreEvents";
@@ -31,6 +33,7 @@ import { SimpleStoreIDBv2 } from "octagonal-wheels/databases/SimpleStoreIDBv2";
import type { BrowserAPIService } from "@lib/services/implements/browser/BrowserAPIService";
import type { InjectableSettingService } from "@lib/services/implements/injectable/InjectableSettingService";
import { LiveSyncTrysteroReplicator } from "@lib/replication/trystero/LiveSyncTrysteroReplicator";
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
function addToList(item: string, list: string) {
return unique(
@@ -137,7 +140,7 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
this._initP2PReplicator();
setTimeout(() => {
compatGlobal.setTimeout(() => {
if (this.settings.P2P_AutoStart && this.settings.P2P_Enabled) {
void this.open();
}
@@ -164,12 +167,12 @@ export class P2PReplicatorShim implements P2PReplicatorBase {
getConfig(key: string) {
const vaultName = this.services.vault.getVaultName();
const dbKey = `${vaultName}-${key}`;
return localStorage.getItem(dbKey);
return compatGlobal.localStorage.getItem(dbKey);
}
setConfig(key: string, value: string) {
const vaultName = this.services.vault.getVaultName();
const dbKey = `${vaultName}-${key}`;
localStorage.setItem(dbKey, value);
compatGlobal.localStorage.setItem(dbKey, value);
}
getDeviceName(): string {

View File

@@ -1,9 +1,10 @@
import { mount } from "svelte";
import "./app.css";
import App from "./App.svelte";
import { _activeDocument } from "@lib/common/coreEnvFunctions.ts";
const app = mount(App, {
target: document.getElementById("app")!,
target: _activeDocument.getElementById("app")!,
});
export default app;

View File

@@ -1,9 +1,10 @@
import { mount } from "svelte";
import "./app.css";
import App from "./UITest.svelte";
import { _activeDocument } from "@lib/common/coreEnvFunctions.ts";
const app = mount(App, {
target: document.getElementById("app")!,
target: _activeDocument.getElementById("app")!,
});
export default app;

View File

@@ -1,7 +1,7 @@
{
"extends": "../../../tsconfig.json",
"compilerOptions": {
"sourceRoot": "../",
// "sourceRoot": "../",
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",

View File

@@ -36,7 +36,7 @@ export async function generateReport(settings: ObsidianLiveSyncSettings, core: L
const r = await requestToCouchDBWithCredentials(
settings.couchDB_URI,
credential,
window.origin,
compatGlobal.origin,
undefined,
undefined,
undefined,

View File

@@ -132,7 +132,7 @@ export const _requestToCouchDBFetch = async (
method?: string
) => {
const utf8str = String.fromCharCode.apply(null, [...writeString(`${username}:${password}`)]);
const encoded = window.btoa(utf8str);
const encoded = compatGlobal.btoa(utf8str);
const authHeader = "Basic " + encoded;
const transformedHeaders: Record<string, string> = {
authorization: authHeader,
@@ -214,7 +214,7 @@ import { BASE_IS_NEW, EVEN, TARGET_IS_NEW } from "@lib/common/models/shared.cons
export { BASE_IS_NEW, EVEN, TARGET_IS_NEW };
// Why 2000? : ZIP FILE Does not have enough resolution.
import { compareMTime } from "@lib/common/utils.ts";
import { _fetch } from "@lib/common/coreEnvFunctions.ts";
import { _fetch, compatGlobal } from "@lib/common/coreEnvFunctions.ts";
export { compareMTime };
function getKey(file: AnyEntry | string | UXFileInfoStub) {
const key = typeof file == "string" ? file : stripAllPrefixes(file.path);

View File

@@ -16,9 +16,11 @@ export class PluginDialogModal extends Modal {
override onOpen() {
const { contentEl } = this;
this.contentEl.style.overflow = "auto";
this.contentEl.style.display = "flex";
this.contentEl.style.flexDirection = "column";
this.contentEl.setCssStyles({
overflow: "auto",
display: "flex",
flexDirection: "column",
});
this.titleEl.setText("Customization Sync (Beta3)");
if (!this.component) {
this.component = mount(PluginPane, {

Submodule src/lib updated: 2accfbce49...c926417f82

View File

@@ -182,9 +182,9 @@ export class ModuleConflictResolver extends AbstractModule {
revs.map(async (rev) => {
const leaf = await this.core.databaseFileAccess.fetchEntryMeta(filename, rev);
if (leaf == false) {
return [0, rev] as [number, string];
return [0, rev];
}
return [leaf.mtime, rev] as [number, string];
return [leaf.mtime, rev];
})
)),
] as [number, string][]

View File

@@ -192,8 +192,10 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
const { contentEl } = this;
this.titleEl.setText(this.title);
const div = contentEl.createDiv();
div.style.userSelect = "text";
div.style["webkitUserSelect"] = "text";
div.setCssStyles({
userSelect: "text",
webkitUserSelect: "text",
});
void MarkdownRenderer.render(this.plugin.app, this.contentMd, div, "/", this.plugin);
const buttonSetting = new Setting(contentEl);
const labelWrapper = contentEl.createDiv();
@@ -202,21 +204,23 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
labelEl.addClass("sls-dialogue-note-countdown");
if (!this.timeout || !this.timer) {
labelWrapper.empty();
labelWrapper.style.display = "none";
labelWrapper.setCssStyles({ display: "none" });
}
buttonSetting.infoEl.style.display = "none";
buttonSetting.controlEl.style.flexWrap = "wrap";
buttonSetting.infoEl.setCssStyles({ display: "none" });
buttonSetting.controlEl.setCssStyles({ flexWrap: "wrap" });
if (this.wideButton) {
buttonSetting.controlEl.style.flexDirection = "column";
buttonSetting.controlEl.style.alignItems = "center";
buttonSetting.controlEl.style.justifyContent = "center";
buttonSetting.controlEl.style.flexGrow = "1";
buttonSetting.controlEl.setCssStyles({
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
flexGrow: "1",
});
}
contentEl.addEventListener("click", () => {
if (this.timer) {
labelWrapper.empty();
labelWrapper.style.display = "none";
labelWrapper.setCssStyles({ display: "none" });
compatGlobal.clearInterval(this.timer);
this.timer = undefined;
this.defaultButtonComponent?.setButtonText(`${this.defaultAction}`);
@@ -238,8 +242,10 @@ export class MessageBox<T extends readonly string[]> extends AutoClosableModal {
btn.setCta();
}
if (this.wideButton) {
btn.buttonEl.style.flexGrow = "1";
btn.buttonEl.style.width = "100%";
btn.buttonEl.setCssStyles({
flexGrow: "1",
width: "100%",
});
}
return btn;
});

View File

@@ -7,6 +7,8 @@ import { FetchHttpHandler, type FetchHttpHandlerOptions } from "@smithy/fetch-ht
import { HttpRequest, HttpResponse, type HttpHandlerOptions } from "@smithy/protocol-http";
import { buildQueryString } from "@smithy/querystring-builder";
import { requestUrl, type RequestUrlParam } from "@/deps.ts";
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
////////////////////////////////////////////////////////////////////////////////
// special handler using Obsidian requestUrl
////////////////////////////////////////////////////////////////////////////////
@@ -14,7 +16,7 @@ import { requestUrl, type RequestUrlParam } from "@/deps.ts";
function requestTimeout(timeoutInMs: number = 0): Promise<never> {
return new Promise((_, reject) => {
if (timeoutInMs) {
window.setTimeout(() => {
compatGlobal.setTimeout(() => {
const timeoutError = new Error(`Request did not complete within ${timeoutInMs} ms`);
timeoutError.name = "TimeoutError";
reject(timeoutError);

View File

@@ -63,12 +63,12 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
// eslint-disable-next-line @typescript-eslint/no-this-alias
const _this = this;
//@ts-ignore
if (!window.CodeMirrorAdapter) {
if (!compatGlobal.CodeMirrorAdapter) {
this._log("CodeMirrorAdapter is not available");
return;
}
//@ts-ignore
window.CodeMirrorAdapter.commands.save = () => {
compatGlobal.CodeMirrorAdapter.commands.save = () => {
//@ts-ignore
void _this.app.commands.executeCommandById("editor:save-file");
// _this.app.performCommand('editor:save-file');
@@ -86,14 +86,14 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
// Already bound
// eslint-disable-next-line @typescript-eslint/unbound-method
this.plugin.registerDomEvent(activeDocument, "visibilitychange", this.watchWindowVisibility);
this.plugin.registerDomEvent(window, "focus", () => this.setHasFocus(true));
this.plugin.registerDomEvent(window, "blur", () => this.setHasFocus(false));
this.plugin.registerDomEvent(compatGlobal, "focus", () => this.setHasFocus(true));
this.plugin.registerDomEvent(compatGlobal, "blur", () => this.setHasFocus(false));
// Already bound
// eslint-disable-next-line @typescript-eslint/unbound-method
this.plugin.registerDomEvent(window, "online", this.watchOnline);
this.plugin.registerDomEvent(compatGlobal, "online", this.watchOnline);
// Already bound
// eslint-disable-next-line @typescript-eslint/unbound-method
this.plugin.registerDomEvent(window, "offline", this.watchOnline);
this.plugin.registerDomEvent(compatGlobal, "offline", this.watchOnline);
}
hasFocus = true;
@@ -114,7 +114,7 @@ export class ModuleObsidianEvents extends AbstractObsidianModule {
async watchOnlineAsync() {
// If some files were failed to retrieve, scan files again.
// TODO:FIXME AT V0.17.31, this logic has been disabled.
if (navigator.onLine && this.localDatabase.needScanning) {
if (compatGlobal.navigator.onLine && this.localDatabase.needScanning) {
this.localDatabase.needScanning = false;
await this.services.vault.scanVault();
}

View File

@@ -367,10 +367,10 @@ export class DocumentHistoryModal extends Modal {
*/
updateDiffNavVisibility() {
if (this.diffNavContainer) {
this.diffNavContainer.style.display = this.showDiff ? "flex" : "none";
this.diffNavContainer.setCssStyles({ display: this.showDiff ? "flex" : "none" });
}
if (this.diffOnlyLabel) {
this.diffOnlyLabel.style.display = this.showDiff ? "inline-block" : "none";
this.diffOnlyLabel.setCssStyles({ display: this.showDiff ? "inline-block" : "none" });
}
}
@@ -573,13 +573,13 @@ export class DocumentHistoryModal extends Modal {
});
diffOnlyLabel.appendText("Diff only");
diffOnlyLabel.addClass("diff-only-label");
diffOnlyLabel.style.display = this.showDiff ? "inline-block" : "none";
diffOnlyLabel.setCssStyles({ display: this.showDiff ? "inline-block" : "none" });
this.diffOnlyLabel = diffOnlyLabel;
// Diff navigation buttons
this.diffNavContainer = diffOptionsRow.createDiv("");
this.diffNavContainer.addClass("diff-nav");
this.diffNavContainer.style.display = this.showDiff ? "flex" : "none";
this.diffNavContainer.setCssStyles({ display: this.showDiff ? "flex" : "none" });
this.diffNavContainer.createEl("button", { text: "\u25B2 Prev" }, (e) => {
e.addClass("diff-nav-btn");
@@ -608,7 +608,7 @@ export class DocumentHistoryModal extends Modal {
e.addClass("mod-cta");
e.addEventListener("click", () => {
fireAndForget(async () => {
await navigator.clipboard.writeText(this.currentText);
await compatGlobal.navigator.clipboard.writeText(this.currentText);
Logger(`Old content copied to clipboard`, LOG_LEVEL_NOTICE);
});
});

View File

@@ -316,7 +316,7 @@ export class ModuleLog extends AbstractObsidianModule {
const showStatusOnEditor = this.settings?.showStatusOnEditor ?? false;
if (this.statusDiv) {
this.statusDiv.style.display = showStatusOnEditor ? "" : "none";
this.statusDiv.setCssStyles({ display: showStatusOnEditor ? "" : "none" });
}
if (!showStatusOnEditor) {
this.messageArea.innerText = "";
@@ -351,7 +351,7 @@ export class ModuleLog extends AbstractObsidianModule {
});
}
nextFrameQueue: ReturnType<typeof requestAnimationFrame> | undefined = undefined;
nextFrameQueue: ReturnType<typeof compatGlobal.requestAnimationFrame> | undefined = undefined;
logLines: { ttl: number; message: string }[] = [];
applyStatusBarText() {
@@ -371,7 +371,7 @@ export class ModuleLog extends AbstractObsidianModule {
this.statusBar?.setText(newMsg.split("\n")[0]);
if (this.statusDiv) {
this.statusDiv.style.display = this.settings?.showStatusOnEditor ? "" : "none";
this.statusDiv.setCssStyles({ display: this.settings?.showStatusOnEditor ? "" : "none" });
}
if (this.settings?.showStatusOnEditor && this.statusDiv) {
if (this.settings.showLongerLogInsideEditor) {
@@ -472,7 +472,7 @@ ${stringifyYaml(info)}
this.messageArea = this.statusDiv.createDiv({ cls: "livesync-status-messagearea" });
this.logMessage = this.statusDiv.createDiv({ cls: "livesync-status-logmessage" });
this.logHistory = this.statusDiv.createDiv({ cls: "livesync-status-loghistory" });
this.statusDiv.style.display = this.settings?.showStatusOnEditor ? "" : "none";
this.statusDiv.setCssStyles({ display: this.settings?.showStatusOnEditor ? "" : "none" });
}
eventHub.onEvent(EVENT_LAYOUT_READY, () => this.adjustStatusDivPosition());
if (this.settings?.showStatusOnStatusbar) {

View File

@@ -133,7 +133,7 @@ export function paneSetup(
cls: "sls-troubleshoot-preview",
});
const loadMarkdownPage = async (pathAll: string, basePathParam: string = "") => {
troubleShootEl.style.minHeight = troubleShootEl.clientHeight + "px";
troubleShootEl.setCssStyles({ minHeight: troubleShootEl.clientHeight + "px" });
troubleShootEl.empty();
const fullPath = pathAll.startsWith("/") ? pathAll : `${basePathParam}/${pathAll}`;
@@ -201,7 +201,7 @@ export function paneSetup(
});
});
});
troubleShootEl.style.minHeight = "";
troubleShootEl.setCssStyles({ minHeight: "" });
};
void loadMarkdownPage(topPath);
});

View File

@@ -5,6 +5,7 @@ import type { ObsidianLiveSyncSettings } from "@lib/common/types";
import { fireAndForget, parseHeaderValues } from "@lib/common/utils";
import { isCloudantURI } from "@lib/pouchdb/utils_couchdb";
import { generateCredentialObject } from "@lib/replication/httplib";
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
export const checkConfig = async (
checkResultDiv: HTMLDivElement | undefined,
@@ -35,7 +36,7 @@ export const checkConfig = async (
const r = await requestToCouchDBWithCredentials(
editingSettings.couchDB_URI,
credential,
window.origin,
compatGlobal.origin,
undefined,
undefined,
undefined,
@@ -218,7 +219,7 @@ export const checkConfig = async (
isSuccessful = false;
}
addResult($msg("obsidianLiveSyncSettingTab.msgConnectionCheck"), ["ob-btn-config-head"]);
addResult($msg("obsidianLiveSyncSettingTab.msgCurrentOrigin", { origin: window.location.origin }));
addResult($msg("obsidianLiveSyncSettingTab.msgCurrentOrigin", { origin: compatGlobal.location.origin }));
// Request header check
const origins = ["app://obsidian.md", "capacitor://localhost", "http://localhost"];

View File

@@ -5,6 +5,8 @@ import type { ObsidianLiveSyncSettings } from "@lib/common/types";
import { parseHeaderValues } from "@lib/common/utils";
import { isCloudantURI } from "@lib/pouchdb/utils_couchdb";
import { generateCredentialObject } from "@lib/replication/httplib";
import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
export type ResultMessage = { message: string; classes: string[] };
export type ResultErrorMessage = { message: string; result: "error"; classes: string[] };
export type ResultOk = { message: string; result: "ok"; value?: any };
@@ -93,7 +95,7 @@ export const checkConfig = async (editingSettings: ObsidianLiveSyncSettings) =>
const r = await requestToCouchDBWithCredentials(
editingSettings.couchDB_URI,
credential,
window.origin,
compatGlobal.origin,
undefined,
undefined,
undefined,
@@ -239,7 +241,7 @@ export const checkConfig = async (editingSettings: ObsidianLiveSyncSettings) =>
);
}
addMessage($msg("obsidianLiveSyncSettingTab.msgConnectionCheck"), ["ob-btn-config-head"]);
addMessage($msg("obsidianLiveSyncSettingTab.msgCurrentOrigin", { origin: window.location.origin }));
addMessage($msg("obsidianLiveSyncSettingTab.msgCurrentOrigin", { origin: compatGlobal.location.origin }));
// Request header check
const origins = ["app://obsidian.md", "capacitor://localhost", "http://localhost"];

114
utilsdeno/README.md Normal file
View File

@@ -0,0 +1,114 @@
# Refactoring and Code Quality Utilities
This directory contains Deno-based scripts that utilise `ts-morph` to perform codebase-wide refactoring, code quality clean-up, and static analysis.
These utilities are designed to help maintain code quality, resolve compiler warnings, and ensure popout window compatibility in the Obsidian plug-in environment.
---
## Prerequisites
To execute these scripts, you must have Deno installed on your system.
---
## General Usage
By default, all refactoring scripts run in **dry-run mode**. They will output the proposed changes to the console without modifying any files.
To apply the changes to the files, append the `'--run'` flag:
```bash
deno run --allow-read --allow-write --allow-env <script_name>.ts --run
```
---
## Utilities Reference
### 1. Global Wrapper Refactoring (`refactor-globals.ts`)
Converts standard global variable usages to compatibility wrappers to ensure safe operation when running in Obsidian popout windows (which run in separate window contexts).
* **Targets**: `setTimeout`, `clearTimeout`, `setInterval`, `clearInterval`, `requestAnimationFrame`, `cancelAnimationFrame`, `localStorage`, `navigator`, `location`, `window`, `globalThis`, and `document`.
* **Actions**:
* Replaces global namespace references (like `window` and `globalThis`) with `compatGlobal`.
* Replaces `document` with `_activeDocument` (from `@lib/common/coreEnvFunctions.ts`).
* Injects or updates the necessary imports in modified files.
* **Command**:
```bash
deno run --allow-read --allow-write --allow-env refactor-globals.ts
```
### 2. Element Style Normalisation (`refactor-styles.ts`)
Converts direct style assignments on HTML/SVG elements to use the plug-in's `setCssStyles` helper.
* **Actions**:
* Replaces statements like `element.style.color = 'red';` with `element.setCssStyles({ color: 'red' });`.
* Groups multiple consecutive style assignments on the same element into a single call.
* Supports both static keys and computed bracket properties.
* **Command**:
```bash
deno run --allow-read --allow-write --allow-env refactor-styles.ts
```
### 3. Redundant Assertions Cleanup (`refactor-assertions.ts`)
Finds and removes type assertions that are redundant because the expression already evaluates to the asserted type.
* **Actions**:
* Removes redundant `as Type` or `<Type>` assertions.
* Preserves critical literal assertions such as `as const` and `<const>`.
* **Command**:
```bash
deno run --allow-read --allow-write --allow-env refactor-assertions.ts
```
### 4. Unused Code Refactoring (`refactor-unused.ts`)
Cleans up unused imports and catch variables to reduce bundle size and warnings.
* **Actions**:
* Converts unused catch variables to simple catch statements (e.g. `catch (error)` -> `catch`).
* Removes unused items in named imports, handling alias bindings (e.g. `import { A as B }`) correctly.
* Deletes empty import declarations resulting from the named import clean-up.
* **Command**:
```bash
deno run --allow-read --allow-write --allow-env refactor-unused.ts
```
### 5. Explicit Any Detection (`detect-any.ts`)
Scans the codebase and logs all occurrences of explicit `any` types.
* **Actions**:
* Identifies uses of the `any` keyword in TypeScript and Svelte files.
* Logs the filename, line number, and matching code line for audit purposes.
* **Command**:
```bash
deno run --allow-read --allow-env detect-any.ts
```
### 6. Import Normalisation (`normalise-imports.ts`)
Ensures that all import statements are standardised across the codebase, resolving paths to aliases such as `@lib/` and `@/` where applicable.
* **Command**:
```bash
deno run --allow-read --allow-write --allow-env normalise-imports.ts
```
### 7. CLI Node.js Import Redirection (`refactor-cli-node-imports.ts`)
Redirects direct Node.js built-in module imports (like `fs` and `path`) within the CLI codebase to use a single barrel file (`src/apps/cli/node-compat.ts`).
* **Actions**:
* Finds imports of Node.js built-in APIs (`fs`, `fs/promises`, `path`, and `readline/promises`) in CLI source files.
* Replaces them with imports from the local `node-compat.ts` barrel file.
* This eliminates duplicate browser-targeted linter warnings on Node.js built-ins in the CLI workspace, keeping linter ignores consolidated.
* **Command**:
```bash
deno run --allow-read --allow-write --allow-env refactor-cli-node-imports.ts
```
---
## Safety and Exclusions
* **Tests Excluded**: All scripts automatically skip files located in `_test/` or `testdeno/` folders, as well as files ending with `.spec.ts` or `.test.ts`.
* **Submodule Caution**: Some tools will run against the `src/lib/` submodule. Ensure you verify changes inside the submodule prior to committing.
* **Verification**: Always run `npm run check` and `npm run test:unit` after performing refactoring tasks to verify that type safety and tests remain intact.

49
utilsdeno/detect-any.ts Normal file
View File

@@ -0,0 +1,49 @@
// Detect explicit usage of 'any' type in the codebase.
// Use this script by running `deno run --allow-read --allow-env detect-any.ts` from the utilsdeno directory.
import { Project, SyntaxKind } from "npm:ts-morph";
import path from "node:path";
import { fileURLToPath } from "node:url";
const project = new Project({ tsConfigFilePath: "../tsconfig.json" });
project.addSourceFilesAtPaths("../src/**/*.ts");
project.addSourceFilesAtPaths("../src/**/*.svelte");
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, "..");
function toPosixPath(filePath: string): string {
return filePath.replace(/\\/g, "/");
}
const posixProjectRoot = toPosixPath(projectRoot);
const posixSrc = `${posixProjectRoot}/src`;
let anyCount = 0;
for (const sourceFile of project.getSourceFiles()) {
const filePath = sourceFile.getFilePath();
const posixFilePath = toPosixPath(filePath);
if (!posixFilePath.startsWith(posixSrc)) continue;
if (
posixFilePath.includes("/_test/") ||
posixFilePath.endsWith(".spec.ts") ||
posixFilePath.endsWith(".test.ts")
) {
continue;
}
const anyNodes = sourceFile.getDescendantsOfKind(SyntaxKind.AnyKeyword);
if (anyNodes.length > 0) {
console.log(`File: ${posixFilePath.slice(posixProjectRoot.length + 1)}`);
for (const anyNode of anyNodes) {
const { line } = sourceFile.getLineAndColumnAtPos(anyNode.getStart());
const lineText = sourceFile.getFullText().split(/\r?\n/)[line - 1];
console.log(` Line ${line}: ${lineText.trim()}`);
anyCount++;
}
}
}
console.log(`\nTotal explicit 'any' usages found: ${anyCount}`);

View File

@@ -0,0 +1,96 @@
// Refactor unnecessary type assertions (e.g. `expr as Type` where type of `expr` is already `Type`).
// Use this script by running `deno run --allow-read --allow-write --allow-env refactor-assertions.ts` from the utilsdeno directory.
// Run with --run flag to apply changes.
import { Project, SyntaxKind, Node } from "npm:ts-morph";
import path from "node:path";
import { fileURLToPath } from "node:url";
const isDryRun = !Deno.args.includes("--run");
if (isDryRun) {
console.log("=== DRY RUN MODE ===");
console.log(
"To apply changes, run with: deno run --allow-read --allow-write --allow-env refactor-assertions.ts --run\n"
);
} else {
console.log("=== RUN MODE: WILL MODIFY FILES ===");
}
const project = new Project({ tsConfigFilePath: "../tsconfig.json" });
project.addSourceFilesAtPaths("../src/**/*.ts");
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, "..");
function toPosixPath(filePath: string): string {
return filePath.replace(/\\/g, "/");
}
const posixProjectRoot = toPosixPath(projectRoot);
const posixSrc = `${posixProjectRoot}/src`;
let modifiedFilesCount = 0;
for (const sourceFile of project.getSourceFiles()) {
const filePath = sourceFile.getFilePath();
const posixFilePath = toPosixPath(filePath);
if (!posixFilePath.startsWith(posixSrc)) continue;
if (posixFilePath.includes("/_test/") || posixFilePath.endsWith(".spec.ts") || posixFilePath.endsWith(".test.ts")) continue;
// Find AsExpression (expr as Type) and TypeAssertion (<Type>expr)
const asExpressions = sourceFile.getDescendantsOfKind(SyntaxKind.AsExpression);
const typeAssertions = sourceFile.getDescendantsOfKind(SyntaxKind.TypeAssertion);
const allAssertions = [...asExpressions, ...typeAssertions];
const nodesToRemove: Node[] = [];
for (const node of allAssertions) {
const expr = node.getExpression();
const exprType = expr.getType();
const assertType = node.getType();
// Skip `as const` or `<const>` assertions
const typeNode = (node as any).getTypeNode?.();
if (typeNode && typeNode.getText() === "const") {
continue;
}
// Compare type texts to find redundant assertions
const exprTypeText = exprType.getText();
const assertTypeText = assertType.getText();
if (exprTypeText === assertTypeText) {
nodesToRemove.push(node);
}
}
if (nodesToRemove.length > 0) {
console.log(`File: ${posixFilePath.slice(posixProjectRoot.length + 1)}`);
// Reverse nodes order to keep indices/references valid when modifying
const sortedNodes = [...nodesToRemove].sort((a, b) => b.getStart() - a.getStart());
for (const node of sortedNodes) {
const { line } = sourceFile.getLineAndColumnAtPos(node.getStart());
const exprText = node.getExpression().getText();
console.log(` Line ${line}: "${node.getText()}" -> "${exprText}"`);
if (!isDryRun) {
node.replaceWithText(exprText);
}
}
modifiedFilesCount++;
}
}
console.log(`\nTotal files to modify: ${modifiedFilesCount}`);
if (!isDryRun) {
project.saveSync();
console.log("All changes successfully saved.");
} else {
console.log("Dry run complete. No changes were written to files.");
}

View File

@@ -0,0 +1,132 @@
// Refactor Node.js imports in the CLI application to use the barrel compatibility file.
// Use this script by running `deno run --allow-read --allow-write --allow-env refactor-cli-node-imports.ts` from the utilsdeno directory.
// Run with --run flag to apply changes.
import { Project, SyntaxKind, Node } from "npm:ts-morph";
import path from "node:path";
import { fileURLToPath } from "node:url";
const isDryRun = !Deno.args.includes("--run");
if (isDryRun) {
console.log("=== DRY RUN MODE ===");
console.log(
"To apply changes, run with: deno run --allow-read --allow-write --allow-env refactor-cli-node-imports.ts --run\n"
);
} else {
console.log("=== RUN MODE: WILL MODIFY FILES ===");
}
const project = new Project({ tsConfigFilePath: "../tsconfig.json" });
project.addSourceFilesAtPaths("../src/apps/cli/**/*.ts");
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, "..");
const nodeCompatPath = path.resolve(projectRoot, "src", "apps", "cli", "node-compat.ts");
function toPosixPath(filePath: string): string {
return filePath.replace(/\\/g, "/");
}
const posixProjectRoot = toPosixPath(projectRoot);
const posixSrc = `${posixProjectRoot}/src`;
function getRelativeImportPath(fromFile: string, toFile: string): string {
let rel = path.relative(path.dirname(fromFile), toFile);
rel = rel.replace(/\\/g, "/");
if (!rel.startsWith(".") && !rel.startsWith("/")) {
rel = "./" + rel;
}
if (rel.endsWith(".ts")) {
rel = rel.slice(0, -3);
}
return rel;
}
let modifiedFilesCount = 0;
for (const sourceFile of project.getSourceFiles()) {
const filePath = sourceFile.getFilePath();
const posixFilePath = toPosixPath(filePath);
// Only process CLI source files under src/apps/cli/
if (!posixFilePath.includes("/src/apps/cli/")) continue;
if (
posixFilePath.endsWith("node-compat.ts") ||
posixFilePath.endsWith("vite.config.ts") ||
posixFilePath.endsWith(".spec.ts") ||
posixFilePath.endsWith(".test.ts") ||
posixFilePath.includes("/_test/") ||
posixFilePath.includes("/testdeno/") ||
posixFilePath.includes("/test/")
) {
continue;
}
const importDeclarations = sourceFile.getImportDeclarations();
const targetImports: any[] = [];
const namedImportsToAdd: string[] = [];
for (const impDecl of importDeclarations) {
const specifier = impDecl.getModuleSpecifierValue();
// Check if it's a Node.js built-in module we want to redirect
let exportedName = "";
if (specifier === "fs/promises" || specifier === "node:fs/promises") {
exportedName = "fsPromises";
} else if (specifier === "fs" || specifier === "node:fs") {
exportedName = "fs";
} else if (specifier === "path" || specifier === "node:path") {
exportedName = "path";
} else if (specifier === "node:readline/promises") {
exportedName = "readline";
}
if (exportedName) {
const localName = impDecl.getNamespaceImport()?.getText() || impDecl.getDefaultImport()?.getText();
if (localName) {
targetImports.push({ impDecl, exportedName, localName });
}
}
}
if (targetImports.length > 0) {
console.log(`File: ${posixFilePath.slice(posixProjectRoot.length + 1)}`);
for (const { impDecl, exportedName, localName } of targetImports) {
const { line } = sourceFile.getLineAndColumnAtPos(impDecl.getStart());
console.log(` Line ${line}: Redirecting "${impDecl.getText()}"`);
if (exportedName === localName) {
namedImportsToAdd.push(exportedName);
} else {
namedImportsToAdd.push(`${exportedName} as ${localName}`);
}
if (!isDryRun) {
impDecl.remove();
}
}
const relImportPath = getRelativeImportPath(filePath, nodeCompatPath);
console.log(` Adding: import { ${namedImportsToAdd.join(", ")} } from "${relImportPath}"`);
if (!isDryRun) {
sourceFile.addImportDeclaration({
namedImports: namedImportsToAdd,
moduleSpecifier: relImportPath,
});
}
modifiedFilesCount++;
}
}
console.log(`\nTotal files to modify: ${modifiedFilesCount}`);
if (!isDryRun) {
project.saveSync();
console.log("All changes successfully saved.");
} else {
console.log("Dry run complete. No changes were written to files.");
}

View File

@@ -0,0 +1,226 @@
// Refactor global variables (setTimeout, document, navigator, etc.) to use compatGlobal.
// Use this script by running `deno run --allow-read --allow-write --allow-run refactor-globals.ts` from the utilsdeno directory.
// Run with --run flag to apply changes.
import { Project, SyntaxKind, Node } from "npm:ts-morph";
import path from "node:path";
import { fileURLToPath } from "node:url";
const isDryRun = !Deno.args.includes("--run");
if (isDryRun) {
console.log("=== DRY RUN MODE ===");
console.log(
"To apply changes, run with: deno run --allow-read --allow-write --allow-run refactor-globals.ts --run\n"
);
} else {
console.log("=== RUN MODE: WILL MODIFY FILES ===");
}
const project = new Project({ tsConfigFilePath: "../tsconfig.json" });
// Manually add files under src/ to ensure those excluded by tsconfig.json are processed if needed.
project.addSourceFilesAtPaths("../src/**/*.ts");
project.addSourceFilesAtPaths("../src/**/*.svelte");
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, "..");
function toPosixPath(filePath: string): string {
return filePath.replace(/\\/g, "/");
}
const posixProjectRoot = toPosixPath(projectRoot);
const posixSrc = `${posixProjectRoot}/src`;
const posixLibSrc = `${posixProjectRoot}/src/lib`;
const TARGET_GLOBALS = new Set([
"setTimeout",
"clearTimeout",
"setInterval",
"clearInterval",
"requestAnimationFrame",
"cancelAnimationFrame",
"localStorage",
"navigator",
"location",
"document",
"window",
]);
let modifiedFilesCount = 0;
for (const sourceFile of project.getSourceFiles()) {
const filePath = sourceFile.getFilePath();
const posixFilePath = toPosixPath(filePath);
// Only process files inside the project src directory.
if (!posixFilePath.startsWith(posixSrc)) {
continue;
}
// Exclude coreEnvFunctions.ts to avoid self-referential definitions
if (posixFilePath.endsWith("/coreEnvFunctions.ts") || posixFilePath.endsWith("/coreEnvFunctions")) {
continue;
}
// Exclude unit and integration test files
if (
posixFilePath.endsWith(".spec.ts") ||
posixFilePath.endsWith(".test.ts") ||
posixFilePath.includes("/_test/") ||
posixFilePath.includes("/testdeno/")
) {
continue;
}
// Collect all identifier nodes
const identifiers = sourceFile.getDescendantsOfKind(SyntaxKind.Identifier);
const nodesToReplace: { node: Node; replacement: string }[] = [];
for (const idNode of identifiers) {
const name = idNode.getText();
if (!TARGET_GLOBALS.has(name)) {
continue;
}
const parent = idNode.getParent();
if (!parent) {
continue;
}
// 1. Skip if it is the property name in a PropertyAccessExpression (e.g. the "setTimeout" in "obj.setTimeout")
if (parent.getKind() === SyntaxKind.PropertyAccessExpression) {
const propAccess = parent.asKindOrThrow(SyntaxKind.PropertyAccessExpression);
if (propAccess.getNameNode() === idNode) {
continue;
}
}
// 1.5. Skip if it is the right-hand side of a QualifiedName (e.g. the "requestAnimationFrame" in "typeof compatGlobal.requestAnimationFrame")
if (parent.getKind() === SyntaxKind.QualifiedName) {
const qualified = parent.asKindOrThrow(SyntaxKind.QualifiedName);
if (qualified.getRight() === idNode) {
continue;
}
}
// 2. Skip if it is the operand of a typeof expression (e.g. "typeof window")
if (parent.getKind() === SyntaxKind.TypeOfExpression) {
continue;
}
// 3. Skip if it is a declaration name node
const kind = parent.getKind();
if (
kind === SyntaxKind.VariableDeclaration ||
kind === SyntaxKind.Parameter ||
kind === SyntaxKind.FunctionDeclaration ||
kind === SyntaxKind.MethodDeclaration ||
kind === SyntaxKind.PropertyDeclaration ||
kind === SyntaxKind.ClassDeclaration ||
kind === SyntaxKind.InterfaceDeclaration ||
kind === SyntaxKind.TypeAliasDeclaration ||
kind === SyntaxKind.ImportSpecifier ||
kind === SyntaxKind.ExportSpecifier ||
kind === SyntaxKind.MethodSignature ||
kind === SyntaxKind.PropertySignature ||
kind === SyntaxKind.PropertyAssignment
) {
if ((parent as any).getNameNode?.() === idNode || (parent as any).getName?.() === name) {
continue;
}
}
// 4. Verify it is a global variable reference using definitions
let isGlobal = false;
try {
const definitions = idNode.getDefinitions();
isGlobal =
definitions.length === 0 ||
definitions.every((def) => {
const sf = def.getSourceFile();
if (!sf) return true;
const path = sf.getFilePath();
return path.includes("node_modules/typescript/lib/") || path.includes("node_modules/@types/");
});
} catch (_err) {
// If checking definitions fails, assume it is local/imported to be safe
isGlobal = false;
}
if (!isGlobal) {
continue;
}
// Determine replacement
let replacement = "";
if (name === "window" || name === "globalThis") {
replacement = "compatGlobal";
} else if (name === "document") {
replacement = "_activeDocument";
} else {
replacement = `compatGlobal.${name}`;
}
nodesToReplace.push({ node: idNode, replacement });
}
if (nodesToReplace.length > 0) {
console.log(`File: ${posixFilePath.slice(posixProjectRoot.length + 1)}`);
for (const { node, replacement } of nodesToReplace) {
const { line } = sourceFile.getLineAndColumnAtPos(node.getStart());
console.log(` Line ${line}: "${node.getText()}" -> "${replacement}"`);
}
if (!isDryRun) {
// Apply replacements
// Note: replaceWithText changes AST, so we replace them directly
for (const { node, replacement } of nodesToReplace) {
node.replaceWithText(replacement);
}
// Determine what needs to be imported based on replacements
const needsCompatGlobal = nodesToReplace.some((r) => r.replacement.includes("compatGlobal"));
const needsActiveDocument = nodesToReplace.some((r) => r.replacement.includes("_activeDocument"));
const requiredImports: string[] = [];
if (needsCompatGlobal) requiredImports.push("compatGlobal");
if (needsActiveDocument) requiredImports.push("_activeDocument");
if (requiredImports.length > 0) {
const existingImport = sourceFile.getImportDeclarations().find((imp) => {
const spec = imp.getModuleSpecifierValue();
return spec === "@lib/common/coreEnvFunctions" || spec === "@lib/common/coreEnvFunctions.ts";
});
if (existingImport) {
for (const nameToImport of requiredImports) {
const alreadyImported = existingImport
.getNamedImports()
.some((ni) => ni.getName() === nameToImport);
if (!alreadyImported) {
existingImport.addNamedImport(nameToImport);
}
}
} else {
sourceFile.addImportDeclaration({
namedImports: requiredImports,
moduleSpecifier: "@lib/common/coreEnvFunctions.ts",
});
}
}
}
modifiedFilesCount++;
}
}
console.log(`\nTotal files to modify: ${modifiedFilesCount}`);
if (!isDryRun) {
project.saveSync();
console.log("All changes successfully saved.");
} else {
console.log("Dry run complete. No changes were written to files.");
}

View File

@@ -0,0 +1,222 @@
// Refactor element.style.XXXX = YYYY to element.setCssStyles({ XXXX: YYYY }).
// Use this script by running `deno run --allow-read --allow-write --allow-run refactor-styles.ts` from the utilsdeno directory.
// Run with --run flag to apply changes.
import { Project, SyntaxKind, Node, Expression } from "npm:ts-morph";
import path from "node:path";
import { fileURLToPath } from "node:url";
const isDryRun = !Deno.args.includes("--run");
if (isDryRun) {
console.log("=== DRY RUN MODE ===");
console.log(
"To apply changes, run with: deno run --allow-read --allow-write --allow-run refactor-styles.ts --run\n"
);
} else {
console.log("=== RUN MODE: WILL MODIFY FILES ===");
}
const project = new Project({ tsConfigFilePath: "../tsconfig.json" });
// Manually add files under src/ to ensure those excluded by tsconfig.json are processed if needed.
project.addSourceFilesAtPaths("../src/**/*.ts");
project.addSourceFilesAtPaths("../src/**/*.svelte");
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, "..");
function toPosixPath(filePath: string): string {
return filePath.replace(/\\/g, "/");
}
const posixProjectRoot = toPosixPath(projectRoot);
const posixSrc = `${posixProjectRoot}/src`;
const posixLibSrc = `${posixProjectRoot}/src/lib`;
function matchStyleAccess(node: Node): { element: Node; propertyName: string; isComputed: boolean } | undefined {
if (Node.isPropertyAccessExpression(node)) {
const expr = node.getExpression();
if (Node.isPropertyAccessExpression(expr) && expr.getName() === "style") {
return {
element: expr.getExpression(),
propertyName: node.getName(),
isComputed: false,
};
}
} else if (Node.isElementAccessExpression(node)) {
const expr = node.getExpression();
if (Node.isPropertyAccessExpression(expr) && expr.getName() === "style") {
const arg = node.getArgumentExpression();
if (arg) {
return {
element: expr.getExpression(),
propertyName: arg.getText(),
isComputed: true,
};
}
}
}
return undefined;
}
function getStyleAssignment(statement: Node) {
if (!Node.isExpressionStatement(statement)) return undefined;
const expr = statement.getExpression();
if (!Node.isBinaryExpression(expr)) return undefined;
if (expr.getOperatorToken().getKind() !== SyntaxKind.EqualsToken) return undefined;
const styleAccess = matchStyleAccess(expr.getLeft());
if (!styleAccess) return undefined;
return {
elementText: styleAccess.element.getText(),
property: styleAccess.propertyName,
valueText: expr.getRight().getText(),
isComputed: styleAccess.isComputed,
statementNode: statement,
};
}
interface StyleGroup {
elementText: string;
assignments: {
property: string;
valueText: string;
isComputed: boolean;
statementNode: Node;
}[];
}
let modifiedFilesCount = 0;
for (const sourceFile of project.getSourceFiles()) {
const filePath = sourceFile.getFilePath();
const posixFilePath = toPosixPath(filePath);
// Only process files inside the project src directory.
if (!posixFilePath.startsWith(posixSrc)) {
continue;
}
// Exclude unit and integration test files
if (
posixFilePath.endsWith(".spec.ts") ||
posixFilePath.endsWith(".test.ts") ||
posixFilePath.includes("/_test/") ||
posixFilePath.includes("/testdeno/")
) {
continue;
}
// Collect all blocks, case clauses, and the source file itself
const containers = [
sourceFile,
...sourceFile.getDescendantsOfKind(SyntaxKind.Block),
...sourceFile.getDescendantsOfKind(SyntaxKind.CaseClause),
...sourceFile.getDescendantsOfKind(SyntaxKind.DefaultClause),
];
const fileGroups: StyleGroup[] = [];
for (const container of containers) {
const statements = container.getStatements();
let i = 0;
while (i < statements.length) {
const assignment = getStyleAssignment(statements[i]);
if (assignment) {
const currentGroup: StyleGroup = {
elementText: assignment.elementText,
assignments: [
{
property: assignment.property,
valueText: assignment.valueText,
isComputed: assignment.isComputed,
statementNode: assignment.statementNode,
},
],
};
// Look ahead to collect consecutive assignments to the same element
let j = i + 1;
while (j < statements.length) {
const nextAssignment = getStyleAssignment(statements[j]);
if (nextAssignment && nextAssignment.elementText === assignment.elementText) {
currentGroup.assignments.push({
property: nextAssignment.property,
valueText: nextAssignment.valueText,
isComputed: nextAssignment.isComputed,
statementNode: nextAssignment.statementNode,
});
j++;
} else {
break;
}
}
fileGroups.push(currentGroup);
i = j;
} else {
i++;
}
}
}
if (fileGroups.length > 0) {
console.log(`File: ${posixFilePath.slice(posixProjectRoot.length + 1)}`);
// Process groups in reverse order to keep Node references valid when removing
const reversedGroups = [...fileGroups].reverse();
for (const group of reversedGroups) {
const props = group.assignments.map((c) => {
if (c.isComputed) {
if (
(c.property.startsWith("'") && c.property.endsWith("'")) ||
(c.property.startsWith('"') && c.property.endsWith('"')) ||
(c.property.startsWith("`") && c.property.endsWith("`"))
) {
return `${c.property}: ${c.valueText}`;
}
return `[${c.property}]: ${c.valueText}`;
}
return `${c.property}: ${c.valueText}`;
});
let newText = "";
if (props.length === 1) {
newText = `${group.elementText}.setCssStyles({ ${props[0]} });`;
} else {
newText = `${group.elementText}.setCssStyles({\n ${props.join(",\n ")}\n});`;
}
const firstNode = group.assignments[0].statementNode;
const { line } = sourceFile.getLineAndColumnAtPos(firstNode.getStart());
console.log(` Line ${line}: Replacing consecutive style assignments on "${group.elementText}" with:`);
console.log(
newText
.split("\n")
.map((l) => ` ${l}`)
.join("\n")
);
if (!isDryRun) {
firstNode.replaceWithText(newText);
for (let k = 1; k < group.assignments.length; k++) {
group.assignments[k].statementNode.remove();
}
}
}
modifiedFilesCount++;
}
}
console.log(`\nTotal files to modify: ${modifiedFilesCount}`);
if (!isDryRun) {
project.saveSync();
console.log("All changes successfully saved.");
} else {
console.log("Dry run complete. No changes were written to files.");
}

View File

@@ -0,0 +1,138 @@
// Refactor unused catch variables and unused imports in the codebase.
// Use this script by running `deno run --allow-read --allow-write --allow-env refactor-unused.ts` from the utilsdeno directory.
// Run with --run flag to apply changes.
import { Project, SyntaxKind, Node } from "npm:ts-morph";
import path from "node:path";
import { fileURLToPath } from "node:url";
const isDryRun = !Deno.args.includes("--run");
if (isDryRun) {
console.log("=== DRY RUN MODE ===");
console.log(
"To apply changes, run with: deno run --allow-read --allow-write --allow-env refactor-unused.ts --run\n"
);
} else {
console.log("=== RUN MODE: WILL MODIFY FILES ===");
}
const project = new Project({ tsConfigFilePath: "../tsconfig.json" });
// Only add .ts files to avoid Svelte-markup-blindness references
project.addSourceFilesAtPaths("../src/**/*.ts");
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, "..");
function toPosixPath(filePath: string): string {
return filePath.replace(/\\/g, "/");
}
const posixProjectRoot = toPosixPath(projectRoot);
const posixSrc = `${posixProjectRoot}/src`;
let modifiedFilesCount = 0;
for (const sourceFile of project.getSourceFiles()) {
const filePath = sourceFile.getFilePath();
const posixFilePath = toPosixPath(filePath);
if (!posixFilePath.startsWith(posixSrc)) continue;
if (posixFilePath.includes("/_test/") || posixFilePath.endsWith(".spec.ts") || posixFilePath.endsWith(".test.ts")) continue;
let fileModified = false;
// 1. Find unused catch variables: catch (error) -> catch
const catchClauses = sourceFile.getDescendantsOfKind(SyntaxKind.CatchClause);
const catchVarsToRemove: Node[] = [];
for (const catchClause of catchClauses) {
const varDec = catchClause.getVariableDeclaration();
if (varDec) {
const varName = varDec.getName();
// Count references within the catch clause itself
const count = catchClause.getDescendantsOfKind(SyntaxKind.Identifier)
.filter((id) => id.getText() === varName)
.length;
if (count === 1) { // Only the declaration itself
catchVarsToRemove.push(varDec);
}
}
}
if (catchVarsToRemove.length > 0) {
if (!fileModified) {
console.log(`File: ${posixFilePath.slice(posixProjectRoot.length + 1)}`);
fileModified = true;
}
for (const varDec of catchVarsToRemove) {
const { line } = sourceFile.getLineAndColumnAtPos(varDec.getStart());
console.log(` Line ${line}: Unused catch variable "${varDec.getText()}" -> Remove it`);
if (!isDryRun) {
varDec.remove();
}
}
}
// 2. Find unused named imports
const importDeclarations = sourceFile.getImportDeclarations();
const importsToRemove: { namedImport: any; impDecl: any }[] = [];
const modifiedDecls = new Set<any>();
for (const impDecl of importDeclarations) {
const namedImports = impDecl.getNamedImports();
if (namedImports.length === 0) continue;
for (const namedImport of namedImports) {
const importName = namedImport.getAliasNode()?.getText() ?? namedImport.getName();
// Count references in the entire file
const count = sourceFile.getDescendantsOfKind(SyntaxKind.Identifier)
.filter((id) => id.getText() === importName)
.length;
if (count === 1) { // Only the import specifier itself
importsToRemove.push({ namedImport, impDecl });
}
}
}
if (importsToRemove.length > 0) {
if (!fileModified) {
console.log(`File: ${posixFilePath.slice(posixProjectRoot.length + 1)}`);
fileModified = true;
}
for (const { namedImport, impDecl } of importsToRemove) {
const { line } = sourceFile.getLineAndColumnAtPos(namedImport.getStart());
console.log(` Line ${line}: Unused named import "${namedImport.getText()}" -> Remove it`);
if (!isDryRun) {
namedImport.remove();
modifiedDecls.add(impDecl);
}
}
}
// 3. Clean up empty import declarations (only those we actually modified)
if (!isDryRun && fileModified && modifiedDecls.size > 0) {
for (const impDecl of modifiedDecls) {
if (
impDecl.getNamedImports().length === 0 &&
!impDecl.getDefaultImport() &&
!impDecl.getNamespaceImport()
) {
impDecl.remove();
}
}
}
if (fileModified) {
modifiedFilesCount++;
}
}
console.log(`\nTotal files to modify: ${modifiedFilesCount}`);
if (!isDryRun) {
project.saveSync();
console.log("All changes successfully saved.");
} else {
console.log("Dry run complete. No changes were written to files.");
}