/** * notion-enhancer * (c) 2024 dragonwocky (https://dragonwocky.me/) * (https://notion-enhancer.github.io/) under the MIT license */ import os from "node:os"; import fsp from "node:fs/promises"; import { resolve } from "node:path"; import { existsSync } from "node:fs"; import { execSync } from "node:child_process"; import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; import asar from "@electron/asar"; import patch from "./patch-desktop-app.mjs"; const nodeRequire = createRequire(import.meta.url), platform = process.platform === "linux" && os.release().toLowerCase().includes("microsoft") ? "wsl" : process.platform, getEnv = (name) => { if (platform !== "wsl" || process.env[name]) return process.env[name]; // read windows environment variables and convert // windows paths to paths mounted in the wsl fs const pipe = { encoding: "utf8", stdio: "pipe" }, value = execSync(`cmd.exe /c echo %${name}%`, pipe).trim(), isAbsolutePath = /^[a-zA-Z]:[\\\/]/.test(value), isSystemPath = /^[\\\/]/.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 (isSystemPath) { // e.g. \Program Files const drive = getEnv("SYSTEMDRIVE")[0].toLowerCase(), path = value.replace(/\\/g, "/"); process.env[name] = `/mnt/${drive}${path}`; } else process.env[name] = value; return process.env[name]; }; let __notionResources; const setNotionPath = (path) => { // sets notion resource path to user provided value // e.g. with the --path cli option __notionResources = path; }, getResourcePath = (...paths) => { if (__notionResources) return resolve(__notionResources, ...paths); // prettier-ignore for (const [platforms, notionResources] of [ [['win32', 'wsl'], resolve(`${getEnv("LOCALAPPDATA")}/Programs/Notion/resources`)], [['win32', 'wsl'], resolve(`${getEnv("PROGRAMW6432")}/Notion/resources`)], [['darwin'], `/Users/${getEnv("USER")}/Applications/Notion.app/Contents/Resources`], [['darwin'], "/Applications/Notion.app/Contents/Resources"], [['linux'], "/opt/notion-app"], ]) { if (!platforms.includes(platform)) continue; if (!existsSync(notionResources)) continue; __notionResources = notionResources; return resolve(__notionResources, ...paths); } }, extractFile = (path) => { const archive = getResourcePath("app.asar"); return asar.extractFile(archive, path); }; const getInsertPath = (...paths) => { return "node_modules/notion-enhancer/" + paths.join("/"); }, getInsertVersion = () => { try { const manifest = extractFile(getInsertPath("package.json")).toString(); return JSON.parse(manifest).version; } catch { return null; } }; const backupApp = async () => { const archive = getResourcePath("app.asar"); if (!existsSync(archive)) return false; await fsp.cp(archive, archive + ".bak"); return true; }, restoreApp = async () => { const archive = getResourcePath("app.asar"); if (!existsSync(archive + ".bak")) return false; await fsp.rename(archive + ".bak", archive); return true; }, enhanceApp = async (debug = false) => { const app = getResourcePath("app"), archive = getResourcePath("app.asar"); if (!existsSync(archive)) return false; if (existsSync(app)) await fsp.rm(app, { recursive: true, force: true }); await fsp.mkdir(app); // extract archive to folder and apply patches for (let file of asar.listPackage(archive)) { file = file.replace(/^\//g, ""); const stat = asar.statFile(archive, file), isFolder = !!stat.files, isSymlink = !!stat.link, isExecutable = stat.executable, appPath = resolve(app, file); if (isFolder) { await fsp.mkdir(appPath); } else if (isSymlink) { await fsp.symlink(appPath, resolve(app, link)); } else { await fsp.writeFile(appPath, patch(file, extractFile(file))); if (isExecutable) await fsp.chmod(appPath, "755"); } } // insert the notion-enhancer/src folder into notion's node_modules const insertSrc = fileURLToPath(new URL("../src", import.meta.url)), insertDest = resolve(app, getInsertPath()); await fsp.cp(insertSrc, insertDest, { recursive: true }); // create package.json with cli-specific fields removed const insertManifest = resolve(insertDest, "package.json"), manifest = { ...nodeRequire("../package.json"), main: "init.js" }, excludes = ["bin", "type", "scripts", "engines", "dependencies"]; for (const key of excludes) delete manifest[key]; await fsp.writeFile(insertManifest, JSON.stringify(manifest)); // re-package enhanced sources into executable archive await asar.createPackage(app, archive); // cleanup extracted files unless in debug mode if (!debug) await fsp.rm(app, { recursive: true }); return true; }; export { backupApp, restoreApp, enhanceApp, getInsertVersion, getResourcePath, setNotionPath, };