From d8cec4368da2be38aaccc724b4444ab8e96de066 Mon Sep 17 00:00:00 2001 From: dragonwocky Date: Sun, 7 Nov 2021 16:10:42 +1100 Subject: [PATCH] patch scheme, init client, cjs api, env storage hidden --dev-patch apply opt --- bin.mjs | 1 + insert/api | 2 +- insert/client.mjs | 34 +++++++ insert/dep | 2 +- insert/env/env.cjs | 48 ++++++++++ insert/env/env.mjs | 12 ++- insert/env/fs.cjs | 53 +++++++++++ insert/env/fs.mjs | 4 +- insert/env/storage.cjs | 137 +++++++++++++++++++++++++++ insert/env/storage.mjs | 55 +---------- insert/init.cjs | 33 +++++++ insert/init.js | 11 --- insert/media | 2 +- insert/repo | 2 +- package.json | 3 +- pkg/apply.mjs | 41 ++++---- pkg/remove.mjs | 2 + pkg/replacers/main/main.mjs | 2 +- pkg/replacers/main/schemeHandler.mjs | 43 +++++++++ 19 files changed, 394 insertions(+), 93 deletions(-) create mode 100644 insert/client.mjs create mode 100644 insert/env/env.cjs create mode 100644 insert/env/fs.cjs create mode 100644 insert/env/storage.cjs create mode 100644 insert/init.cjs delete mode 100644 insert/init.js create mode 100644 pkg/replacers/main/schemeHandler.mjs diff --git a/bin.mjs b/bin.mjs index fab6e91..5cfad11 100644 --- a/bin.mjs +++ b/bin.mjs @@ -104,6 +104,7 @@ try { const res = await apply(notionPath, { overwritePrevious: promptRes, takeBackup: opts.get('no-backup') ? false : true, + applyDevPatch: opts.get('dev-patch') ? true : false, }); if (res) { log`{bold.rgb(245,245,245) SUCCESS} {green ✔}`; diff --git a/insert/api b/insert/api index aff6f2d..465e5a1 160000 --- a/insert/api +++ b/insert/api @@ -1 +1 @@ -Subproject commit aff6f2dafa9d2666306f4e088da86528aeee0cd8 +Subproject commit 465e5a10ccf526ea0f6567650c0603d44ecd631c diff --git a/insert/client.mjs b/insert/client.mjs new file mode 100644 index 0000000..a0c6d71 --- /dev/null +++ b/insert/client.mjs @@ -0,0 +1,34 @@ +/* + * notion-enhancer core: api + * (c) 2021 dragonwocky (https://dragonwocky.me/) + * (https://notion-enhancer.github.io/) under the MIT license + */ + +'use strict'; + +(async () => { + const page = location.pathname.split(/[/-]/g).reverse()[0].length === 32, + whitelisted = ['/', '/onboarding'].includes(location.pathname), + signedIn = localStorage['LRU:KeyValueStore2:current-user-id']; + + if (page || (whitelisted && signedIn)) { + const api = await import('./api/_.mjs'), + { fs, registry, web } = api; + + for (const mod of await registry.list((mod) => registry.enabled(mod.id))) { + for (const sheet of mod.css?.client || []) { + web.loadStylesheet(`repo/${mod._dir}/${sheet}`); + } + for (let script of mod.js?.client || []) { + script = await import(fs.localPath(`repo/${mod._dir}/${script}`)); + script.default(api, await registry.db(mod.id)); + } + } + + const errors = await registry.errors(); + if (errors.length) { + console.log('[notion-enhancer] registry errors:'); + console.table(errors); + } + } +})(); diff --git a/insert/dep b/insert/dep index 9a3893f..65ae194 160000 --- a/insert/dep +++ b/insert/dep @@ -1 +1 @@ -Subproject commit 9a3893fbd5af4d02b89ea4c6f2b35971a3a91408 +Subproject commit 65ae1944a75b9525ee79610586438facf14d8531 diff --git a/insert/env/env.cjs b/insert/env/env.cjs new file mode 100644 index 0000000..e62fc1c --- /dev/null +++ b/insert/env/env.cjs @@ -0,0 +1,48 @@ +/* + * notion-enhancer core: api + * (c) 2021 dragonwocky (https://dragonwocky.me/) + * (https://notion-enhancer.github.io/) under the MIT license + */ + +'use strict'; + +/** + * environment-specific methods and constants + * @module notion-enhancer/api/env + */ + +module.exports = {}; + +/** + * the environment/platform name code is currently being executed in + * @constant + * @type {string} + */ +module.exports.name = process.platform; + +/** + * the current version of the enhancer + * @constant + * @type {string} + */ +module.exports.version = require('notion-enhancer/package.json').version; + +/** + * open the enhancer's menu + * @type {function} + */ +module.exports.focusMenu = () => console.log(1); +// window.__enhancerElectronApi.sendMessage({ action: 'focusMenu' }); + +/** + * focus an active notion tab + * @type {function} + */ +module.exports.focusNotion = () => console.log(1); +// window.__enhancerElectronApi.sendMessage({ action: 'focusNotion' }); + +/** + * reload all notion and enhancer menu tabs to apply changes + * @type {function} + */ +module.exports.reload = () => console.log(1); // window.__enhancerElectronApi.sendMessage({ action: 'reload' }); diff --git a/insert/env/env.mjs b/insert/env/env.mjs index 705c578..5d41e54 100644 --- a/insert/env/env.mjs +++ b/insert/env/env.mjs @@ -16,29 +16,31 @@ * @constant * @type {string} */ -export const name = process.platform; +export const name = window.__enhancerElectronApi.platform; /** * the current version of the enhancer * @constant * @type {string} */ -export const version = chrome.runtime.getManifest().version; +export const version = window.__enhancerElectronApi.version; /** * open the enhancer's menu * @type {function} */ -export const focusMenu = () => chrome.runtime.sendMessage({ action: 'focusMenu' }); +export const focusMenu = () => + window.__enhancerElectronApi.sendMessage({ action: 'focusMenu' }); /** * focus an active notion tab * @type {function} */ -export const focusNotion = () => chrome.runtime.sendMessage({ action: 'focusNotion' }); +export const focusNotion = () => + window.__enhancerElectronApi.sendMessage({ action: 'focusNotion' }); /** * reload all notion and enhancer menu tabs to apply changes * @type {function} */ -export const reload = () => chrome.runtime.sendMessage({ action: 'reload' }); +export const reload = () => window.__enhancerElectronApi.sendMessage({ action: 'reload' }); diff --git a/insert/env/fs.cjs b/insert/env/fs.cjs new file mode 100644 index 0000000..0bc3071 --- /dev/null +++ b/insert/env/fs.cjs @@ -0,0 +1,53 @@ +/* + * notion-enhancer core: api + * (c) 2021 dragonwocky (https://dragonwocky.me/) + * (https://notion-enhancer.github.io/) under the MIT license + */ + +'use strict'; + +/** + * environment-specific file reading + * @module notion-enhancer/api/fs + */ + +module.exports = {}; + +/** + * transform a path relative to the enhancer root directory into an absolute path + * @param {string} path - a url or within-the-enhancer filepath + * @returns {string} an absolute filepath + */ +module.exports.localPath = (path) => `notion://www.notion.so/__notion-enhancer/${path}`; + +/** + * fetch and parse a json file's contents + * @param {string} path - a url or within-the-enhancer filepath + * @param {object} [opts] - the second argument of a fetch() request + * @returns {object} the json value of the requested file as a js object + */ +module.exports.getJSON = (path, opts = {}) => + fetch(path.startsWith('http') ? path : localPath(path), opts).then((res) => res.json()); + +/** + * fetch a text file's contents + * @param {string} path - a url or within-the-enhancer filepath + * @param {object} [opts] - the second argument of a fetch() request + * @returns {string} the text content of the requested file + */ +module.exports.getText = (path, opts = {}) => + fetch(path.startsWith('http') ? path : localPath(path), opts).then((res) => res.text()); + +/** + * check if a file exists + * @param {string} path - a url or within-the-enhancer filepath + * @returns {boolean} whether or not the file exists + */ +module.exports.isFile = async (path) => { + try { + await fetch(path.startsWith('http') ? path : localPath(path)); + return true; + } catch { + return false; + } +}; diff --git a/insert/env/fs.mjs b/insert/env/fs.mjs index 376440f..5b6cdcb 100644 --- a/insert/env/fs.mjs +++ b/insert/env/fs.mjs @@ -7,7 +7,7 @@ 'use strict'; /** - * environment-specific filesystem reading + * environment-specific file reading * @module notion-enhancer/api/fs */ @@ -16,7 +16,7 @@ * @param {string} path - a url or within-the-enhancer filepath * @returns {string} an absolute filepath */ -export const localPath = chrome.runtime.getURL; +export const localPath = (path) => `notion://www.notion.so/__notion-enhancer/${path}`; /** * fetch and parse a json file's contents diff --git a/insert/env/storage.cjs b/insert/env/storage.cjs new file mode 100644 index 0000000..307c763 --- /dev/null +++ b/insert/env/storage.cjs @@ -0,0 +1,137 @@ +/* + * notion-enhancer core: api + * (c) 2021 dragonwocky (https://dragonwocky.me/) + * (https://notion-enhancer.github.io/) under the MIT license + */ + +'use strict'; + +/** + * environment-specific data persistence + * @module notion-enhancer/api/storage + */ + +module.exports = {}; + +const _queue = [], + _onChangeListeners = []; + +const os = require('os'), + path = require('path'), + fs = require('fs'), + _cacheFile = path.resolve(`${os.homedir()}/.notion-enhancer`); + +// handle leftover cache from prev versions +if (fs.existsSync(_cacheFile) && fs.lstatSync(_cacheFile).isDirectory()) { + fs.rmdirSync(_cacheFile); +} +if (!fs.existsSync(_cacheFile)) fs.writeFileSync(_cacheFile, '{}', 'utf8'); + +const getData = () => { + try { + return JSON.parse(fs.readFileSync(_cacheFile)); + } catch (err) { + return {}; + } + }, + saveData = (data) => fs.writeFileSync(_cacheFile, JSON.stringify(data)); + +/** + * get persisted data + * @param {array} path - the path of keys to the value being fetched + * @param {*} [fallback] - a default value if the path is not matched + * @returns {Promise} value ?? fallback + */ +module.exports.get = (path, fallback = undefined) => { + if (!path.length) return fallback; + const values = getData(); + let value = values; + while (path.length) { + if (value === undefined) { + value = fallback; + break; + } + value = value[path.shift()]; + } + return Promise.resolve(value ?? fallback); +}; + +/** + * persist data + * @param {array} path - the path of keys to the value being set + * @param {*} value - the data to save + * @returns {Promise} resolves when data has been saved + */ +module.exports.set = (path, value) => { + if (!path.length) return undefined; + const precursor = _queue[_queue.length - 1] || undefined, + interaction = new Promise(async (res, rej) => { + if (precursor !== undefined) { + await precursor; + _queue.shift(); + } + const pathClone = [...path], + values = getData(); + let pointer = values, + old; + while (path.length) { + const key = path.shift(); + if (!path.length) { + old = pointer[key]; + pointer[key] = value; + break; + } + pointer[key] = pointer[key] ?? {}; + pointer = pointer[key]; + } + saveData(values); + _onChangeListeners.forEach((listener) => + listener({ type: 'set', path: pathClone, new: value, old }) + ); + res(value); + }); + _queue.push(interaction); + return interaction; +}; + +/** + * create a wrapper for accessing a partition of the storage + * @param {array} namespace - the path of keys to prefix all storage requests with + * @param {function} [get] - the storage get function to be wrapped + * @param {function} [set] - the storage set function to be wrapped + * @returns {object} an object with the wrapped get/set functions + */ +module.exports.db = (namespace, getFunc = get, setFunc = set) => { + if (typeof namespace === 'string') namespace = [namespace]; + return { + get: (path = [], fallback = undefined) => getFunc([...namespace, ...path], fallback), + set: (path, value) => setFunc([...namespace, ...path], value), + }; +}; + +/** + * add an event listener for changes in storage + * @param {onStorageChangeCallback} callback - called whenever a change in + * storage is initiated from the current process + */ +module.exports.addChangeListener = (callback) => { + _onChangeListeners.push(callback); +}; + +/** + * remove a listener added with storage.addChangeListener + * @param {onStorageChangeCallback} callback + */ +module.exports.removeChangeListener = (callback) => { + _onChangeListeners = _onChangeListeners.filter((listener) => listener !== callback); +}; + +/** + * @callback onStorageChangeCallback + * @param {object} event + * @param {string} event.type - 'set' or 'reset' + * @param {string} event.namespace- the name of the store, e.g. a mod id + * @param {string} [event.key] - the key associated with the changed value + * @param {string} [event.new] - the new value being persisted to the store + * @param {string} [event.old] - the previous value associated with the key + */ diff --git a/insert/env/storage.mjs b/insert/env/storage.mjs index 3003231..a4cc72f 100644 --- a/insert/env/storage.mjs +++ b/insert/env/storage.mjs @@ -11,9 +11,6 @@ * @module notion-enhancer/api/storage */ -const _queue = [], - _onChangeListeners = []; - /** * get persisted data * @param {array} path - the path of keys to the value being fetched @@ -21,20 +18,7 @@ const _queue = [], * @returns {Promise} value ?? fallback */ export const get = (path, fallback = undefined) => { - if (!path.length) return fallback; - return new Promise((res, rej) => - chrome.storage.local.get(async (values) => { - let value = values; - while (path.length) { - if (value === undefined) { - value = fallback; - break; - } - value = value[path.shift()]; - } - res(value ?? fallback); - }) - ); + return window.__enhancerElectronApi.db.get(path, fallback); }; /** @@ -44,38 +28,7 @@ export const get = (path, fallback = undefined) => { * @returns {Promise} resolves when data has been saved */ export const set = (path, value) => { - if (!path.length) return undefined; - const precursor = _queue[_queue.length - 1] || undefined, - interaction = new Promise(async (res, rej) => { - if (precursor !== undefined) { - await precursor; - _queue.shift(); - } - const pathClone = [...path], - namespace = path[0]; - chrome.storage.local.get(async (values) => { - let pointer = values, - old; - while (path.length) { - const key = path.shift(); - if (!path.length) { - old = pointer[key]; - pointer[key] = value; - break; - } - pointer[key] = pointer[key] ?? {}; - pointer = pointer[key]; - } - chrome.storage.local.set({ [namespace]: values[namespace] }, () => { - _onChangeListeners.forEach((listener) => - listener({ type: 'set', path: pathClone, new: value, old }) - ); - res(value); - }); - }); - }); - _queue.push(interaction); - return interaction; + return window.__enhancerElectronApi.db.set(path, value); }; /** @@ -99,7 +52,7 @@ export const db = (namespace, getFunc = get, setFunc = set) => { * storage is initiated from the current process */ export const addChangeListener = (callback) => { - _onChangeListeners.push(callback); + return window.__enhancerElectronApi.db.addChangeListener(callback); }; /** @@ -107,7 +60,7 @@ export const addChangeListener = (callback) => { * @param {onStorageChangeCallback} callback */ export const removeChangeListener = (callback) => { - _onChangeListeners = _onChangeListeners.filter((listener) => listener !== callback); + return window.__enhancerElectronApi.db.removeChangeListener(callback); }; /** diff --git a/insert/init.cjs b/insert/init.cjs new file mode 100644 index 0000000..46680ec --- /dev/null +++ b/insert/init.cjs @@ -0,0 +1,33 @@ +/* + * notion-enhancer core: api + * (c) 2021 dragonwocky (https://dragonwocky.me/) + * (https://notion-enhancer.github.io/) under the MIT license + */ + +'use strict'; + +const api = require('notion-enhancer/api/_.cjs'); + +module.exports = async function (target, __exports) { + if (target === 'renderer/preload') { + window.__enhancerElectronApi = { + platform: process.platform, + version: require('notion-enhancer/package.json').version, + db: { + get: api.storage.get, + set: api.storage.set, + addChangeListener: api.storage.addChangeListener, + removeChangeListener: api.storage.removeChangeListener, + }, + sendMessage: (message) => {}, + }; + + document.addEventListener('readystatechange', (event) => { + if (document.readyState !== 'complete') return false; + const script = document.createElement('script'); + script.type = 'module'; + script.src = 'notion://www.notion.so/__notion-enhancer/client.mjs'; + document.head.appendChild(script); + }); + } +}; diff --git a/insert/init.js b/insert/init.js deleted file mode 100644 index 1cd0040..0000000 --- a/insert/init.js +++ /dev/null @@ -1,11 +0,0 @@ -/* - * notion-enhancer - * (c) 2020 dragonwocky (https://dragonwocky.me/) - * (https://dragonwocky.me/notion-enhancer) under the MIT license - */ - -'use strict'; - -module.exports = function (target, __exports) { - console.log(target); -}; diff --git a/insert/media b/insert/media index 0e56fb9..5472e23 160000 --- a/insert/media +++ b/insert/media @@ -1 +1 @@ -Subproject commit 0e56fb9242a00e41132b9ad30adef9ae910a2159 +Subproject commit 5472e23e983ab893b72af6493bbf582017c182be diff --git a/insert/repo b/insert/repo index 4c589ec..bede50b 160000 --- a/insert/repo +++ b/insert/repo @@ -1 +1 @@ -Subproject commit 4c589ec5915cccfb004098caf08e8934eb73ade7 +Subproject commit bede50bedaacfe198de2e9a0015b454295dbb1e4 diff --git a/package.json b/package.json index 2b0a70f..9c8033d 100644 --- a/package.json +++ b/package.json @@ -3,11 +3,12 @@ "version": "0.11.0-dev", "author": "dragonwocky (https://dragonwocky.me/)", "description": "an enhancer/customiser for the all-in-one productivity workspace notion.so", - "homepage": "https://github.com/notion-enhancer/notion-enhancer", + "homepage": "https://github.com/notion-enhancer/desktop", "license": "MIT", "bin": { "notion-enhancer": "bin.mjs" }, + "private": true, "type": "module", "engines": { "node": ">=16.x.x" diff --git a/pkg/apply.mjs b/pkg/apply.mjs index 3492b78..9293684 100644 --- a/pkg/apply.mjs +++ b/pkg/apply.mjs @@ -17,7 +17,7 @@ import remove from './remove.mjs'; export default async function ( notionFolder = findNotion(), - { overwritePrevious = undefined, takeBackup = true } = {} + { overwritePrevious = undefined, takeBackup = true, applyDevPatch = false } = {} ) { let status = check(notionFolder); switch (status.code) { @@ -26,8 +26,11 @@ export default async function ( case 1: // corrupted throw Error(status.message); case 2: // same version already applied - log` {grey * notion-enhancer v${status.version} already applied}`; - return true; + if (!applyDevPatch) { + log` {grey * notion-enhancer v${status.version} already applied}`; + return true; + } + break; case 3: // diff version already applied log` * ${status.message}`; const prompt = ['Y', 'y', 'N', 'n', ''], @@ -60,31 +63,33 @@ export default async function ( } s = spinner(' * inserting enhancements').loop(); - const notionFiles = (await readDirDeep(status.executable)) - .map((file) => file.path) - .filter((file) => file.endsWith('.js') && !file.includes('node_modules')); - for (const file of notionFiles) { - const target = file.slice(status.executable.length + 1, -3), - replacer = path.resolve(`${__dirname(import.meta)}/replacers/${target}.mjs`); - if (fs.existsSync(replacer)) { - await (await import(`./replacers/${target}.mjs`)).default(file); + if (!(status.code === 2 && applyDevPatch)) { + const notionFiles = (await readDirDeep(status.executable)) + .map((file) => file.path) + .filter((file) => file.endsWith('.js') && !file.includes('node_modules')); + for (const file of notionFiles) { + const target = file.slice(status.executable.length + 1, -3), + replacer = path.resolve(`${__dirname(import.meta)}/replacers/${target}.mjs`); + if (fs.existsSync(replacer)) { + await (await import(`./replacers/${target}.mjs`)).default(file); + } + await fsp.appendFile( + file, + `\n\n//notion-enhancer\nrequire('notion-enhancer')('${target}', exports))` + ); } - await fsp.appendFile( - file, - `\n\n//notion-enhancer\nrequire('notion-enhancer')('${target}', exports);` - ); } + const node_modules = path.resolve(`${status.executable}/node_modules/notion-enhancer`); + await copyDir(`${__dirname(import.meta)}/../insert`, node_modules); s.stop(); s = spinner(' * recording version').loop(); - const node_modules = path.resolve(`${status.executable}/node_modules/notion-enhancer`); - await copyDir(`${__dirname(import.meta)}/../insert`, node_modules); await fsp.writeFile( path.resolve(`${node_modules}/package.json`), `{ "name": "notion-enhancer", "version": "${pkg().version}", - "main": "launcher.js" + "main": "init.cjs" }` ); s.stop(); diff --git a/pkg/remove.mjs b/pkg/remove.mjs index 6fcfa35..561593d 100644 --- a/pkg/remove.mjs +++ b/pkg/remove.mjs @@ -43,4 +43,6 @@ export default async function (notionFolder = findNotion(), { delCache = undefin s.stop(); } } else log` {grey * enhancer cache not found: skipping}`; + + return true; } diff --git a/pkg/replacers/main/main.mjs b/pkg/replacers/main/main.mjs index f8b6fe6..2edfa6b 100644 --- a/pkg/replacers/main/main.mjs +++ b/pkg/replacers/main/main.mjs @@ -9,7 +9,7 @@ import fsp from 'fs/promises'; export default async function (filepath) { - // https://github.com/notion-enhancer/notion-enhancer/issues/160 + // https://github.com/notion-enhancer/desktop/issues/160 // enable the notion:// url scheme/protocol on linux const contents = await fsp.readFile(filepath, 'utf8'); await fsp.writeFile( diff --git a/pkg/replacers/main/schemeHandler.mjs b/pkg/replacers/main/schemeHandler.mjs new file mode 100644 index 0000000..5f21cfe --- /dev/null +++ b/pkg/replacers/main/schemeHandler.mjs @@ -0,0 +1,43 @@ +/* + * notion-enhancer + * (c) 2021 dragonwocky (https://dragonwocky.me/) + * (https://notion-enhancer.github.io/) under the MIT license + */ + +'use strict'; + +import fsp from 'fs/promises'; + +export default async function (filepath) { + // https://github.com/notion-enhancer/desktop/issues/291 + // bypass csp issues by intercepting notion:// protocol + const contents = await fsp.readFile(filepath, 'utf8'); + await fsp.writeFile( + filepath, + contents.replace( + /const success = protocol\.registerStreamProtocol\(config_1.default.protocol, async \(req, callback\) => \{/, + `const success = protocol.registerStreamProtocol(config_1.default.protocol, async (req, callback) => { + { + // notion-enhancer + const schemePrefix = 'notion://www.notion.so/__notion-enhancer/'; + if (req.url.startsWith(schemePrefix)) { + const resolvePath = (path) => require('path').resolve(\`\${__dirname}/\${path}\`), + fileExt = req.url.split('.').reverse()[0], + filePath = resolvePath( + \`../node_modules/notion-enhancer/\${req.url.slice(schemePrefix.length)}\` + ), + mimeDB = Object.entries(require('notion-enhancer/dep/mime-db.json')), + mimeType = mimeDB + .filter(([mime, data]) => data.extensions) + .find(([mime, data]) => data.extensions.includes(fileExt)); + callback({ + data: require('fs').createReadStream(filePath), + headers: { 'content-type': mimeType }, + }); + } + }` + ) + ); + + return true; +}