/** * notion-enhancer * (c) 2023 dragonwocky (https://dragonwocky.me/) * (https://notion-enhancer.github.io/) under the MIT license */ "use strict"; const IS_ELECTRON = typeof module !== "undefined", IS_RENDERER = IS_ELECTRON && process.type === "renderer", whenReady = new Promise((res, rej) => { (globalThis.__enhancerApi ??= {}).__isReady = res; }); // expected values: 'linux', 'win32', 'darwin' (== macos), 'firefox' // and 'chromium' (inc. chromium-based browsers like edge and brave) // other possible values: 'aix', 'freebsd', 'openbsd', 'sunos' const platform = IS_ELECTRON ? process.platform : navigator.userAgent.includes("Firefox") ? "firefox" : "chromium", // currently installed version of the notion-enhancer version = IS_ELECTRON ? require("notion-enhancer/package.json").version : chrome.runtime.getManifest().version, // forms a url to a notion-enhancer asset or source file // that can be accessed reliably over http enhancerUrl = (target) => IS_ELECTRON ? `notion://www.notion.so/__notion-enhancer/${target.replace(/^\//, "")}` : chrome.runtime.getURL(target), // require a file from the root of notion's app/ folder, // only available in an electron main process notionRequire = (target) => IS_ELECTRON && !IS_RENDERER ? require(`../../../${target}`) : undefined; let __port; const connectToPort = () => { if (__port) return; __port = chrome.runtime.connect(); __port.onDisconnect.addListener(() => (__port = null)); }, onMessage = (channel, listener) => { // from worker to client if (IS_RENDERER) { const { ipcRenderer } = require("electron"); ipcRenderer.on(channel, (event, message) => listener(message)); } else if (!IS_ELECTRON) { const onMessage = (msg) => { if (msg?.channel !== channel || msg?.invocation) return; listener(msg.message); }; connectToPort(); __port.onMessage.addListener(onMessage); chrome.runtime.onMessage.addListener(onMessage); } }, sendMessage = (channel, message) => { // to worker from client if (IS_RENDERER) { const { ipcRenderer } = require("electron"); ipcRenderer.send(channel, message); } else if (!IS_ELECTRON) { connectToPort(); __port.postMessage({ channel, message }); } }, invokeInWorker = (channel, message) => { // sends a payload to the worker/main // process and waits for a response if (IS_RENDERER) { const { ipcRenderer } = require("electron"); return ipcRenderer.invoke(channel, message); } else if (!IS_ELECTRON) { // polyfills the electron.ipcRenderer.invoke method in // the browser: uses a long-lived ipc connection to // pass messages and handle responses asynchronously let fulfilled; connectToPort(); const id = crypto.randomUUID(); return new Promise((res, rej) => { __port.onMessage.addListener((msg) => { if (msg?.invocation !== id || fulfilled) return; fulfilled = true; res(msg.message); }); __port.postMessage({ channel, message, invocation: id }); }); } }; const readFile = (file) => { if (IS_ELECTRON) { // read directly from filesys if possible, // treating notion-enhancer/src as fs root if (!file.startsWith("http")) { const fsp = require("fs/promises"), { resolve } = require("path"); return fsp.readFile(resolve(`${__dirname}/../${file}`), "utf-8"); } // prefer using versions of files cached by the app // or routed through the notion-enhancer's url interception const notionProtocol = "notion://www.notion.so/"; file = file.replace(/^https:\/\/www\.notion\.so\//, notionProtocol); } else file = file.startsWith("http") ? file : enhancerUrl(file); return fetch(file).then((res) => res.text()); }, readJson = (file) => { // as above, uses require instead of readFile // and res.json() instead of res.text() to return // json content of file in object form if (IS_ELECTRON) { if (!file.startsWith("http")) { const { resolve } = require("path"); return require(resolve(`${__dirname}/../${file}`)); } const notionProtocol = "notion://www.notion.so/"; file = file.replace(/^https:\/\/www\.notion\.so\//, notionProtocol); } else file = file.startsWith("http") ? file : enhancerUrl(file); return fetch(file).then((res) => res.json()); }; const initDatabase = (namespace, fallbacks = {}) => { // all db operations are performed via ipc: // with nodeintegration disabled, sqlite cannot // be require()-d from the renderer process const query = (query, args = {}) => IS_ELECTRON && !IS_RENDERER ? globalThis.__enhancerApi.queryDatabase(namespace, query, args) : invokeInWorker("notion-enhancer", { action: "query-database", data: { namespace, query, args }, }); return { get: (key) => query("get", { key, fallbacks }), set: (key, value) => query("set", { key, value }), remove: (keys) => query("remove", { keys }), export: () => query("export"), import: (obj) => query("import", { obj }), }; }, reloadApp = () => { if (IS_ELECTRON && !IS_RENDERER) { const { app } = require("electron"); app.relaunch(), app.exit(); } else sendMessage("notion-enhancer", "reload-app"); }; Object.assign((globalThis.__enhancerApi ??= {}), { platform, version, enhancerUrl, notionRequire, onMessage, sendMessage, invokeInWorker, readFile, readJson, initDatabase, reloadApp, whenReady: (callback) => whenReady.then(callback), });