mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-06-18 00:03:01 +03:00
Compare commits
8 Commits
main
...
refactor_s
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
72033472f3 | ||
|
|
93dc03e86f | ||
|
|
dae8443fe8 | ||
|
|
88a8bcbd5a | ||
|
|
4a5283543d | ||
|
|
7895336189 | ||
|
|
2d5cdccf7d | ||
|
|
497fd04081 |
2195
package-lock.json
generated
2195
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
21
package.json
21
package.json
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
13
src/apps/cli/node-compat.ts
Normal file
13
src/apps/cli/node-compat.ts
Normal 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,
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
/* Path mapping */
|
||||
"baseUrl": ".",
|
||||
// "baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["../../*"],
|
||||
"@lib/*": ["../../lib/src/*"]
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"extends": "../../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"sourceRoot": "../",
|
||||
// "sourceRoot": "../",
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, {
|
||||
|
||||
2
src/lib
2
src/lib
Submodule src/lib updated: 2accfbce49...c926417f82
@@ -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][]
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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"];
|
||||
|
||||
@@ -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
114
utilsdeno/README.md
Normal 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
49
utilsdeno/detect-any.ts
Normal 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}`);
|
||||
96
utilsdeno/refactor-assertions.ts
Normal file
96
utilsdeno/refactor-assertions.ts
Normal 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.");
|
||||
}
|
||||
132
utilsdeno/refactor-cli-node-imports.ts
Normal file
132
utilsdeno/refactor-cli-node-imports.ts
Normal 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.");
|
||||
}
|
||||
226
utilsdeno/refactor-globals.ts
Normal file
226
utilsdeno/refactor-globals.ts
Normal 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.");
|
||||
}
|
||||
222
utilsdeno/refactor-styles.ts
Normal file
222
utilsdeno/refactor-styles.ts
Normal 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.");
|
||||
}
|
||||
138
utilsdeno/refactor-unused.ts
Normal file
138
utilsdeno/refactor-unused.ts
Normal 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.");
|
||||
}
|
||||
Reference in New Issue
Block a user