mirror of
https://github.com/notion-enhancer/notion-enhancer.git
synced 2025-04-02 19:59:03 +00:00
feat(cli): repack enhanced sources into app.asar
This commit is contained in:
parent
23ea8c1c55
commit
a4b1e6e5f2
232
bin.mjs
232
bin.mjs
@ -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
10
package-lock.json
generated
@ -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": {
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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 };
|
||||
|
@ -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"]
|
||||
]
|
||||
}
|
||||
|
@ -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");
|
||||
|
Loading…
Reference in New Issue
Block a user