/** * notion-enhancer * (c) 2023 dragonwocky (https://dragonwocky.me/) * (https://notion-enhancer.github.io/) under the MIT license */ import asar from "@electron/asar"; import os from "node:os"; import fsp from "node:fs/promises"; import { existsSync } from "node:fs"; import { fileURLToPath } from "node:url"; import { join, resolve } from "node:path"; import { execSync } from "node:child_process"; import { createRequire } from "node:module"; import patch from "./patch-desktop-app.mjs"; let __notionResources; const nodeRequire = createRequire(import.meta.url), manifest = nodeRequire("../package.json"), platform = process.platform === "linux" && os.release().toLowerCase().includes("microsoft") ? "wsl" : process.platform, polyfillWslEnv = (name) => { if (platform !== "wsl" || process.env[name]) return process.env[name]; // adds a windows environment variable to process.env // in a wsl environment, inc. path conversion const value = execSync(`cmd.exe /c echo %${name}%`, { encoding: "utf8", stdio: "pipe", }).trim(), isAbsolutePath = /^[a-zA-Z]:[\\\/]/.test(value), onSystemDrive = /^[\\\/]/.test(value); if (isAbsolutePath) { // e.g. C:\Program Files const drive = value[0].toLowerCase(), path = value.slice(2).replace(/\\/g, "/"); process.env[name] = `/mnt/${drive}${path}`; } else if (onSystemDrive) { // e.g. \Program Files const drive = polyfillWslEnv("SYSTEMDRIVE")[0].toLowerCase(), path = value.replace(/\\/g, "/"); process.env[name] = `/mnt/${drive}${path}`; } else process.env[name] = value; return process.env[name]; }, readdirDeep = async (dir) => { dir = resolve(dir); let files = []; for (let file of await fsp.readdir(dir)) { if (["node_modules", ".git"].includes(file)) continue; file = join(dir, file); const stat = await fsp.lstat(file); if (stat.isDirectory()) { files = files.concat(await readdirDeep(file)); } else if (stat.isSymbolicLink()) { } else files.push(file); } return files; }; const setNotionPath = (path) => { // sets notion resource path to user provided value // e.g. with the --path cli option __notionResources = path; }, getResourcePath = (path) => { if (__notionResources) return resolve(`${__notionResources}/${path}`); polyfillWslEnv("LOCALAPPDATA"); polyfillWslEnv("PROGRAMW6432"); const potentialPaths = { win32: [ resolve(`${process.env.LOCALAPPDATA}/Programs/Notion/resources`), resolve(`${process.env.PROGRAMW6432}/Notion/resources`), ], darwin: [ `/Users/${process.env.USER}/Applications/Notion.app/Contents/Resources`, "/Applications/Notion.app/Contents/Resources", ], linux: ["/opt/notion-app"], }; potentialPaths["wsl"] = potentialPaths["win32"]; for (const testPath of potentialPaths[platform]) { if (!existsSync(testPath)) continue; __notionResources = testPath; return resolve(`${__notionResources}/${path}`); } }, // prefer unpacked if both exist getAppPath = () => ["app", "app.asar"].map(getResourcePath).find(existsSync), getBackupPath = () => ["app.bak", "app.asar.bak"].map(getResourcePath).find(existsSync), checkEnhancementVersion = () => { // prettier-ignore const manifestPath = getResourcePath("app/node_modules/notion-enhancer/package.json"); if (!existsSync(manifestPath)) return undefined; const insertVersion = nodeRequire(manifestPath).version; return insertVersion; }; const unpackApp = async () => { const appPath = getAppPath(); if (!appPath || !appPath.endsWith("asar")) return false; // asar reads synchronously asar.extractAll(appPath, appPath.replace(/\.asar$/, "")); await fsp.rm(appPath); return true; }, applyEnhancements = async () => { const appPath = getAppPath(); if (!appPath || appPath.endsWith("asar")) return false; const srcPath = fileURLToPath(new URL("../src", import.meta.url)), insertPath = getResourcePath("app/node_modules/notion-enhancer"); if (existsSync(insertPath)) await fsp.rm(insertPath, { recursive: true }); // insert the notion-enhancer/src folder into notion's node_modules folder await fsp.cp(srcPath, insertPath, { recursive: true }); // call patch-desktop-app.mjs on each file // prettier-ignore const notionScripts = (await readdirDeep(appPath)) .filter((file) => file.endsWith(".js")), scriptUpdates = []; for (const file of notionScripts) { const scriptId = file.slice(appPath.length + 1, -3).replace(/\\/g, "/"), scriptContent = await fsp.readFile(file, { encoding: "utf8" }), patchedContent = await patch(scriptId, scriptContent), changesMade = patchedContent !== scriptContent; if (changesMade) scriptUpdates.push(fsp.writeFile(file, patchedContent)); } // create package.json // prettier-ignore const manifestPath = getResourcePath("app/node_modules/notion-enhancer/package.json"), jsManifest = { ...manifest, main: "init.js" }; // remove cli-specific fields delete jsManifest.bin; delete jsManifest.type; delete jsManifest.scripts; delete jsManifest.engines; delete jsManifest.packageManager; delete jsManifest.dependencies; const jsonManifest = JSON.stringify(jsManifest); scriptUpdates.push(fsp.writeFile(manifestPath, jsonManifest)); await Promise.all(scriptUpdates); return true; }, takeBackup = async () => { const appPath = getAppPath(); if (!appPath) return false; const backupPath = getBackupPath(); if (backupPath) await fsp.rm(backupPath, { recursive: true }); const destPath = `${appPath}.bak`; if (!appPath.endsWith(".asar")) { await fsp.cp(appPath, destPath, { recursive: true }); } else await fsp.rename(appPath, destPath); return true; }, restoreBackup = async () => { const backupPath = getBackupPath(); if (!backupPath) return false; const destPath = backupPath.replace(/\.bak$/, ""); if (existsSync(destPath)) await fsp.rm(destPath, { recursive: true }); await fsp.rename(backupPath, destPath); const appPath = getAppPath(); if (destPath !== appPath) await fsp.rm(appPath, { recursive: true }); return true; }; export { getResourcePath, getAppPath, getBackupPath, checkEnhancementVersion, setNotionPath, unpackApp, applyEnhancements, takeBackup, restoreBackup, };