Reduce Community lint type-safety warnings

This commit is contained in:
vorotamoroz
2026-07-20 18:08:09 +00:00
parent 2cf535332b
commit 104eeadb4b
29 changed files with 664 additions and 318 deletions
+1 -1
View File
@@ -189,7 +189,7 @@ export async function syncWithPeer(
}
const pushResult = await replicator.requestSynchroniseToPeer(targetPeer.peerId);
if (!pushResult || pushResult.ok !== true) {
const err = pushResult?.error;
const err: unknown = pushResult && "error" in pushResult ? pushResult.error : undefined;
throw err instanceof Error
? err
: LiveSyncError.fromError(err ?? "P2P sync failed while requesting remote sync");
+64 -37
View File
@@ -8,11 +8,8 @@ import LevelDBAdapter from "pouchdb-adapter-leveldb";
import find from "pouchdb-find";
import transform from "transform-pouch";
//@ts-ignore
import { findPathToLeaf } from "pouchdb-merge";
//@ts-ignore
import { findPathToLeaf, type RevisionTreeNode } from "pouchdb-merge";
import { adapterFun } from "pouchdb-utils";
//@ts-ignore
import { createError, MISSING_DOC, UNKNOWN_ERROR } from "pouchdb-errors";
import { mapAllTasksWithConcurrencyLimit, unwrapTaskResult } from "octagonal-wheels/concurrency/task";
@@ -28,8 +25,32 @@ type PurgeLogDocument = {
purgeSeq: number;
purges: Array<{ docId: string; rev: string; purgeSeq: number }>;
};
type PurgeMultiResultMap = Record<string, unknown>;
function appendPurgeSeqs(db: PouchDB.Database, docs: PurgeMultiParam[]) {
interface PouchDBPrivateDatabase extends PouchDB.Database {
adapter: string;
purged_infos_limit: number;
_getRevisionTree(
documentId: string,
callback: (error: Error | undefined, revisions?: RevisionTreeNode[]) => void
): void;
_purge(
documentId: string,
revisionPath: string[],
callback: (error: Error | undefined, result?: PurgeMultiResult) => void
): void;
purgeMulti(documents: PurgeMultiParam[]): Promise<PurgeMultiResultMap>;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null;
}
function isSuccessfulPurge(value: unknown): value is PurgeMultiResult {
return isRecord(value) && value.ok === true;
}
function appendPurgeSeqs(db: PouchDBPrivateDatabase, docs: PurgeMultiParam[]) {
return db
.get<PurgeLogDocument>("_local/purges")
.then(function (doc) {
@@ -40,18 +61,16 @@ function appendPurgeSeqs(db: PouchDB.Database, docs: PurgeMultiParam[]) {
rev: rev$$1,
purgeSeq,
});
//@ts-ignore : missing type def
if (doc.purges.length > db.purged_infos_limit) {
//@ts-ignore : missing type def
doc.purges.splice(0, doc.purges.length - db.purged_infos_limit);
}
doc.purgeSeq = purgeSeq;
}
return doc;
})
.catch(function (err) {
if (err.status !== 404) {
throw err;
.catch(function (error: unknown) {
if (!isRecord(error) || error.status !== 404) {
throw error;
}
return {
_id: "_local/purges",
@@ -71,68 +90,76 @@ function appendPurgeSeqs(db: PouchDB.Database, docs: PurgeMultiParam[]) {
/**
* purge multiple documents at once.
*/
PouchDB.prototype.purgeMulti = adapterFun(
const pouchDBPrototype = (PouchDB as typeof PouchDB & { prototype: PouchDBPrivateDatabase }).prototype;
pouchDBPrototype.purgeMulti = adapterFun<PouchDBPrivateDatabase, [documents: PurgeMultiParam[]], PurgeMultiResultMap>(
"_purgeMulti",
function (
this: PouchDBPrivateDatabase,
docs: PurgeMultiParam[],
callback: (
error: Error,
result?: {
[x: string]: PurgeMultiResult | Error;
}
) => void
callback: (error?: Error, result?: PurgeMultiResultMap) => void
) {
//@ts-ignore
if (typeof this._purge === "undefined") {
return callback(
//@ts-ignore: this ts-ignore might be hiding a `this` bug where we don't have "this" conext.
createError(UNKNOWN_ERROR, "Purge is not implemented in the " + this.adapter + " adapter.")
);
}
//@ts-ignore
// eslint-disable-next-line @typescript-eslint/no-this-alias -- The adapter task callbacks must retain this PouchDB instance.
const self = this;
const tasks = docs.map(
(param) => () =>
new Promise<[PurgeMultiParam, PurgeMultiResult | Error]>((res, rej) => {
new Promise<[PurgeMultiParam, unknown]>((res) => {
const [docId, rev$$1] = param;
self._getRevisionTree(docId, (error: Error, revs: string[]) => {
self._getRevisionTree(docId, (error, revs) => {
if (error) {
return res([param, error]);
}
if (!revs) {
return res([param, createError(MISSING_DOC)]);
}
let path;
let path: string[];
try {
path = findPathToLeaf(revs, rev$$1);
} catch (error) {
//@ts-ignore
return res([param, error.message || error]);
} catch (caught: unknown) {
const failure = caught instanceof Error && caught.message ? caught.message : caught;
return res([param, failure]);
}
self._purge(docId, path, (error: Error, result: PurgeMultiResult) => {
self._purge(docId, path, (error, result) => {
if (error) {
return res([param, error]);
} else {
return res([param, result]);
}
return res([param, result]);
});
});
})
);
(async () => {
const ret = await mapAllTasksWithConcurrencyLimit(1, tasks);
const retAll = ret.map((e) => unwrapTaskResult(e)) as [PurgeMultiParam, PurgeMultiResult | Error][];
await appendPurgeSeqs(
self,
retAll.filter((e) => "ok" in e[1]).map((e) => e[0])
);
const result = Object.fromEntries(retAll.map((e) => [e[0][0], e[1]]));
const retAll: Array<[PurgeMultiParam, unknown]> = [];
for (const entry of ret) {
const outcome = unwrapTaskResult(entry);
if (outcome instanceof Error) {
throw outcome;
}
retAll.push(outcome);
}
const successfullyPurged: PurgeMultiParam[] = [];
const resultEntries: Array<[string, unknown]> = [];
for (const [document, outcome] of retAll) {
if (isSuccessfulPurge(outcome)) {
successfullyPurged.push(document);
}
resultEntries.push([document[0], outcome]);
}
await appendPurgeSeqs(self, successfullyPurged);
const result: PurgeMultiResultMap = Object.fromEntries(resultEntries);
return result;
})()
//@ts-ignore
.then((result) => callback(undefined, result))
.catch((error) => callback(error));
.catch((caught: unknown) => {
const error = caught instanceof Error ? caught : new Error(String(caught));
callback(error);
});
}
);
+21
View File
@@ -0,0 +1,21 @@
declare module "pouchdb-merge" {
export interface RevisionTreeNode {
pos: number;
ids: [revision: string, metadata: Record<string, unknown>, branches: RevisionTreeNode["ids"][]];
}
export function findPathToLeaf(revisions: RevisionTreeNode[], targetRevision: string): string[];
}
declare module "pouchdb-utils" {
export function adapterFun<TThis, TArguments extends unknown[], TResult>(
name: string,
callback: (this: TThis, ...args: [...TArguments, callback: (error?: Error, result?: TResult) => void]) => void
): (this: TThis, ...args: TArguments) => Promise<TResult>;
}
declare module "pouchdb-errors" {
export const MISSING_DOC: unknown;
export const UNKNOWN_ERROR: unknown;
export function createError(error: unknown, reason?: string): Error;
}
+27 -14
View File
@@ -82,6 +82,17 @@ function deserializeFromNodeKV(value: unknown): unknown {
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, deserializeFromNodeKV(v)]));
}
function asKeyString(key: unknown): string {
if (typeof key === "string") {
return key;
}
const serialised = JSON.stringify(key);
if (typeof serialised !== "string") {
throw new TypeError("The IndexedDB key could not be serialised");
}
return serialised;
}
class NodeFileKeyValueDatabase implements KeyValueDatabase {
private filePath: string;
private data = new Map<string, unknown>();
@@ -91,13 +102,6 @@ class NodeFileKeyValueDatabase implements KeyValueDatabase {
this.load();
}
private asKeyString(key: IDBValidKey): string {
if (typeof key === "string") {
return key;
}
return JSON.stringify(key);
}
private load() {
try {
const loaded = JSON.parse(nodeFs.readFileSync(this.filePath, "utf-8")) as Record<string, unknown>;
@@ -116,17 +120,17 @@ class NodeFileKeyValueDatabase implements KeyValueDatabase {
}
async get<T>(key: IDBValidKey): Promise<T> {
return this.data.get(this.asKeyString(key)) as T;
return this.data.get(asKeyString(key)) as T;
}
async set<T>(key: IDBValidKey, value: T): Promise<IDBValidKey> {
this.data.set(this.asKeyString(key), value);
this.data.set(asKeyString(key), value);
this.flush();
return key;
}
async del(key: IDBValidKey): Promise<void> {
this.data.delete(this.asKeyString(key));
this.data.delete(asKeyString(key));
this.flush();
}
@@ -144,11 +148,12 @@ class NodeFileKeyValueDatabase implements KeyValueDatabase {
let filtered = allKeys;
if (typeof query !== "undefined") {
if (this.isIDBKeyRangeLike(query)) {
const lower = query.lower?.toString() ?? "";
const upper = query.upper?.toString() ?? "\uffff";
const lower = query.lower === undefined ? "" : String(query.lower);
const upper = query.upper === undefined ? "\uffff" : String(query.upper);
filtered = filtered.filter((key) => key >= lower && key <= upper);
} else {
const exact = query.toString();
const exactValue: unknown = query;
const exact = String(exactValue);
filtered = filtered.filter((key) => key === exact);
}
}
@@ -272,7 +277,15 @@ export class NodeKeyValueDBService<T extends ServiceContext = ServiceContext>
await getDB().del(`${prefix}${key}`);
},
keys: async (from: string | undefined, to: string | undefined, count?: number): Promise<string[]> => {
const allKeys = (await getDB().keys(undefined, count)).map((e) => e.toString());
const rawKeys: unknown = await getDB().keys(undefined, count);
if (!Array.isArray(rawKeys)) {
throw new TypeError("The key-value database returned an invalid key list");
}
const keyList: unknown[] = rawKeys;
const allKeys: string[] = [];
for (const key of keyList) {
allKeys.push(String(key));
}
const lower = `${prefix}${from ?? ""}`;
const upper = `${prefix}${to ?? "\uffff"}`;
return allKeys
+13 -4
View File
@@ -4,8 +4,17 @@ import { fileURLToPath, fs, isBuiltin, path } from "@vrtmrz/livesync-commonlib/n
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const resolve = (...args: string[]) => path.resolve(...args).replace(/\\/g, "/");
const repoRoot = path.resolve(__dirname, "../../..");
const packageJson = JSON.parse(fs.readFileSync(path.resolve(repoRoot, "package.json"), "utf-8"));
const manifestJson = JSON.parse(fs.readFileSync(path.resolve(repoRoot, "manifest.json"), "utf-8"));
function readVersion(filePath: string): string | undefined {
const parsed: unknown = JSON.parse(fs.readFileSync(filePath, "utf-8"));
if (typeof parsed !== "object" || parsed === null || !("version" in parsed)) {
return undefined;
}
return typeof parsed.version === "string" ? parsed.version : undefined;
}
const packageVersion = readVersion(path.resolve(repoRoot, "package.json"));
const manifestVersion = readVersion(path.resolve(repoRoot, "manifest.json"));
// https://vite.dev/config/
const defaultExternal = [
"obsidian",
@@ -113,7 +122,7 @@ export default defineConfig({
global: "globalThis",
nonInteractive: "true",
// localStorage: "undefined", // Prevent usage of localStorage in the CLI environment
MANIFEST_VERSION: JSON.stringify(process.env.MANIFEST_VERSION || manifestJson.version || "0.0.0"),
PACKAGE_VERSION: JSON.stringify(process.env.PACKAGE_VERSION || packageJson.version || "0.0.0"),
MANIFEST_VERSION: JSON.stringify(process.env.MANIFEST_VERSION || manifestVersion || "0.0.0"),
PACKAGE_VERSION: JSON.stringify(process.env.PACKAGE_VERSION || packageVersion || "0.0.0"),
},
});
+5 -1
View File
@@ -176,7 +176,11 @@ class LiveSyncWebApp {
const fileHandle = await livesyncDir.getFileHandle(SETTINGS_FILE);
const file = await fileHandle.getFile();
const text = await file.text();
return JSON.parse(text);
const parsed: unknown = JSON.parse(text);
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
throw new Error("The WebApp settings file does not contain an object");
}
return parsed;
} catch {
// File doesn't exist yet
return null;
+223 -148
View File
@@ -5,16 +5,43 @@
* Obsidian mock, and must not become a general test environment for the plug-in. When the Webapp compatibility boundary
* is redesigned, replace this implementation here rather than extending it as a shared Obsidian simulation.
*/
export const SettingCache = new Map<any, any>();
//@ts-ignore obsidian global
globalThis.activeDocument = document;
import type {
Command,
DataWriteOptions,
ListedFiles,
MarkdownFileInfo,
PluginManifest,
RequestUrlParam,
RequestUrlResponse,
ValueComponent,
} from "obsidian";
export type {
DataWriteOptions,
ListedFiles,
MarkdownFileInfo,
PluginManifest,
RequestUrlParam,
RequestUrlResponse,
ValueComponent,
};
type EventCallback = (...args: unknown[]) => unknown;
declare global {
interface Window {
activeDocument: Document;
}
}
export const SettingCache = new Map<object, unknown>();
window.activeDocument = document;
declare const hostPlatform: string | undefined;
globalThis.process = {
platform: (hostPlatform || "win32") as any,
} as any;
console.warn(`[Obsidian Mock] process.platform is set to ${globalThis.process.platform}`);
Reflect.set(window, "process", {
platform: hostPlatform || "win32",
});
export class TAbstractFile {
vault: Vault;
path: string;
@@ -55,7 +82,12 @@ export class TFolder extends TAbstractFile {
}
}
export class EventRef {}
export class EventRef {
constructor(
readonly name: string,
readonly callback: EventCallback
) {}
}
// class StorageMap<T, U> extends Map<T, U> {
// constructor(saveName?: string) {
@@ -114,7 +146,7 @@ export class Vault {
private files: Map<string, TAbstractFile> = new Map();
private contents: Map<string, string | ArrayBuffer> = new Map();
private root: TFolder;
private listeners: Map<string, Set<(...args: any[]) => any>> = new Map();
private listeners: Map<string, Set<EventCallback>> = new Map();
constructor(vaultName?: string) {
if (vaultName) {
@@ -194,19 +226,16 @@ export class Vault {
if (this.files.has(path)) throw new Error("File already exists");
const name = path.split("/").pop() || "";
const parentPath = path.includes("/") ? path.substring(0, path.lastIndexOf("/")) : "";
let parent = this.getAbstractFileByPath(parentPath);
if (!parent || !(parent instanceof TFolder)) {
parent = await this.createFolder(parentPath);
}
const existingParent = this.getAbstractFileByPath(parentPath);
const parent = existingParent instanceof TFolder ? existingParent : await this.createFolder(parentPath);
const file = new TFile(this, path, name, parent as TFolder);
const file = new TFile(this, path, name, parent);
file.stat.size = typeof data === "string" ? new TextEncoder().encode(data).length : data.byteLength;
file.stat.ctime = options?.ctime ?? Date.now();
file.stat.mtime = options?.mtime ?? Date.now();
this.files.set(path, file);
this.contents.set(path, data);
(parent as TFolder).children.push(file);
// console.dir(this.files);
parent.children.push(file);
this.trigger("create", file);
return file;
@@ -224,7 +253,6 @@ export class Vault {
file.stat.mtime = options?.mtime ?? Date.now();
file.stat.ctime = options?.ctime ?? file.stat.ctime ?? Date.now();
file.stat.size = typeof data === "string" ? data.length : data.byteLength;
console.warn(`[Obsidian Mock ${this.vaultName}] Modified file at path: '${file.path}'`);
this.files.set(file.path, file);
this.trigger("modify", file);
}
@@ -252,6 +280,14 @@ export class Vault {
}
async delete(file: TAbstractFile, force?: boolean): Promise<void> {
return this.removeFile(file);
}
async trash(file: TAbstractFile, system: boolean): Promise<void> {
return this.removeFile(file);
}
private async removeFile(file: TAbstractFile): Promise<void> {
await Promise.resolve();
this.files.delete(file.path);
this.contents.delete(file.path);
@@ -261,30 +297,27 @@ export class Vault {
this.trigger("delete", file);
}
async trash(file: TAbstractFile, system: boolean): Promise<void> {
await Promise.resolve();
return this.delete(file);
async removeFromAdapter(file: TAbstractFile): Promise<void> {
return this.removeFile(file);
}
on(name: string, callback: (...args: any[]) => any, ctx?: any): EventRef {
if (!this.listeners.has(name)) {
this.listeners.set(name, new Set());
}
const boundCallback = ctx ? callback.bind(ctx) : callback;
this.listeners.get(name)!.add(boundCallback);
return { name, callback: boundCallback } as any;
on(name: string, callback: EventCallback, ctx?: object): EventRef {
const listeners = this.listeners.get(name) ?? new Set<EventCallback>();
this.listeners.set(name, listeners);
const boundCallback = ctx ? (...args: unknown[]) => callback.apply(ctx, args) : callback;
listeners.add(boundCallback);
return new EventRef(name, boundCallback);
}
off(name: string, callback: any) {
off(name: string, callback: EventCallback) {
this.listeners.get(name)?.delete(callback);
}
offref(ref: EventRef) {
const { name, callback } = ref as any;
this.off(name, callback);
this.off(ref.name, ref.callback);
}
trigger(name: string, ...args: any[]) {
trigger(name: string, ...args: unknown[]) {
this.listeners.get(name)?.forEach((cb) => cb(...args));
}
@@ -366,28 +399,29 @@ export class DataAdapter {
}
async remove(path: string): Promise<void> {
const file = this.vault.getAbstractFileByPath(path);
if (file) await this.vault.delete(file);
if (file) await this.vault.removeFromAdapter(file);
}
}
class Events {
_eventEmitter = new EventTarget();
_events = new Map<any, any>();
_eventTarget(cb: any) {
const x = this._events.get(cb);
if (x) {
return x;
_events = new Map<EventCallback, EventListener>();
_eventTarget(callback: EventCallback): EventListener {
const registered = this._events.get(callback);
if (registered) {
return registered;
}
const callback = (evt: any) => {
x(evt?.detail ?? undefined);
const eventListener = (event: Event) => {
callback(event instanceof CustomEvent ? event.detail : undefined);
};
this._events.set(cb, callback);
return callback;
this._events.set(callback, eventListener);
return eventListener;
}
on(name: string, cb: any, ctx?: any) {
this._eventEmitter.addEventListener(name, this._eventTarget(cb));
on(name: string, callback: EventCallback, ctx?: object) {
const registered = ctx ? (...args: unknown[]) => callback.apply(ctx, args) : callback;
this._eventEmitter.addEventListener(name, this._eventTarget(registered));
}
trigger(name: string, args: any) {
trigger(name: string, args: unknown) {
const evt = new CustomEvent(name, {
detail: args,
});
@@ -403,13 +437,13 @@ class Workspace extends Events {
return null;
}
onLayoutReady(cb: any) {
onLayoutReady(callback: () => void) {
// cb();
// console.log("[Obsidian Mock] Workspace onLayoutReady registered");
// this._eventEmitter.addEventListener("layout-ready", () => {
// console.log("[Obsidian Mock] Workspace layout-ready event triggered");
setTimeout(() => {
cb();
window.setTimeout(() => {
callback();
}, 200);
// });
}
@@ -434,33 +468,36 @@ export class App {
}
vault: Vault;
workspace: Workspace = new Workspace();
metadataCache: any = {
on: (name: string, cb: any, ctx?: any) => {},
metadataCache = {
on: (name: string, callback: EventCallback, ctx?: object): EventRef => {
const registered = ctx ? (...args: unknown[]) => callback.apply(ctx, args) : callback;
return new EventRef(name, registered);
},
getFileCache: (): null => null,
};
}
export class Plugin {
app: App;
manifest: any;
settings: any;
commands: Map<string, any> = new Map();
constructor(app: App, manifest: any) {
manifest: PluginManifest;
settings: unknown;
commands: Map<string, Command> = new Map();
constructor(app: App, manifest: PluginManifest) {
this.app = app;
this.manifest = manifest;
}
async loadData(): Promise<any> {
async loadData(): Promise<unknown> {
await Promise.resolve();
return SettingCache.get(this.app) ?? {};
}
async saveData(data: any): Promise<void> {
async saveData(data: unknown): Promise<void> {
await Promise.resolve();
SettingCache.set(this.app, data);
}
onload() {}
onunload() {}
addSettingTab(tab: any) {}
addCommand(command: any) {
addSettingTab(tab: PluginSettingTab) {}
addCommand(command: Command) {
this.commands.set(command.id, command);
}
addStatusBarItem() {
@@ -478,10 +515,14 @@ export class Plugin {
};
return icon;
}
registerView(type: string, creator: any) {}
registerObsidianProtocolHandler(handler: any) {}
registerEvent(handler: any) {}
registerDomEvent(target: any, eventName: string, handler: any) {}
registerView(type: string, creator: () => ItemView) {}
registerObsidianProtocolHandler(handler: (params: Record<string, string>) => unknown) {}
registerEvent(handler: EventRef) {}
registerDomEvent<K extends keyof HTMLElementEventMap>(
target: HTMLElement,
eventName: K,
handler: (event: HTMLElementEventMap[K]) => unknown
) {}
}
export class Notice {
@@ -489,11 +530,8 @@ export class Notice {
private static _counter = 0;
constructor(message: string) {
this._key = Notice._counter++;
console.log(`Notice [${this._key}]:`, message);
}
setMessage(message: string) {
console.log(`Notice [${this._key}]:`, message);
}
setMessage(message: string) {}
}
export class Modal {
@@ -558,7 +596,7 @@ export const Platform = {
};
export class Menu {
addItem(cb: (item: MenuItem) => any) {
addItem(cb: (item: MenuItem) => unknown) {
cb(new MenuItem());
return this;
}
@@ -571,7 +609,7 @@ export class MenuItem {
setIcon(icon: string) {
return this;
}
onClick(cb: (evt: MouseEvent) => any) {
onClick(cb: (evt: MouseEvent) => unknown) {
return this;
}
}
@@ -584,7 +622,7 @@ export class Component {
export class ButtonComponent extends Component {
buttonEl: HTMLButtonElement = document.createElement("button");
private clickHandler: ((evt: MouseEvent) => any) | null = null;
private clickHandler: ((evt: MouseEvent) => unknown) | null = null;
constructor() {
super();
@@ -602,7 +640,7 @@ export class ButtonComponent extends Component {
return this;
}
onClick(cb: (evt: MouseEvent) => any) {
onClick(cb: (evt: MouseEvent) => unknown) {
this.clickHandler = cb;
this.buttonEl.removeEventListener("click", this.clickHandler);
this.buttonEl.addEventListener("click", (evt) => cb(evt as MouseEvent));
@@ -627,7 +665,7 @@ export class ButtonComponent extends Component {
export class TextComponent extends Component {
inputEl: HTMLInputElement = document.createElement("input");
private changeHandler: ((value: string) => any) | null = null;
private changeHandler: ((value: string) => unknown) | null = null;
constructor() {
super();
@@ -635,7 +673,7 @@ export class TextComponent extends Component {
this.inputEl.type = "text";
}
onChange(cb: (value: string) => any) {
onChange(cb: (value: string) => unknown) {
this.changeHandler = cb;
this.inputEl.removeEventListener("change", this.handleChange);
this.inputEl.addEventListener("change", this.handleChange);
@@ -671,7 +709,7 @@ export class TextComponent extends Component {
export class ToggleComponent extends Component {
inputEl: HTMLInputElement = document.createElement("input");
private changeHandler: ((value: boolean) => any) | null = null;
private changeHandler: ((value: boolean) => unknown) | null = null;
constructor() {
super();
@@ -679,7 +717,7 @@ export class ToggleComponent extends Component {
this.inputEl.type = "checkbox";
}
onChange(cb: (value: boolean) => any) {
onChange(cb: (value: boolean) => unknown) {
this.changeHandler = cb;
this.inputEl.addEventListener("change", (evt) => {
const target = evt.target as HTMLInputElement;
@@ -701,7 +739,7 @@ export class ToggleComponent extends Component {
export class DropdownComponent extends Component {
selectEl: HTMLSelectElement = document.createElement("select");
private changeHandler: ((value: string) => any) | null = null;
private changeHandler: ((value: string) => unknown) | null = null;
constructor() {
super();
@@ -723,7 +761,7 @@ export class DropdownComponent extends Component {
return this;
}
onChange(cb: (value: string) => any) {
onChange(cb: (value: string) => unknown) {
this.changeHandler = cb;
this.selectEl.addEventListener("change", (evt) => {
const target = evt.target as HTMLSelectElement;
@@ -745,7 +783,7 @@ export class DropdownComponent extends Component {
export class SliderComponent extends Component {
inputEl: HTMLInputElement = document.createElement("input");
private changeHandler: ((value: number) => any) | null = null;
private changeHandler: ((value: number) => unknown) | null = null;
constructor() {
super();
@@ -753,7 +791,7 @@ export class SliderComponent extends Component {
this.inputEl.type = "range";
}
onChange(cb: (value: number) => any) {
onChange(cb: (value: number) => unknown) {
this.changeHandler = cb;
this.inputEl.addEventListener("change", (evt) => {
const target = evt.target as HTMLInputElement;
@@ -816,72 +854,115 @@ export class Setting {
this.controlEl.addClass(c);
return this;
}
addText(cb: (text: TextComponent) => any) {
addText(cb: (text: TextComponent) => unknown) {
const component = new TextComponent();
this.controlEl.appendChild(component.inputEl);
cb(component);
return this;
}
addToggle(cb: (toggle: ToggleComponent) => any) {
addToggle(cb: (toggle: ToggleComponent) => unknown) {
const component = new ToggleComponent();
cb(component);
return this;
}
addButton(cb: (btn: ButtonComponent) => any) {
addButton(cb: (btn: ButtonComponent) => unknown) {
const btn = new ButtonComponent();
this.controlEl.appendChild(btn.buttonEl);
cb(btn);
return this;
}
addDropdown(cb: (dropdown: DropdownComponent) => any) {
addDropdown(cb: (dropdown: DropdownComponent) => unknown) {
const component = new DropdownComponent();
cb(component);
return this;
}
addSlider(cb: (slider: SliderComponent) => any) {
addSlider(cb: (slider: SliderComponent) => unknown) {
const component = new SliderComponent();
cb(component);
return this;
}
}
// HTMLElement extensions
function applyDomElementInfo(element: HTMLElement, info?: DomElementInfo | string): void {
if (typeof info === "string") {
element.textContent = info;
return;
}
if (!info) return;
if (info.cls) {
const classes = Array.isArray(info.cls) ? info.cls : info.cls.split(" ");
element.classList.add(...classes.filter((className) => className !== ""));
}
if (info.text !== undefined) {
element.replaceChildren(info.text);
}
if (info.attr) {
for (const [name, value] of Object.entries(info.attr)) {
if (value === null) element.removeAttribute(name);
else element.setAttribute(name, String(value));
}
}
if (info.title !== undefined) element.title = info.title;
if (info.value !== undefined && "value" in element) element.value = info.value;
if (info.type !== undefined && "type" in element) element.type = info.type;
if (info.placeholder !== undefined && "placeholder" in element) element.placeholder = info.placeholder;
if (info.href !== undefined) element.setAttribute("href", info.href);
}
// HTMLElement extensions used by the Webapp compatibility implementation.
if (typeof HTMLElement !== "undefined") {
const proto = HTMLElement.prototype as any;
proto.createDiv = function (o?: any) {
const div = document.createElement("div");
if (o?.cls) div.addClass(o.cls);
if (o?.text) div.setText(o.text);
this.appendChild(div);
return div;
const proto = HTMLElement.prototype;
proto.createDiv = function (
this: HTMLElement,
info?: DomElementInfo | string,
callback?: (element: HTMLDivElement) => void
): HTMLDivElement {
const element = document.createElement("div");
applyDomElementInfo(element, info);
this.appendChild(element);
callback?.(element);
return element;
};
proto.createEl = function (tag: string, o?: any) {
const el = document.createElement(tag);
if (o?.cls) el.addClass(o.cls);
if (o?.text) el.setText(o.text);
this.appendChild(el);
return el;
proto.createEl = function <K extends keyof HTMLElementTagNameMap>(
this: HTMLElement,
tag: K,
info?: DomElementInfo | string,
callback?: (element: HTMLElementTagNameMap[K]) => void
): HTMLElementTagNameMap[K] {
const element = document.createElement(tag);
applyDomElementInfo(element, info);
this.appendChild(element);
callback?.(element);
return element;
};
proto.createSpan = function (o?: any) {
return this.createEl("span", o);
proto.createSpan = function (
this: HTMLElement,
info?: DomElementInfo | string,
callback?: (element: HTMLSpanElement) => void
): HTMLSpanElement {
const element = document.createElement("span");
applyDomElementInfo(element, info);
this.appendChild(element);
callback?.(element);
return element;
};
proto.empty = function () {
this.innerHTML = "";
proto.empty = function (this: HTMLElement): void {
this.replaceChildren();
};
proto.setText = function (t: string) {
this.textContent = t;
proto.setText = function (this: HTMLElement, text: string): void {
this.textContent = text;
};
proto.addClass = function (c: string) {
this.classList.add(c);
proto.addClass = function (this: HTMLElement, className: string): void {
this.classList.add(className);
};
proto.removeClass = function (c: string) {
this.classList.remove(c);
proto.removeClass = function (this: HTMLElement, className: string): void {
this.classList.remove(className);
};
proto.toggleClass = function (c: string, b: boolean) {
this.classList.toggle(c, b);
proto.toggleClass = function (this: HTMLElement, className: string, value: boolean): void {
this.classList.toggle(className, value);
};
proto.hasClass = function (c: string) {
return this.classList.contains(c);
proto.hasClass = function (this: HTMLElement, className: string): boolean {
return this.classList.contains(className);
};
}
@@ -896,10 +977,19 @@ export class FuzzySuggestModal<T> {
throw new Error("Not implemented.");
}
}
function parseHtmlFragment(html: string): DocumentFragment {
const parsed = new DOMParser().parseFromString(html, "text/html");
const fragment = document.createDocumentFragment();
for (const child of [...parsed.body.childNodes]) {
fragment.appendChild(document.importNode(child, true));
}
return fragment;
}
export class MarkdownRenderer {
static render(app: App, md: string, el: HTMLElement, path: string, component: Component) {
// eslint-disable-next-line no-unsanitized/property -- This compatibility method mirrors Obsidian's trusted Markdown renderer boundary.
el.innerHTML = md;
el.replaceChildren(parseHtmlFragment(md));
return Promise.resolve();
}
}
@@ -910,26 +1000,23 @@ export class WorkspaceLeaf {}
export function sanitizeHTMLToDom(html: string) {
const div = document.createElement("div");
// eslint-disable-next-line no-unsanitized/property -- This compatibility method mirrors Obsidian's sanitised-HTML API contract.
div.innerHTML = html;
div.appendChild(parseHtmlFragment(html));
return div;
}
export function addIcon() {}
export const debounce = (fn: any) => fn;
export async function request(options: any) {
export function debounce<Arguments extends unknown[], Result>(
fn: (...args: Arguments) => Result
): (...args: Arguments) => Result {
return fn;
}
export async function request(options: RequestUrlParam | string): Promise<string> {
const result = await requestUrl(options);
return result.text;
}
export async function requestUrl({
body,
headers,
method,
url,
contentType,
}: RequestUrlParam): Promise<RequestUrlResponse> {
// console.log("[requestUrl] Mock called:", { method, url, contentType });
export async function requestUrl(options: RequestUrlParam | string): Promise<RequestUrlResponse> {
const { body, headers, method, url, contentType } = typeof options === "string" ? { url: options } : options;
const reqHeadersObj: Record<string, string> = {};
for (const key of Object.keys(headers || {})) {
reqHeadersObj[key.toLowerCase()] = headers[key];
@@ -940,21 +1027,21 @@ export async function requestUrl({
reqHeadersObj["Cache-Control"] = "no-cache, no-store, must-revalidate";
reqHeadersObj["Pragma"] = "no-cache";
reqHeadersObj["Expires"] = "0";
const result = await fetch(url, {
method: method,
const result = await window.fetch(url, {
method,
headers: {
...reqHeadersObj,
},
body: body,
body,
});
const headersObj: Record<string, string> = {};
result.headers.forEach((value, key) => {
headersObj[key] = value;
});
let json = undefined;
let text = undefined;
let arrayBuffer = undefined;
let json: unknown;
let text = "";
let arrayBuffer = new ArrayBuffer(0);
try {
const isJson = result.headers.get("content-type")?.includes("application/json");
arrayBuffer = await result.arrayBuffer();
@@ -965,9 +1052,8 @@ export async function requestUrl({
if (isJson) {
json = await JSON.parse(text || "{}");
}
} catch (e) {
console.warn("Failed to parse response:", e);
// ignore
} catch {
json = undefined;
}
return {
status: result.status,
@@ -977,11 +1063,12 @@ export async function requestUrl({
arrayBuffer: arrayBuffer,
};
}
export function stringifyYaml(obj: any) {
export function stringifyYaml(obj: unknown): string {
return JSON.stringify(obj);
}
export function parseYaml(s: string) {
return JSON.parse(s);
export function parseYaml(s: string): unknown {
const parsed: unknown = JSON.parse(s);
return parsed;
}
export function getLanguage() {
return "en";
@@ -997,15 +1084,3 @@ export function arrayBufferToBase64(buffer: ArrayBuffer): string {
export function base64ToArrayBuffer(base64: string): ArrayBuffer {
return Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)).buffer;
}
export type DataWriteOptions = any;
export type PluginManifest = any;
export type RequestUrlParam = any;
export type RequestUrlResponse = any;
export type MarkdownFileInfo = any;
export type ListedFiles = {
files: string[];
folders: string[];
};
export type ValueComponent = any;
+20 -7
View File
@@ -15,6 +15,18 @@ export type VaultHistoryItem = {
type VaultHistoryValue = VaultHistoryItem;
function isVaultHistoryValue(value: unknown): value is VaultHistoryValue {
if (typeof value !== "object" || value === null) return false;
const candidate = value as Partial<VaultHistoryValue>;
return (
typeof candidate.id === "string" &&
typeof candidate.name === "string" &&
typeof candidate.handle === "object" &&
candidate.handle !== null &&
typeof candidate.lastUsedAt === "number"
);
}
function makeVaultKey(id: string): string {
return `${VAULT_KEY_PREFIX}${id}`;
}
@@ -87,7 +99,7 @@ export class VaultHistoryStore {
async getLastUsedVaultId(): Promise<string | null> {
return this.withStore("readonly", async (store) => {
const value = await this.requestAsPromise(store.get(LAST_USED_KEY));
const value: unknown = await this.requestAsPromise(store.get(LAST_USED_KEY));
return typeof value === "string" ? value : null;
});
}
@@ -95,20 +107,21 @@ export class VaultHistoryStore {
async getVaultHistory(): Promise<VaultHistoryItem[]> {
return this.withStore("readonly", async (store) => {
const keys = await this.requestAsPromise(store.getAllKeys());
const values = (await this.requestAsPromise(store.getAll())) as unknown[];
const values = await this.requestAsPromise<unknown[]>(store.getAll() as IDBRequest<unknown[]>);
const items: VaultHistoryItem[] = [];
for (let i = 0; i < keys.length; i++) {
const key = String(keys[i]);
const key = keys[i];
if (typeof key !== "string") continue;
const id = parseVaultId(key);
const value = values[i] as Partial<VaultHistoryValue> | undefined;
if (!id || !value || !value.handle || !value.name) {
const value = values[i];
if (!id || !isVaultHistoryValue(value)) {
continue;
}
items.push({
id,
name: String(value.name),
name: value.name,
handle: value.handle,
lastUsedAt: Number(value.lastUsedAt || 0),
lastUsedAt: value.lastUsedAt,
});
}
items.sort((a, b) => b.lastUsedAt - a.lastUsedAt);
+13 -4
View File
@@ -4,8 +4,17 @@ import istanbul from "vite-plugin-istanbul";
import { fileURLToPath, fs, path } from "@vrtmrz/livesync-commonlib/node";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const repoRoot = path.resolve(__dirname, "../../..");
const packageJson = JSON.parse(fs.readFileSync(path.resolve(repoRoot, "package.json"), "utf-8"));
const manifestJson = JSON.parse(fs.readFileSync(path.resolve(repoRoot, "manifest.json"), "utf-8"));
function readVersion(filePath: string): string | undefined {
const parsed: unknown = JSON.parse(fs.readFileSync(filePath, "utf-8"));
if (typeof parsed !== "object" || parsed === null || !("version" in parsed)) {
return undefined;
}
return typeof parsed.version === "string" ? parsed.version : undefined;
}
const packageVersion = readVersion(path.resolve(repoRoot, "package.json"));
const manifestVersion = readVersion(path.resolve(repoRoot, "manifest.json"));
const enableCoverage = process.env.PW_COVERAGE === "1";
// https://vite.dev/config/
export default defineConfig({
@@ -57,8 +66,8 @@ export default defineConfig({
},
},
define: {
MANIFEST_VERSION: JSON.stringify(process.env.MANIFEST_VERSION || manifestJson.version || "0.0.0"),
PACKAGE_VERSION: JSON.stringify(process.env.PACKAGE_VERSION || packageJson.version || "0.0.0"),
MANIFEST_VERSION: JSON.stringify(process.env.MANIFEST_VERSION || manifestVersion || "0.0.0"),
PACKAGE_VERSION: JSON.stringify(process.env.PACKAGE_VERSION || packageVersion || "0.0.0"),
global: "globalThis",
hostPlatform: JSON.stringify(process.platform || "linux"),
},