feat(cli): repack enhanced sources into app.asar

This commit is contained in:
dragonwocky 2024-04-21 15:45:29 +10:00
parent 23ea8c1c55
commit a4b1e6e5f2
Signed by: dragonwocky
GPG Key ID: 7998D08F7D7BD7A8
8 changed files with 278 additions and 348 deletions

232
bin.mjs
View File

@ -6,21 +6,21 @@
* (https://notion-enhancer.github.io/) under the MIT license
*/
import arg from "arg";
import chalk from "chalk-template";
import os from "node:os";
import { createRequire } from "node:module";
import chalk from "chalk-template";
import arg from "arg";
import {
getAppPath,
getBackupPath,
checkEnhancementVersion,
backupApp,
enhanceApp,
getInsertVersion,
getResourcePath,
restoreApp,
setNotionPath,
unpackApp,
applyEnhancements,
takeBackup,
restoreBackup,
} from "./scripts/enhance-desktop-app.mjs";
import { greaterThan } from "./src/core/updateCheck.mjs";
import { existsSync } from "node:fs";
const nodeRequire = createRequire(import.meta.url),
manifest = nodeRequire("./package.json");
@ -144,7 +144,7 @@ const printHelp = (commands, options) => {
chalk` ${cmd[0].padEnd(cmdPad)} {grey :} ${cmd[1]}`,
parseOpt = (opt) =>
chalk` ${opt[0].padEnd(optPad)} {grey :} ${opt[1][1]}`;
print`{bold.whiteBright ${name} v${version}}\n{grey ${homepage}}
print`{bold.whiteBright.underline ${name} v${version}}\n{grey ${homepage}}
\n{bold.whiteBright USAGE}\n${name} <command> [options]
\n{bold.whiteBright COMMANDS}\n${commands.map(parseCmd).join("\n")}
\n{bold.whiteBright OPTIONS}\n${options.map(parseOpt).join("\n")}\n`;
@ -160,8 +160,8 @@ const printHelp = (commands, options) => {
os: os.release(),
});
} else {
const enhancerVersion = `${manifest.name}@v${manifest.version}`,
nodeVersion = `node@${process.version}`,
const nodeVersion = `node@${process.version}`,
enhancerVersion = `${manifest.name}@v${manifest.version}`,
osVersion = `${process.platform}-${process.arch}/${os.release()}`;
print`${enhancerVersion} via ${nodeVersion} on ${osVersion}\n`;
}
@ -170,40 +170,42 @@ const printHelp = (commands, options) => {
try {
const commands = [
// ["command", "description"]
["apply", "add enhancements to the notion app"],
["remove", "return notion to its pre-enhanced/pre-modded state"],
["check", "check the current state of the notion app"],
["apply", "Inject the notion-enhancer into Notion desktop."],
["remove", "Restore Notion desktop to its pre-enhanced state."],
["check", "Report Notion desktop's enhancement state."],
],
options = [
// ["alias, option=example", [type, "description"]]
[
"--path=</path/to/notion/resources>",
[String, "manually provide a notion installation location"],
],
[
"--overwrite",
[Boolean, "for rapid development; unsafely overwrite sources"],
[String, "Manually provide a Notion installation location."],
],
[
"--no-backup",
[Boolean, "skip backup; enhancement will be faster but irreversible"],
[Boolean, "Skip backup; enhancement will be irreversible."],
],
[
"--json",
[Boolean, "Output JSON from the `check` and `--version` commands."],
],
[
"-y, --yes",
[Boolean, 'skip prompts; assume "yes" and run non-interactively'],
[Boolean, 'Skip prompts; assume "yes" and run non-interactively.'],
],
[
"-n, --no",
[Boolean, 'skip prompts; assume "no" and run non-interactively'],
[Boolean, 'Skip prompts; assume "no" and run non-interactively.'],
],
[
"-q, --quiet",
[Boolean, 'skip prompts; assume "no" unless -y and hide all output'],
[Boolean, 'Skip prompts; assume "no" unless -y and hide all output.'],
],
["-d, --debug", [Boolean, "show detailed error messages"]],
["-j, --json", [Boolean, "display json output (where applicable)"]],
["-h, --help", [Boolean, "display usage information"]],
["-v, --version", [Boolean, "display version number"]],
[
"-d, --debug",
[Boolean, "Show detailed error messages and keep extracted files."],
],
["-h, --help", [Boolean, "Display usage information for this CLI."]],
["-v, --version", [Boolean, "Display this CLI's version number."]],
];
const args = arg(compileOptsToArgSpec(options));
@ -216,149 +218,121 @@ try {
if (args["--version"]) printVersion(), process.exit();
if (args["--path"]) setNotionPath(args["--path"]);
const appPath = getAppPath(),
backupPath = getBackupPath(),
insertVersion = checkEnhancementVersion();
const appPath = getResourcePath("app.asar"),
backupPath = getResourcePath("app.asar.bak"),
insertVersion = await getInsertVersion(),
updateAvailable = greaterThan(manifest.version, insertVersion);
const messages = {
"notion-found": `notion installation found`,
"notion-not-found": `notion installation not found (corrupted or nonexistent)`,
"notion-is-packed": `electron archive found: extracting app.asar`,
"notion-found": insertVersion
? // prettier-ignore
`Notion desktop found with ${manifest.name} v${insertVersion
} applied${updateAvailable ? "" : " (up to date)"}.`
: `Notion desktop found (no enhancements applied).`,
"notion-not-found": `Notion desktop not found.`,
"not-applied": `notion-enhancer not applied`,
"version-applied": `notion-enhancer v${manifest.version} applied`,
"version-mismatch": `notion-enhancer v${insertVersion} applied != v${manifest.version} current`,
"prompt-version-replace": `replace?`,
// prettier-ignore
"update-available": chalk`v${manifest.version
} is available! To apply, run {underline ${manifest.name} apply -y}.`,
// prettier-ignore
"update-confirm": `${updateAvailable ? "Upgrade" : "Downgrade"
} to ${manifest.name}${manifest.name} v${manifest.version}?`,
"backup-found": `backup found`,
"backup-not-found": `backup not found`,
"creating-backup": `backing up notion before enhancement`,
"restoring-backup": `restoring`,
"inserting-enhancements": `inserting enhancements and patching notion sources`,
"manual-removal-instructions": `to remove the notion-enhancer from notion, uninstall notion and
then install a vanilla version of the app from https://www.notion.so/desktop (mac,
windows) or ${manifest.homepage}/getting-started/installation (linux)`,
"backup-found": `Restoring to pre-enhanced state from backup...`,
"backup-not-found": chalk`No backup found: to restore Notion desktop to its pre-enhanced state,
uninstall it and reinstall Notion from {underline https://www.notion.so/desktop}.`,
"backup-app": `Backing up app before enhancement...`,
"enhance-app": `Enhancing and patching app sources...`,
};
const SUCCESS = chalk`{bold.whiteBright SUCCESS} {green ✔}`,
FAILURE = chalk`{bold.whiteBright FAILURE} {red ✘}`,
CANCELLED = chalk`{bold.whiteBright CANCELLED} {red ✘}`,
INCOMPLETE = Symbol();
const interactiveRestoreBackup = async () => {
if (backupPath) {
// replace enhanced app with vanilla app.bak/app.asar.bak
print` {grey * ${messages["backup-found"]}: ${messages["restoring-backup"]}} `;
startSpinner();
await restoreBackup();
stopSpinner();
return INCOMPLETE;
} else {
print` {red * ${messages["backup-not-found"]}: ${messages["manual-removal-instructions"]}}\n`;
const interactiveRestore = async () => {
if (!backupPath || !existsSync(backupPath)) {
print` {red * ${messages["backup-not-found"]}}\n`;
return FAILURE;
}
print` {grey * ${messages["backup-found"]}} `;
startSpinner();
await restoreApp();
stopSpinner();
return SUCCESS;
};
const canEnhancementsBeApplied = async () => {
if (!appPath) {
// notion not installed
const getNotion = () => {
if (!appPath || !existsSync(appPath)) {
print` {red * ${messages["notion-not-found"]}}\n`;
return FAILURE;
} else if (insertVersion === manifest.version) {
// same version already applied
if (args["--overwrite"]) {
print` {grey * ${messages["inserting-enhancements"]}} `;
startSpinner();
await applyEnhancements();
stopSpinner();
print` {grey * ${messages["version-applied"]}}\n`;
} else {
print` {grey * ${messages["notion-found"]}: ${messages["version-applied"]}}\n`;
}
return SUCCESS;
} else {
print` {grey * ${messages["notion-found"]}}\n`;
return INCOMPLETE;
}
if (insertVersion && insertVersion !== manifest.version) {
},
compareVersions = async () => {
if (insertVersion === manifest.version) {
// same version already applied
print` {grey * ${messages["notion-found"]}}\n`;
return SUCCESS;
} else if (insertVersion) {
// diff version already applied
print` {grey * ${messages["notion-found"]}: ${messages["version-mismatch"]}}\n`;
// prettier-ignore
const promptReplacement = await promptConfirmation(messages["prompt-version-replace"]);
print` {grey * ${messages["notion-found"]}}\n`;
const replace = await promptConfirmation(messages["update-confirm"]);
print`\n`;
return ["Y", "y"].includes(promptReplacement)
? await interactiveRestoreBackup()
return ["Y", "y"].includes(replace)
? (await interactiveRestore()) === SUCCESS
? INCOMPLETE
: FAILURE
: CANCELLED;
} else return INCOMPLETE;
},
interactiveApplyEnhancements = async () => {
if (appPath.endsWith(".asar")) {
print` {grey * ${messages["notion-is-packed"]}} `;
// asar blocks thread = spinner won't actually spin
// first frame at least can serve as waiting indicator
startSpinner();
await unpackApp();
stopSpinner();
}
// backup is used to restore app to pre-enhanced state
// new backup should be taken every enhancement
// e.g. in case old backup was from prev. version of app
interactiveEnhance = async () => {
if (!args["--no-backup"]) {
print` {grey * ${messages["creating-backup"]}} `;
print` {grey * ${messages["backup-app"]}} `;
startSpinner();
await takeBackup();
await backupApp();
stopSpinner();
}
print` {grey * ${messages["inserting-enhancements"]}} `;
print` {grey * ${messages["enhance-app"]}} `;
startSpinner();
await applyEnhancements();
await enhanceApp(__debug);
stopSpinner();
print` {grey * ${messages["version-applied"]}}\n`;
return SUCCESS;
},
interactiveRemoveEnhancements = async () => {
if (!appPath) {
// notion not installed
print` {red * ${messages["notion-not-found"]}}\n`;
return FAILURE;
} else if (insertVersion) {
print` {grey * ${messages["notion-found"]}: ${messages["version-applied"]}}\n`;
return (await interactiveRestoreBackup()) === INCOMPLETE
? SUCCESS
: FAILURE;
}
print` {grey * ${messages["notion-found"]}: ${messages["not-applied"]}}\n`;
return SUCCESS;
};
switch (args["_"][0]) {
case "apply": {
print`{bold.whiteBright [NOTION-ENHANCER] APPLY}\n`;
let res = await canEnhancementsBeApplied();
if (res === INCOMPLETE) res = await interactiveApplyEnhancements();
print`{bold.whiteBright [${manifest.name.toUpperCase()}] APPLY}\n`;
let res = getNotion();
if (res === INCOMPLETE) res = await compareVersions();
if (res === INCOMPLETE) res = await interactiveEnhance();
print`${res}\n`;
break;
}
case "remove": {
print`{bold.whiteBright [NOTION-ENHANCER] REMOVE}\n`;
const res = await interactiveRemoveEnhancements();
print`{bold.whiteBright [${manifest.name.toUpperCase()}] REMOVE}\n`;
let res = getNotion();
if (res === INCOMPLETE) {
res = insertVersion ? await interactiveRestore() : SUCCESS;
}
print`${res}\n`;
break;
}
case "check": {
if (__json) {
printObject({
appPath,
backupPath,
insertVersion,
currentVersion: manifest.version,
});
process.exit();
const cliVersion = manifest.version,
state = { appPath, backupPath, insertVersion, cliVersion };
if (appPath && !existsSync(appPath)) state.appPath = null;
if (backupPath && !existsSync(backupPath)) state.backupPath = null;
printObject(state), process.exit();
}
print`{bold.whiteBright [${manifest.name.toUpperCase()}] CHECK}\n`;
let res = getNotion();
if (res === INCOMPLETE && updateAvailable) {
print` {grey * ${messages["update-available"]}}\n`;
}
print`{bold.whiteBright [NOTION-ENHANCER] CHECK:} `;
if (manifest.version === insertVersion) {
print`${messages["version-applied"]}\n`;
} else if (insertVersion) {
print`${messages["version-mismatch"]}\n`;
} else if (appPath) {
print`${messages["not-applied"]}\n`;
} else print`${messages["notion-not-found"]}\n`;
break;
}
@ -375,6 +349,6 @@ try {
.map((at) => at.replace(/\s{4}/g, " "))
.join("\n")}}`;
} else {
print`{bold.red Error:} ${message} {grey (run with -d for more information)}\n`;
print`{bold.red Error:} ${message} {grey (Run with -d for more information.)}\n`;
}
}

10
package-lock.json generated
View File

@ -9,7 +9,7 @@
"version": "0.11.1",
"license": "MIT",
"dependencies": {
"@electron/asar": "^3.2.8",
"@electron/asar": "^3.2.9",
"arg": "^5.0.2",
"chalk-template": "^1.1.0"
},
@ -24,9 +24,9 @@
}
},
"node_modules/@electron/asar": {
"version": "3.2.8",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.8.tgz",
"integrity": "sha512-cmskk5M06ewHMZAplSiF4AlME3IrnnZhKnWbtwKVLRkdJkKyUVjMLhDIiPIx/+6zQWVlKX/LtmK9xDme7540Sg==",
"version": "3.2.9",
"resolved": "https://registry.npmjs.org/@electron/asar/-/asar-3.2.9.tgz",
"integrity": "sha512-Vu2P3X2gcZ3MY9W7yH72X9+AMXwUQZEJBrsPIbX0JsdllLtoh62/Q8Wg370/DawIEVKOyfD6KtTLo645ezqxUA==",
"dependencies": {
"commander": "^5.0.0",
"glob": "^7.1.6",
@ -36,7 +36,7 @@
"asar": "bin/asar.js"
},
"engines": {
"node": ">=10.11.1"
"node": ">=10.12.0"
}
},
"node_modules/arg": {

View File

@ -7,7 +7,6 @@
"repository": "github:notion-enhancer/desktop",
"bugs": "https://github.com/notion-enhancer/desktop/issues",
"funding": "https://github.com/sponsors/dragonwocky",
"packageManager": "yarn@3.6.1",
"license": "MIT",
"bin": "bin.mjs",
"type": "module",
@ -35,8 +34,8 @@
"notion-enhancer"
],
"dependencies": {
"@electron/asar": "^3.2.8",
"arg": "^5.0.2",
"chalk-template": "^1.1.0"
"@electron/asar": "^3.2.9",
"chalk-template": "^1.1.0",
"arg": "^5.0.2"
}
}

View File

@ -1,179 +1,143 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (c) 2024 dragonwocky <thedragonring.bod@gmail.com> (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 { resolve } from "node:path";
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 { fileURLToPath } from "node:url";
import asar from "@electron/asar";
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) => {
getEnv = (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(),
// 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),
onSystemDrive = /^[\\\/]/.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 (onSystemDrive) {
} else if (isSystemPath) {
// e.g. \Program Files
const drive = polyfillWslEnv("SYSTEMDRIVE")[0].toLowerCase(),
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];
},
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;
};
let __notionResources;
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}`);
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);
}
},
// 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;
extractFile = (path) => {
const archive = getResourcePath("app.asar");
return asar.extractFile(archive, path);
};
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;
const getInsertPath = (...paths) => {
return "node_modules/notion-enhancer/" + paths.join("/");
},
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 = patch(scriptId, scriptContent),
changesMade = patchedContent !== scriptContent;
if (changesMade) scriptUpdates.push(fsp.writeFile(file, patchedContent));
getInsertVersion = () => {
try {
const manifest = extractFile(getInsertPath("package.json")).toString();
return JSON.parse(manifest).version;
} catch {
return null;
}
// 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);
};
const backupApp = async () => {
const archive = getResourcePath("app.asar");
if (!existsSync(archive)) return false;
await fsp.cp(archive, archive + ".bak");
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);
restoreApp = async () => {
const archive = getResourcePath("app.asar");
if (!existsSync(archive + ".bak")) return false;
await fsp.rename(archive + ".bak", archive);
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 });
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,
getAppPath,
getBackupPath,
checkEnhancementVersion,
setNotionPath,
unpackApp,
applyEnhancements,
takeBackup,
restoreBackup,
};

View File

@ -4,93 +4,85 @@
* (https://notion-enhancer.github.io/) under the MIT license
*/
const replaceIfNotFound = ({ string, mode = "replace" }, search, replacement) =>
string.includes(replacement)
? string
: string.replace(
search,
typeof replacement === "string" && mode === "append"
? `$&${replacement}`
: typeof replacement === "string" && mode === "prepend"
? `${replacement}$&`
: replacement
);
// patch scripts within notion's sources to
// activate and respond to the notion-enhancer
const injectTriggerOnce = (file, content) =>
content +
(!/require\(['|"]notion-enhancer['|"]\)/.test(content)
? `\n\nrequire("notion-enhancer")('${file}',exports,(js)=>eval(js));`
: ""),
replaceIfNotFound = ({ string, mode = "replace" }, search, replacement) =>
string.includes(replacement)
? string
: string.replace(
search,
typeof replacement === "string" && mode === "append"
? `$&${replacement}`
: typeof replacement === "string" && mode === "prepend"
? `${replacement}$&`
: replacement
);
// require()-ing the notion-enhancer in worker scripts
// or in renderer scripts will throw errors => manually
// inject trigger into only the necessary scripts
// (avoid re-injection on re-enhancement)
const injectTriggerOnce = (scriptId, scriptContent) =>
scriptContent +
(!/require\(['|"]notion-enhancer['|"]\)/.test(scriptContent)
? `\n\nrequire("notion-enhancer")('${scriptId}',exports,(js)=>eval(js));`
: "");
const patches = {
// prettier-ignore
".webpack/main/index.js": (file, content) => {
content = injectTriggerOnce(file, content);
const replace = (...args) =>
(content = replaceIfNotFound(
{ string: content, mode: "replace" },
...args
)),
prepend = (...args) =>
(content = replaceIfNotFound(
{ string: content, mode: "prepend" },
...args
));
const mainScript = ".webpack/main/index",
rendererScripts = [
".webpack/renderer/tab_browser_view/preload",
".webpack/renderer/draggable_tabs/preload",
".webpack/renderer/tabs/preload",
],
patches = {
// prettier-ignore
[mainScript]: (scriptContent) => {
scriptContent = injectTriggerOnce(mainScript, scriptContent);
const replace = (...args) =>
(scriptContent = replaceIfNotFound(
{ string: scriptContent, mode: "replace" },
...args
)),
prepend = (...args) =>
(scriptContent = replaceIfNotFound(
{ string: scriptContent, mode: "prepend" },
...args
));
// https://github.com/notion-enhancer/notion-enhancer/issues/160:
// enable the notion:// protocol, windows-style tab layouts, and
// quitting the app when the last window is closed on linux
const isWindows =
/(?:"win32"===process\.platform(?:(?=,isFullscreen)|(?=&&\w\.BrowserWindow)|(?=&&\(\w\.app\.requestSingleInstanceLock)))/g,
isWindowsOrLinux = '["win32","linux"].includes(process.platform)';
replace(isWindows, isWindowsOrLinux);
// https://github.com/notion-enhancer/notion-enhancer/issues/160:
// enable the notion:// protocol, windows-style tab layouts, and
// quitting the app when the last window is closed on linux
const isWindows =
/(?:"win32"===process\.platform(?:(?=,isFullscreen)|(?=&&\w\.BrowserWindow)|(?=&&\(\w\.app\.requestSingleInstanceLock)))/g,
isWindowsOrLinux = '["win32","linux"].includes(process.platform)';
replace(isWindows, isWindowsOrLinux);
// restore node integration in the renderer process
// so the notion-enhancer can be require()-d into it
replace(/sandbox:!0/g, `sandbox:!1,nodeIntegration:!0,session:require('electron').session.fromPartition("persist:notion")`);
// restore node integration in the renderer process
// so the notion-enhancer can be require()-d into it
replace(/sandbox:!0/g, `sandbox:!1,nodeIntegration:!0,session:require('electron').session.fromPartition("persist:notion")`);
// bypass webRequest filter to load enhancer menu
replace(/(\w)\.top!==\w\?(\w)\(\{cancel:!0\}\)/, "$1.top!==$1?$2({})");
// bypass webRequest filter to load enhancer menu
replace(/(\w)\.top!==\w\?(\w)\({cancel:!0}\)/, "$1.top!==$1?$2({})");
// https://github.com/notion-enhancer/desktop/issues/291
// bypass csp issues by intercepting the notion:// protocol
const protocolHandler = /try\{const \w=await \w\.assetCache\.handleRequest\(\w\);/,
protocolInterceptor = `{const n="notion://www.notion.so/__notion-enhancer/";if(e.url.startsWith(n))return require("electron").net.fetch(\`file://\${require("path").join(__dirname,"..","..","node_modules","notion-enhancer",e.url.slice(n.length))}\`)}`;
prepend(protocolHandler, protocolInterceptor);
// expose the app config to the global namespace for manipulation
// e.g. to enable development mode
prepend(/\w\.exports=JSON\.parse\('\{"env":"production"/, "globalThis.__notionConfig=");
// https://github.com/notion-enhancer/desktop/issues/291
// bypass csp issues by intercepting the notion:// protocol
const protocolHandler = /try{const \w=await \w\.assetCache\.handleRequest\(\w\);/,
protocolInterceptor = `{const n="notion://www.notion.so/__notion-enhancer/";if(e.url.startsWith(n))return require("electron").net.fetch(\`file://\${require("path").join(__dirname,"..","..","node_modules","notion-enhancer",e.url.slice(n.length))}\`)}`;
prepend(protocolHandler, protocolInterceptor);
// expose the app config to the global namespace for manipulation
// e.g. to enable development mode
prepend(/\w\.exports=JSON\.parse\('{"env":"production"/, "globalThis.__notionConfig=");
// expose the app store to the global namespace for reading
// e.g. to check if keep in background is enabled
prepend(/\w\.Store=\(0,\w\.configureStore\)/, "globalThis.__notionStore=");
prepend(/\w\.updatePreferences=\w\.updatePreferences/, "globalThis.__updatePreferences=");
// expose the app store to the global namespace for reading
// e.g. to check if keep in background is enabled
prepend(/\w\.Store=\(0,\w\.configureStore\)/, "globalThis.__notionStore=");
prepend(/\w\.updatePreferences=\w\.updatePreferences/, "globalThis.__updatePreferences=");
// conditionally create frameless windows
const titlebarStyle = `titleBarStyle:globalThis.__notionConfig?.titlebarStyle??"hiddenInset"`;
replace(`titleBarStyle:"hiddenInset"`, titlebarStyle);
// conditionally create frameless windows
const titlebarStyle = `titleBarStyle:globalThis.__notionConfig?.titlebarStyle??"hiddenInset"`;
replace(`titleBarStyle:"hiddenInset"`, titlebarStyle);
return scriptContent;
},
["*"]: (scriptId, scriptContent) => {
if (!rendererScripts.includes(scriptId)) return scriptContent;
return injectTriggerOnce(scriptId, scriptContent);
},
};
export default (scriptId, scriptContent) => {
if (patches["*"]) scriptContent = patches["*"](scriptId, scriptContent);
if (patches[scriptId]) scriptContent = patches[scriptId](scriptContent);
return scriptContent;
return content;
},
".webpack/renderer/tabs/preload.js": injectTriggerOnce,
".webpack/renderer/tab_browser_view/preload.js": injectTriggerOnce,
};
const decoder = new TextDecoder(),
encoder = new TextEncoder();
export default (file, content) => {
if (!patches[file]) return content;
content = decoder.decode(content);
content = patches[file](file, content);
return encoder.encode(content);
};

View File

@ -25,8 +25,10 @@ const parseVersion = (semver) => {
.map((v) => v ?? "")
.map((v) => (/^\d+$/.test(v) ? parseInt(v) : v));
},
// is a < b
greaterThan = (a, b) => {
// is a greater than b
if (a && !b) return true;
if (!a && b) return false;
a = parseVersion(a);
b = parseVersion(b);
for (let i = 0; i < a.length; i++) {
@ -44,4 +46,4 @@ const checkForUpdate = async () => {
return !(await checkForUpdate()) && version !== _release;
};
export { checkForUpdate, isDevelopmentBuild };
export { checkForUpdate, isDevelopmentBuild, greaterThan };

View File

@ -50,7 +50,6 @@
"clientScripts": ["client.mjs"],
"electronScripts": [
[".webpack/main/index", "electron.cjs"],
[".webpack/renderer/draggable_tabs/preload", "tabs.cjs"],
[".webpack/renderer/tabs/preload", "tabs.cjs"]
]
}

View File

@ -26,7 +26,7 @@ if (isElectron()) {
module.exports = async (target, __exports, __eval) => {
const __getApi = () => globalThis.__enhancerApi;
if (target === ".webpack/main/index") require("./worker.js");
if (target === ".webpack/main/index.js") require("./worker.js");
else {
// expose globalThis.__enhancerApi to scripts
const { contextBridge } = require("electron");