diff --git a/src/common/reportTool.ts b/src/common/reportTool.ts index 8eef539..2899528 100644 --- a/src/common/reportTool.ts +++ b/src/common/reportTool.ts @@ -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, diff --git a/src/common/utils.ts b/src/common/utils.ts index 8987694..2333b1f 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -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 = { 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); diff --git a/src/features/ConfigSync/PluginDialogModal.ts b/src/features/ConfigSync/PluginDialogModal.ts index fd79981..34cb27c 100644 --- a/src/features/ConfigSync/PluginDialogModal.ts +++ b/src/features/ConfigSync/PluginDialogModal.ts @@ -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, { diff --git a/src/lib b/src/lib index 2accfbc..3dad956 160000 --- a/src/lib +++ b/src/lib @@ -1 +1 @@ -Subproject commit 2accfbce497cf5cba06d59f083f594778b15486f +Subproject commit 3dad9565aa2b58dc3190dcb88182d0f2acd8fb87 diff --git a/src/modules/coreObsidian/UILib/dialogs.ts b/src/modules/coreObsidian/UILib/dialogs.ts index 4476c5f..ef94340 100644 --- a/src/modules/coreObsidian/UILib/dialogs.ts +++ b/src/modules/coreObsidian/UILib/dialogs.ts @@ -192,8 +192,10 @@ export class MessageBox 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 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 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; }); diff --git a/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts b/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts index 9c1a504..c0dbdcf 100644 --- a/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts +++ b/src/modules/essentialObsidian/APILib/ObsHttpHandler.ts @@ -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 { 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); diff --git a/src/modules/essentialObsidian/ModuleObsidianEvents.ts b/src/modules/essentialObsidian/ModuleObsidianEvents.ts index 55849e5..612ce9e 100644 --- a/src/modules/essentialObsidian/ModuleObsidianEvents.ts +++ b/src/modules/essentialObsidian/ModuleObsidianEvents.ts @@ -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(); } diff --git a/src/modules/features/DocumentHistory/DocumentHistoryModal.ts b/src/modules/features/DocumentHistory/DocumentHistoryModal.ts index 1aa6fb7..95760a3 100644 --- a/src/modules/features/DocumentHistory/DocumentHistoryModal.ts +++ b/src/modules/features/DocumentHistory/DocumentHistoryModal.ts @@ -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); }); }); diff --git a/src/modules/features/ModuleLog.ts b/src/modules/features/ModuleLog.ts index ff1fdc1..ecb9e1f 100644 --- a/src/modules/features/ModuleLog.ts +++ b/src/modules/features/ModuleLog.ts @@ -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 | undefined = undefined; + nextFrameQueue: ReturnType | 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) { diff --git a/src/modules/features/SettingDialogue/PaneSetup.ts b/src/modules/features/SettingDialogue/PaneSetup.ts index 5df4fb9..5737b54 100644 --- a/src/modules/features/SettingDialogue/PaneSetup.ts +++ b/src/modules/features/SettingDialogue/PaneSetup.ts @@ -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); }); diff --git a/src/modules/features/SettingDialogue/utilFixCouchDBSetting.ts b/src/modules/features/SettingDialogue/utilFixCouchDBSetting.ts index dd6d703..b9150ba 100644 --- a/src/modules/features/SettingDialogue/utilFixCouchDBSetting.ts +++ b/src/modules/features/SettingDialogue/utilFixCouchDBSetting.ts @@ -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"]; diff --git a/src/modules/features/SetupWizard/dialogs/utilCheckCouchDB.ts b/src/modules/features/SetupWizard/dialogs/utilCheckCouchDB.ts index 0de0a6a..76e725f 100644 --- a/src/modules/features/SetupWizard/dialogs/utilCheckCouchDB.ts +++ b/src/modules/features/SetupWizard/dialogs/utilCheckCouchDB.ts @@ -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"]; diff --git a/utilsdeno/refactor-globals.ts b/utilsdeno/refactor-globals.ts new file mode 100644 index 0000000..86f35c6 --- /dev/null +++ b/utilsdeno/refactor-globals.ts @@ -0,0 +1,210 @@ +// 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 submodule files under src/lib/ + if (posixFilePath.startsWith(posixLibSrc)) { + continue; + } + + // Exclude independent application modules under src/apps/ + if (posixFilePath.startsWith(`${posixSrc}/apps/`)) { + continue; + } + + // Exclude unit and integration test files + if ( + posixFilePath.endsWith(".spec.ts") || + posixFilePath.endsWith(".test.ts") || + posixFilePath.includes("/_test/") + ) { + 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; + } + } + + // 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 { + 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); + } + + // Ensure compatGlobal is imported + const hasCompatGlobalImport = sourceFile.getImportDeclarations().some((imp) => { + return imp.getNamedImports().some((ni) => ni.getName() === "compatGlobal"); + }); + + if (!hasCompatGlobalImport) { + const existingImport = sourceFile.getImportDeclarations().find((imp) => { + const spec = imp.getModuleSpecifierValue(); + return spec === "@lib/common/coreEnvFunctions" || spec === "@lib/common/coreEnvFunctions.ts"; + }); + + if (existingImport) { + existingImport.addNamedImport("compatGlobal"); + } else { + sourceFile.addImportDeclaration({ + namedImports: ["compatGlobal"], + 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."); +} diff --git a/utilsdeno/refactor-styles.ts b/utilsdeno/refactor-styles.ts new file mode 100644 index 0000000..4851d4c --- /dev/null +++ b/utilsdeno/refactor-styles.ts @@ -0,0 +1,214 @@ +// 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/") + ) { + 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."); +}