|
|
@@ -1,8 +1,7 @@
|
|
|
import {
|
|
|
- i18n, getFullUrl, isRemote, getRnd4,
|
|
|
+ i18n, getFullUrl, isRemote, getRnd4, sendCmd,
|
|
|
} from '#/common';
|
|
|
-import { objectGet, objectSet } from '#/common/object';
|
|
|
-import { CMD_SCRIPT_ADD, CMD_SCRIPT_UPDATE } from '#/common/consts';
|
|
|
+import { CMD_SCRIPT_ADD, CMD_SCRIPT_UPDATE, TIMEOUT_WEEK } from '#/common/consts';
|
|
|
import storage from '#/common/storage';
|
|
|
import pluginEvents from '../plugin/events';
|
|
|
import {
|
|
|
@@ -10,9 +9,9 @@ import {
|
|
|
} from './script';
|
|
|
import { testScript, testBlacklist } from './tester';
|
|
|
import { register } from './init';
|
|
|
+import { commands } from './message';
|
|
|
import patchDB from './patch-db';
|
|
|
import { setOption } from './options';
|
|
|
-import { sendMessageOrIgnore } from './message';
|
|
|
|
|
|
const store = {};
|
|
|
|
|
|
@@ -20,152 +19,173 @@ storage.script.onDump = (item) => {
|
|
|
store.scriptMap[item.props.id] = item;
|
|
|
};
|
|
|
|
|
|
-register(initialize());
|
|
|
-
|
|
|
-function initialize() {
|
|
|
- return browser.storage.local.get('version')
|
|
|
- .then(({ version: lastVersion }) => {
|
|
|
- const { version } = browser.runtime.getManifest();
|
|
|
- return (lastVersion ? Promise.resolve() : patchDB())
|
|
|
- .then(() => {
|
|
|
- if (version !== lastVersion) return browser.storage.local.set({ version });
|
|
|
+Object.assign(commands, {
|
|
|
+ CheckPosition: sortScripts,
|
|
|
+ CheckRemove: checkRemove,
|
|
|
+ CheckScript({ name, namespace }) {
|
|
|
+ const script = getScript({ meta: { name, namespace } });
|
|
|
+ return script && !script.config.removed
|
|
|
+ ? script.meta.version
|
|
|
+ : null;
|
|
|
+ },
|
|
|
+ ExportZip({ values }) {
|
|
|
+ return getExportData(values);
|
|
|
+ },
|
|
|
+ GetScriptCode: getScriptCode,
|
|
|
+ GetMetas: getScriptByIds,
|
|
|
+ MarkRemoved({ id, removed }) {
|
|
|
+ return markRemoved(id, removed);
|
|
|
+ },
|
|
|
+ Move({ id, offset }) {
|
|
|
+ return moveScript(id, offset);
|
|
|
+ },
|
|
|
+ RemoveScript(id) {
|
|
|
+ return removeScript(id);
|
|
|
+ },
|
|
|
+ ParseMeta: parseMeta,
|
|
|
+ ParseScript: parseScript,
|
|
|
+ UpdateScriptInfo({ id, config }) {
|
|
|
+ return updateScriptInfo(id, {
|
|
|
+ config,
|
|
|
+ props: { lastModified: Date.now() },
|
|
|
});
|
|
|
- })
|
|
|
- .then(() => browser.storage.local.get())
|
|
|
- .then((data) => {
|
|
|
- const scripts = [];
|
|
|
- const storeInfo = {
|
|
|
- id: 0,
|
|
|
- position: 0,
|
|
|
- };
|
|
|
- const idMap = {};
|
|
|
- const uriMap = {};
|
|
|
- Object.keys(data).forEach((key) => {
|
|
|
- const script = data[key];
|
|
|
- if (key.startsWith('scr:')) {
|
|
|
- // {
|
|
|
- // meta,
|
|
|
- // custom,
|
|
|
- // props: { id, position, uri },
|
|
|
- // config: { enabled, shouldUpdate },
|
|
|
- // }
|
|
|
- const id = getInt(key.slice(4));
|
|
|
- if (!id || idMap[id]) {
|
|
|
- // ID conflicts!
|
|
|
- // Should not happen, discard duplicates.
|
|
|
- return;
|
|
|
- }
|
|
|
- idMap[id] = script;
|
|
|
- const uri = getNameURI(script);
|
|
|
- if (uriMap[uri]) {
|
|
|
- // Namespace conflicts!
|
|
|
- // Should not happen, discard duplicates.
|
|
|
- return;
|
|
|
- }
|
|
|
- uriMap[uri] = script;
|
|
|
- script.props = {
|
|
|
- ...script.props,
|
|
|
- id,
|
|
|
- uri,
|
|
|
- };
|
|
|
- script.custom = {
|
|
|
- ...getDefaultCustom(),
|
|
|
- ...script.custom,
|
|
|
- };
|
|
|
- storeInfo.id = Math.max(storeInfo.id, id);
|
|
|
- storeInfo.position = Math.max(storeInfo.position, getInt(objectGet(script, 'props.position')));
|
|
|
- scripts.push(script);
|
|
|
+ },
|
|
|
+ Vacuum: vacuum,
|
|
|
+});
|
|
|
+
|
|
|
+register(async () => {
|
|
|
+ const { version: lastVersion } = await browser.storage.local.get('version');
|
|
|
+ const { version } = browser.runtime.getManifest();
|
|
|
+ if (!lastVersion) await patchDB();
|
|
|
+ if (version !== lastVersion) browser.storage.local.set({ version });
|
|
|
+ const data = await browser.storage.local.get();
|
|
|
+ const scripts = [];
|
|
|
+ const storeInfo = {
|
|
|
+ id: 0,
|
|
|
+ position: 0,
|
|
|
+ };
|
|
|
+ const idMap = {};
|
|
|
+ const uriMap = {};
|
|
|
+ Object.entries(data).forEach(([key, script]) => {
|
|
|
+ if (key.startsWith('scr:')) {
|
|
|
+ // {
|
|
|
+ // meta,
|
|
|
+ // custom,
|
|
|
+ // props: { id, position, uri },
|
|
|
+ // config: { enabled, shouldUpdate },
|
|
|
+ // }
|
|
|
+ const id = getInt(key.slice(4));
|
|
|
+ if (!id || idMap[id]) {
|
|
|
+ // ID conflicts!
|
|
|
+ // Should not happen, discard duplicates.
|
|
|
+ return;
|
|
|
}
|
|
|
- });
|
|
|
- Object.assign(store, {
|
|
|
- scripts,
|
|
|
- storeInfo,
|
|
|
- scriptMap: scripts.reduce((map, item) => {
|
|
|
- map[item.props.id] = item;
|
|
|
- return map;
|
|
|
- }, {}),
|
|
|
- });
|
|
|
- if (process.env.DEBUG) {
|
|
|
- console.log('store:', store); // eslint-disable-line no-console
|
|
|
+ idMap[id] = script;
|
|
|
+ const uri = getNameURI(script);
|
|
|
+ if (uriMap[uri]) {
|
|
|
+ // Namespace conflicts!
|
|
|
+ // Should not happen, discard duplicates.
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ uriMap[uri] = script;
|
|
|
+ script.props = {
|
|
|
+ ...script.props,
|
|
|
+ id,
|
|
|
+ uri,
|
|
|
+ };
|
|
|
+ script.custom = {
|
|
|
+ ...getDefaultCustom(),
|
|
|
+ ...script.custom,
|
|
|
+ };
|
|
|
+ storeInfo.id = Math.max(storeInfo.id, id);
|
|
|
+ storeInfo.position = Math.max(storeInfo.position, getInt(script.props.position));
|
|
|
+ scripts.push(script);
|
|
|
}
|
|
|
- return sortScripts();
|
|
|
});
|
|
|
-}
|
|
|
+ Object.assign(store, {
|
|
|
+ scripts,
|
|
|
+ storeInfo,
|
|
|
+ scriptMap: scripts.reduce((map, item) => {
|
|
|
+ map[item.props.id] = item;
|
|
|
+ return map;
|
|
|
+ }, {}),
|
|
|
+ });
|
|
|
+ if (process.env.DEBUG) {
|
|
|
+ console.log('store:', store); // eslint-disable-line no-console
|
|
|
+ }
|
|
|
+ return sortScripts();
|
|
|
+});
|
|
|
|
|
|
+/** @return {number} */
|
|
|
function getInt(val) {
|
|
|
return +val || 0;
|
|
|
}
|
|
|
|
|
|
+/** @return {void} */
|
|
|
function updateLastModified() {
|
|
|
setOption('lastModified', Date.now());
|
|
|
}
|
|
|
|
|
|
-export function normalizePosition() {
|
|
|
- const updates = [];
|
|
|
- const positionKey = 'props.position';
|
|
|
- store.scripts.forEach((item, index) => {
|
|
|
+/** @return {Promise<number>} */
|
|
|
+export async function normalizePosition() {
|
|
|
+ const updates = store.scripts.filter(({ props }, index) => {
|
|
|
const position = index + 1;
|
|
|
- if (objectGet(item, positionKey) !== position) {
|
|
|
- objectSet(item, positionKey, position);
|
|
|
- updates.push(item);
|
|
|
- }
|
|
|
+ const res = props.position !== position;
|
|
|
+ if (res) props.position = position;
|
|
|
+ return res;
|
|
|
});
|
|
|
store.storeInfo.position = store.scripts.length;
|
|
|
- const { length } = updates;
|
|
|
- if (!length) return Promise.resolve();
|
|
|
- return storage.script.dump(updates)
|
|
|
- .then(() => {
|
|
|
+ if (updates.length) {
|
|
|
+ await storage.script.dump(updates);
|
|
|
updateLastModified();
|
|
|
- return length;
|
|
|
- });
|
|
|
+ }
|
|
|
+ return updates.length;
|
|
|
}
|
|
|
|
|
|
-export function sortScripts() {
|
|
|
- store.scripts.sort((a, b) => {
|
|
|
- const [pos1, pos2] = [a, b].map(item => getInt(objectGet(item, 'props.position')));
|
|
|
- return pos1 - pos2;
|
|
|
- });
|
|
|
- return normalizePosition()
|
|
|
- .then((changed) => {
|
|
|
- sendMessageOrIgnore({ cmd: 'ScriptsUpdated' });
|
|
|
- return changed;
|
|
|
- });
|
|
|
+/** @return {Promise<number>} */
|
|
|
+export async function sortScripts() {
|
|
|
+ store.scripts.sort((a, b) => getInt(a.props.position) - getInt(b.props.position));
|
|
|
+ const changed = await normalizePosition();
|
|
|
+ sendCmd('ScriptsUpdated', null);
|
|
|
+ return changed;
|
|
|
}
|
|
|
|
|
|
-// TODO: depromisify getScript and all dependent code
|
|
|
-export function getScriptByIdSync(id) {
|
|
|
+/** @return {VMScript} */
|
|
|
+export function getScriptById(id) {
|
|
|
return store.scriptMap[id];
|
|
|
}
|
|
|
|
|
|
-export function getScript(where) {
|
|
|
+/** @return {VMScript} */
|
|
|
+export function getScript({ id, uri, meta }) {
|
|
|
let script;
|
|
|
- if (where.id) {
|
|
|
- script = store.scriptMap[where.id];
|
|
|
+ if (id) {
|
|
|
+ script = getScriptById(id);
|
|
|
} else {
|
|
|
- const uri = where.uri || getNameURI({ meta: where.meta, id: '@@should-have-name' });
|
|
|
- const predicate = item => uri === objectGet(item, 'props.uri');
|
|
|
- script = store.scripts.find(predicate);
|
|
|
+ if (!uri) uri = getNameURI({ meta, id: '@@should-have-name' });
|
|
|
+ script = store.scripts.find(({ props }) => uri === props.uri);
|
|
|
}
|
|
|
- return Promise.resolve(script);
|
|
|
+ return script;
|
|
|
}
|
|
|
|
|
|
+/** @return {VMScript[]} */
|
|
|
export function getScripts() {
|
|
|
- return Promise.resolve(store.scripts)
|
|
|
- .then(scripts => scripts.filter(script => !script.config.removed));
|
|
|
+ return store.scripts.filter(script => !script.config.removed);
|
|
|
}
|
|
|
|
|
|
+/** @return {VMScript[]} */
|
|
|
export function getScriptByIds(ids) {
|
|
|
- return Promise.all(ids.map(id => getScript({ id })))
|
|
|
- .then(scripts => scripts.filter(Boolean));
|
|
|
+ return ids.map(getScriptById).filter(Boolean);
|
|
|
}
|
|
|
|
|
|
+/** @return {Promise<string>} */
|
|
|
export function getScriptCode(id) {
|
|
|
return storage.code.getOne(id);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @desc Load values for batch updates.
|
|
|
- * @param {Array} ids
|
|
|
+ * @param {number[]} ids
|
|
|
+ * @return {Promise}
|
|
|
*/
|
|
|
export function getValueStoresByIds(ids) {
|
|
|
return storage.value.getMulti(ids);
|
|
|
@@ -174,21 +194,18 @@ export function getValueStoresByIds(ids) {
|
|
|
/**
|
|
|
* @desc Dump values for batch updates.
|
|
|
* @param {Object} valueDict { id1: value1, id2: value2, ... }
|
|
|
+ * @return {Promise}
|
|
|
*/
|
|
|
-export function dumpValueStores(valueDict) {
|
|
|
- if (process.env.DEBUG) {
|
|
|
- console.info('Update value stores', valueDict);
|
|
|
- }
|
|
|
- return storage.value.dump(valueDict).then(() => valueDict);
|
|
|
+export async function dumpValueStores(valueDict) {
|
|
|
+ if (process.env.DEBUG) console.info('Update value stores', valueDict);
|
|
|
+ await storage.value.dump(valueDict);
|
|
|
+ return valueDict;
|
|
|
}
|
|
|
|
|
|
-export function dumpValueStore(where, valueStore) {
|
|
|
- return (where.id
|
|
|
- ? Promise.resolve(where.id)
|
|
|
- : getScript(where).then(script => objectGet(script, 'props.id')))
|
|
|
- .then((id) => {
|
|
|
- if (id) return dumpValueStores({ [id]: valueStore });
|
|
|
- });
|
|
|
+/** @return {Promise<Object|undefined>} */
|
|
|
+export async function dumpValueStore(where, valueStore) {
|
|
|
+ const id = where.id || getScript(where)?.props.id;
|
|
|
+ return id && dumpValueStores({ [id]: valueStore });
|
|
|
}
|
|
|
|
|
|
const gmValues = [
|
|
|
@@ -200,8 +217,9 @@ const gmValues = [
|
|
|
|
|
|
/**
|
|
|
* @desc Get scripts to be injected to page with specific URL.
|
|
|
+ * @return {Promise}
|
|
|
*/
|
|
|
-export function getScriptsByURL(url) {
|
|
|
+export async function getScriptsByURL(url) {
|
|
|
const scripts = testBlacklist(url)
|
|
|
? []
|
|
|
: store.scripts.filter(script => !script.config.removed && testScript(url, script));
|
|
|
@@ -223,46 +241,49 @@ export function getScriptsByURL(url) {
|
|
|
.filter(script => script.config.enabled);
|
|
|
const scriptsWithValue = enabledScripts
|
|
|
.filter(script => script.meta.grant?.some(gm => gmValues.includes(gm)));
|
|
|
- return Promise.all([
|
|
|
+ const [require, cache, values, code] = await Promise.all([
|
|
|
storage.require.getMulti(Object.keys(reqKeys)),
|
|
|
storage.cache.getMulti(Object.keys(cacheKeys)),
|
|
|
storage.value.getMulti(scriptsWithValue.map(script => script.props.id), {}),
|
|
|
storage.code.getMulti(enabledScripts.map(script => script.props.id)),
|
|
|
- ])
|
|
|
- .then(([require, cache, values, code]) => ({
|
|
|
+ ]);
|
|
|
+ return {
|
|
|
scripts,
|
|
|
require,
|
|
|
cache,
|
|
|
values,
|
|
|
code,
|
|
|
- }));
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+/** @return {string[]} */
|
|
|
+function getIconUrls() {
|
|
|
+ return store.scripts.reduce((res, script) => {
|
|
|
+ const { icon } = script.meta;
|
|
|
+ if (isRemote(icon)) {
|
|
|
+ res.push(script.custom.pathMap?.[icon] || icon);
|
|
|
+ }
|
|
|
+ return res;
|
|
|
+ }, []);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
* @desc Get data for dashboard.
|
|
|
+ * @return {Promise}
|
|
|
*/
|
|
|
-export function getData() {
|
|
|
- const cacheKeys = {};
|
|
|
- const { scripts } = store;
|
|
|
- scripts.forEach((script) => {
|
|
|
- const icon = objectGet(script, 'meta.icon');
|
|
|
- if (isRemote(icon)) {
|
|
|
- const pathMap = objectGet(script, 'custom.pathMap') || {};
|
|
|
- const fullUrl = pathMap[icon] || icon;
|
|
|
- cacheKeys[fullUrl] = 1;
|
|
|
- }
|
|
|
- });
|
|
|
- return storage.cache.getMulti(Object.keys(cacheKeys))
|
|
|
- .then(cache => ({ scripts, cache }));
|
|
|
+export async function getData() {
|
|
|
+ return {
|
|
|
+ scripts: store.scripts,
|
|
|
+ cache: await storage.cache.getMulti(getIconUrls()),
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
+/** @return {number} */
|
|
|
export function checkRemove({ force } = {}) {
|
|
|
const now = Date.now();
|
|
|
- const toRemove = store.scripts.filter((script) => {
|
|
|
- if (!script.config.removed) return false;
|
|
|
- const lastModified = +script.props.lastModified || 0;
|
|
|
- return force || now - lastModified > 7 * 24 * 60 * 60 * 1000;
|
|
|
- });
|
|
|
+ const toRemove = store.scripts.filter(script => script.config.removed && (
|
|
|
+ force || now - getInt(script.props.lastModified) > TIMEOUT_WEEK
|
|
|
+ ));
|
|
|
if (toRemove.length) {
|
|
|
store.scripts = store.scripts.filter(script => !script.config.removed);
|
|
|
const ids = toRemove.map(script => script.props.id);
|
|
|
@@ -270,24 +291,24 @@ export function checkRemove({ force } = {}) {
|
|
|
storage.code.removeMulti(ids);
|
|
|
storage.value.removeMulti(ids);
|
|
|
}
|
|
|
- return Promise.resolve(toRemove.length);
|
|
|
+ return toRemove.length;
|
|
|
}
|
|
|
|
|
|
-export function removeScript(id) {
|
|
|
- const i = store.scripts.findIndex(item => id === objectGet(item, 'props.id'));
|
|
|
+/** @return {Promise} */
|
|
|
+export async function removeScript(id) {
|
|
|
+ const i = store.scripts.indexOf(getScriptById(id));
|
|
|
if (i >= 0) {
|
|
|
store.scripts.splice(i, 1);
|
|
|
- storage.script.remove(id);
|
|
|
- storage.code.remove(id);
|
|
|
- storage.value.remove(id);
|
|
|
+ await Promise.all([
|
|
|
+ storage.script.remove(id),
|
|
|
+ storage.code.remove(id),
|
|
|
+ storage.value.remove(id),
|
|
|
+ ]);
|
|
|
}
|
|
|
- sendMessageOrIgnore({
|
|
|
- cmd: 'RemoveScript',
|
|
|
- data: id,
|
|
|
- });
|
|
|
- return Promise.resolve();
|
|
|
+ return sendCmd('RemoveScript', id);
|
|
|
}
|
|
|
|
|
|
+/** @return {Promise} */
|
|
|
export function markRemoved(id, removed) {
|
|
|
return updateScriptInfo(id, {
|
|
|
config: {
|
|
|
@@ -299,8 +320,9 @@ export function markRemoved(id, removed) {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
+/** @return {Promise<number>} */
|
|
|
export function moveScript(id, offset) {
|
|
|
- const index = store.scripts.findIndex(item => id === objectGet(item, 'props.id'));
|
|
|
+ const index = store.scripts.indexOf(getScriptById(id));
|
|
|
const step = offset > 0 ? 1 : -1;
|
|
|
const indexStart = index;
|
|
|
const indexEnd = index + offset;
|
|
|
@@ -320,12 +342,18 @@ export function moveScript(id, offset) {
|
|
|
return normalizePosition();
|
|
|
}
|
|
|
|
|
|
+/** @return {string} */
|
|
|
function getUUID(id) {
|
|
|
const idSec = (id + 0x10bde6a2).toString(16).slice(-8);
|
|
|
return `${idSec}-${getRnd4()}-${getRnd4()}-${getRnd4()}-${getRnd4()}${getRnd4()}${getRnd4()}`;
|
|
|
}
|
|
|
|
|
|
-function saveScript(script, code) {
|
|
|
+/**
|
|
|
+ * @param {VMScript} script
|
|
|
+ * @param {string} code
|
|
|
+ * @return {Promise<Array>} [VMScript, codeString]
|
|
|
+ */
|
|
|
+async function saveScript(script, code) {
|
|
|
const config = script.config || {};
|
|
|
config.enabled = getInt(config.enabled);
|
|
|
config.shouldUpdate = getInt(config.shouldUpdate);
|
|
|
@@ -340,10 +368,7 @@ function saveScript(script, code) {
|
|
|
props.uri = getNameURI(script);
|
|
|
props.uuid = props.uuid || getUUID(props.id);
|
|
|
// Do not allow script with same name and namespace
|
|
|
- if (store.scripts.some((item) => {
|
|
|
- const itemProps = item.props || {};
|
|
|
- return props.id !== itemProps.id && props.uri === itemProps.uri;
|
|
|
- })) {
|
|
|
+ if (store.scripts.some(({ props: { id, uri } = {} }) => props.id !== id && props.uri === uri)) {
|
|
|
throw i18n('msgNamespaceConflict');
|
|
|
}
|
|
|
if (oldScript) {
|
|
|
@@ -368,96 +393,83 @@ function saveScript(script, code) {
|
|
|
]);
|
|
|
}
|
|
|
|
|
|
-export function updateScriptInfo(id, data) {
|
|
|
+/** @return {Promise} */
|
|
|
+export async function updateScriptInfo(id, data) {
|
|
|
const script = store.scriptMap[id];
|
|
|
- if (!script) return Promise.reject();
|
|
|
+ if (!script) throw null;
|
|
|
script.props = Object.assign({}, script.props, data.props);
|
|
|
script.config = Object.assign({}, script.config, data.config);
|
|
|
// script.custom = Object.assign({}, script.custom, data.custom);
|
|
|
- return storage.script.dump(script)
|
|
|
- .then(() => sendMessageOrIgnore({
|
|
|
- cmd: CMD_SCRIPT_UPDATE,
|
|
|
- data: {
|
|
|
- where: { id },
|
|
|
- update: script,
|
|
|
- },
|
|
|
- }));
|
|
|
+ await storage.script.dump(script);
|
|
|
+ return sendCmd(CMD_SCRIPT_UPDATE, { where: { id }, update: script });
|
|
|
}
|
|
|
|
|
|
-export function getExportData(withValues) {
|
|
|
- return getScripts()
|
|
|
- .then((scripts) => {
|
|
|
- const ids = scripts.map(({ props: { id } }) => id);
|
|
|
- return storage.code.getMulti(ids)
|
|
|
- .then((codeMap) => {
|
|
|
- const data = {};
|
|
|
- data.items = scripts.map(script => ({ script, code: codeMap[script.props.id] }));
|
|
|
- if (withValues) {
|
|
|
- return storage.value.getMulti(ids)
|
|
|
- .then((values) => {
|
|
|
- data.values = values;
|
|
|
- return data;
|
|
|
- });
|
|
|
- }
|
|
|
- return data;
|
|
|
- });
|
|
|
- });
|
|
|
+/** @return {Promise} */
|
|
|
+export async function getExportData(withValues) {
|
|
|
+ const scripts = getScripts();
|
|
|
+ const ids = scripts.map(({ props: { id } }) => id);
|
|
|
+ const codeMap = await storage.code.getMulti(ids);
|
|
|
+ return {
|
|
|
+ items: scripts.map(script => ({ script, code: codeMap[script.props.id] })),
|
|
|
+ ...withValues && {
|
|
|
+ values: await storage.value.getMulti(ids),
|
|
|
+ },
|
|
|
+ };
|
|
|
}
|
|
|
|
|
|
-export function parseScript(data) {
|
|
|
- const {
|
|
|
- id, code, message, isNew, config, custom, props, update,
|
|
|
- } = data;
|
|
|
- const meta = parseMeta(code);
|
|
|
- if (!meta.name) return Promise.reject(i18n('msgInvalidScript'));
|
|
|
+/** @return {Promise} */
|
|
|
+export async function parseScript(src) {
|
|
|
+ const meta = parseMeta(src.code);
|
|
|
+ if (!meta.name) throw i18n('msgInvalidScript');
|
|
|
const result = {
|
|
|
- cmd: CMD_SCRIPT_UPDATE,
|
|
|
- data: {
|
|
|
- update: {
|
|
|
- message: message == null ? i18n('msgUpdated') : message || '',
|
|
|
- },
|
|
|
+ update: {
|
|
|
+ message: src.message == null ? i18n('msgUpdated') : src.message || '',
|
|
|
},
|
|
|
};
|
|
|
- return getScript({ id, meta })
|
|
|
- .then((oldScript) => {
|
|
|
- let script;
|
|
|
- if (oldScript) {
|
|
|
- if (isNew) throw i18n('msgNamespaceConflict');
|
|
|
- script = Object.assign({}, oldScript);
|
|
|
- } else {
|
|
|
- ({ script } = newScript());
|
|
|
- result.cmd = CMD_SCRIPT_ADD;
|
|
|
- result.data.isNew = true;
|
|
|
- result.data.update.message = i18n('msgInstalled');
|
|
|
- }
|
|
|
- script.config = Object.assign({}, script.config, config, {
|
|
|
- removed: 0, // force reset `removed` since this is an installation
|
|
|
- });
|
|
|
- script.custom = Object.assign({}, script.custom, custom);
|
|
|
- script.props = Object.assign({}, script.props, {
|
|
|
- lastModified: Date.now(),
|
|
|
- lastUpdated: Date.now(),
|
|
|
- }, props);
|
|
|
- script.meta = meta;
|
|
|
- if (!meta.homepageURL && !script.custom.homepageURL && isRemote(data.from)) {
|
|
|
- script.custom.homepageURL = data.from;
|
|
|
- }
|
|
|
- if (isRemote(data.url)) script.custom.lastInstallURL = data.url;
|
|
|
- const position = +data.position;
|
|
|
- if (position) objectSet(script, 'props.position', position);
|
|
|
- buildPathMap(script, data.url);
|
|
|
- return saveScript(script, code).then(() => script);
|
|
|
- })
|
|
|
- .then((script) => {
|
|
|
- fetchScriptResources(script, data);
|
|
|
- Object.assign(result.data.update, script, update);
|
|
|
- result.data.where = { id: script.props.id };
|
|
|
- sendMessageOrIgnore(result);
|
|
|
- pluginEvents.emit('scriptChanged', result.data);
|
|
|
- return result;
|
|
|
- });
|
|
|
+ let cmd = CMD_SCRIPT_UPDATE;
|
|
|
+ let script;
|
|
|
+ const oldScript = await getScript({ id: src.id, meta });
|
|
|
+ if (oldScript) {
|
|
|
+ if (src.isNew) throw i18n('msgNamespaceConflict');
|
|
|
+ script = { ...oldScript };
|
|
|
+ } else {
|
|
|
+ ({ script } = newScript());
|
|
|
+ cmd = CMD_SCRIPT_ADD;
|
|
|
+ result.isNew = true;
|
|
|
+ result.update.message = i18n('msgInstalled');
|
|
|
+ }
|
|
|
+ script.config = {
|
|
|
+ ...script.config,
|
|
|
+ ...src.config,
|
|
|
+ removed: 0, // force reset `removed` since this is an installation
|
|
|
+ };
|
|
|
+ script.custom = {
|
|
|
+ ...script.custom,
|
|
|
+ ...src.custom,
|
|
|
+ };
|
|
|
+ script.props = {
|
|
|
+ ...script.props,
|
|
|
+ lastModified: Date.now(),
|
|
|
+ lastUpdated: Date.now(),
|
|
|
+ ...src.props,
|
|
|
+ };
|
|
|
+ script.meta = meta;
|
|
|
+ if (!meta.homepageURL && !script.custom.homepageURL && isRemote(src.from)) {
|
|
|
+ script.custom.homepageURL = src.from;
|
|
|
+ }
|
|
|
+ if (isRemote(src.url)) script.custom.lastInstallURL = src.url;
|
|
|
+ if (src.position) script.props.position = +src.position;
|
|
|
+ buildPathMap(script, src.url);
|
|
|
+ await saveScript(script, src.code);
|
|
|
+ fetchScriptResources(script, src);
|
|
|
+ Object.assign(result.update, script, src.update);
|
|
|
+ result.where = { id: script.props.id };
|
|
|
+ sendCmd(cmd, result);
|
|
|
+ pluginEvents.emit('scriptChanged', result);
|
|
|
+ return result;
|
|
|
}
|
|
|
|
|
|
+/** @return {Object} */
|
|
|
function buildPathMap(script, base) {
|
|
|
const { meta } = script;
|
|
|
const baseUrl = base || script.custom.lastInstallURL;
|
|
|
@@ -476,12 +488,13 @@ function buildPathMap(script, base) {
|
|
|
return pathMap;
|
|
|
}
|
|
|
|
|
|
+/** @return {void} */
|
|
|
function fetchScriptResources(script, cache) {
|
|
|
const { meta, custom: { pathMap } } = script;
|
|
|
// @require
|
|
|
meta.require.forEach((key) => {
|
|
|
const fullUrl = pathMap[key] || key;
|
|
|
- const cached = objectGet(cache, ['require', fullUrl]);
|
|
|
+ const cached = cache.require?.[fullUrl];
|
|
|
if (cached) {
|
|
|
storage.require.set(fullUrl, cached);
|
|
|
} else {
|
|
|
@@ -491,7 +504,7 @@ function fetchScriptResources(script, cache) {
|
|
|
// @resource
|
|
|
Object.values(meta.resources).forEach((url) => {
|
|
|
const fullUrl = pathMap[url] || url;
|
|
|
- const cached = objectGet(cache, ['resources', fullUrl]);
|
|
|
+ const cached = cache.resources?.[fullUrl];
|
|
|
if (cached) {
|
|
|
storage.cache.set(fullUrl, cached);
|
|
|
} else {
|
|
|
@@ -519,7 +532,8 @@ function fetchScriptResources(script, cache) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-export function vacuum() {
|
|
|
+/** @return {Promise<void>} */
|
|
|
+export async function vacuum() {
|
|
|
const valueKeys = {};
|
|
|
const cacheKeys = {};
|
|
|
const requireKeys = {};
|
|
|
@@ -530,52 +544,106 @@ export function vacuum() {
|
|
|
[storage.require, requireKeys],
|
|
|
[storage.code, codeKeys],
|
|
|
];
|
|
|
- return browser.storage.local.get()
|
|
|
- .then((data) => {
|
|
|
- Object.keys(data).forEach((key) => {
|
|
|
- mappings.some(([substore, map]) => {
|
|
|
- const { prefix } = substore;
|
|
|
- if (key.startsWith(prefix)) {
|
|
|
- // -1 for untouched, 1 for touched, 2 for missing
|
|
|
- map[key.slice(prefix.length)] = -1;
|
|
|
- return true;
|
|
|
- }
|
|
|
- return false;
|
|
|
- });
|
|
|
- });
|
|
|
- const touch = (obj, key) => {
|
|
|
- if (obj[key] < 0) obj[key] = 1;
|
|
|
- else if (!obj[key]) obj[key] = 2;
|
|
|
- };
|
|
|
- store.scripts.forEach((script) => {
|
|
|
- const { id } = script.props;
|
|
|
- touch(codeKeys, id);
|
|
|
- touch(valueKeys, id);
|
|
|
- if (!script.custom.pathMap) buildPathMap(script);
|
|
|
- const { pathMap } = script.custom;
|
|
|
- script.meta.require.forEach((url) => {
|
|
|
- touch(requireKeys, pathMap[url] || url);
|
|
|
- });
|
|
|
- Object.values(script.meta.resources).forEach((url) => {
|
|
|
- touch(cacheKeys, pathMap[url] || url);
|
|
|
- });
|
|
|
- const { icon } = script.meta;
|
|
|
- if (isRemote(icon)) {
|
|
|
- const fullUrl = pathMap[icon] || icon;
|
|
|
- touch(cacheKeys, fullUrl);
|
|
|
+ const data = await browser.storage.local.get();
|
|
|
+ Object.keys(data).forEach((key) => {
|
|
|
+ mappings.some(([substore, map]) => {
|
|
|
+ const { prefix } = substore;
|
|
|
+ if (key.startsWith(prefix)) {
|
|
|
+ // -1 for untouched, 1 for touched, 2 for missing
|
|
|
+ map[key.slice(prefix.length)] = -1;
|
|
|
+ return true;
|
|
|
}
|
|
|
+ return false;
|
|
|
});
|
|
|
- mappings.forEach(([substore, map]) => {
|
|
|
- Object.keys(map).forEach((key) => {
|
|
|
- const value = map[key];
|
|
|
- if (value < 0) {
|
|
|
- // redundant value
|
|
|
- substore.remove(key);
|
|
|
- } else if (value === 2 && substore.fetch) {
|
|
|
- // missing resource
|
|
|
- substore.fetch(key);
|
|
|
- }
|
|
|
- });
|
|
|
+ });
|
|
|
+ const touch = (obj, key) => {
|
|
|
+ if (obj[key] < 0) {
|
|
|
+ obj[key] = 1;
|
|
|
+ } else if (!obj[key]) {
|
|
|
+ obj[key] = 2;
|
|
|
+ }
|
|
|
+ };
|
|
|
+ store.scripts.forEach((script) => {
|
|
|
+ const { id } = script.props;
|
|
|
+ touch(codeKeys, id);
|
|
|
+ touch(valueKeys, id);
|
|
|
+ if (!script.custom.pathMap) buildPathMap(script);
|
|
|
+ const { pathMap } = script.custom;
|
|
|
+ script.meta.require.forEach((url) => {
|
|
|
+ touch(requireKeys, pathMap[url] || url);
|
|
|
+ });
|
|
|
+ Object.values(script.meta.resources).forEach((url) => {
|
|
|
+ touch(cacheKeys, pathMap[url] || url);
|
|
|
+ });
|
|
|
+ const { icon } = script.meta;
|
|
|
+ if (isRemote(icon)) {
|
|
|
+ const fullUrl = pathMap[icon] || icon;
|
|
|
+ touch(cacheKeys, fullUrl);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ mappings.forEach(([substore, map]) => {
|
|
|
+ Object.keys(map).forEach((key) => {
|
|
|
+ const value = map[key];
|
|
|
+ if (value < 0) {
|
|
|
+ // redundant value
|
|
|
+ substore.remove(key);
|
|
|
+ } else if (value === 2 && substore.fetch) {
|
|
|
+ // missing resource
|
|
|
+ substore.fetch(key);
|
|
|
+ }
|
|
|
});
|
|
|
});
|
|
|
}
|
|
|
+
|
|
|
+/** @typedef VMScript
|
|
|
+ * @property {VMScriptConfig} config
|
|
|
+ * @property {VMScriptCustom} custom
|
|
|
+ * @property {VMScriptMeta} meta
|
|
|
+ * @property {VMScriptProps} props
|
|
|
+ */
|
|
|
+/** @typedef VMScriptConfig *
|
|
|
+ * @property {Boolean} enabled - stored as 0 or 1
|
|
|
+ * @property {Boolean} removed - stored as 0 or 1
|
|
|
+ * @property {Boolean} shouldUpdate - stored as 0 or 1
|
|
|
+ */
|
|
|
+/** @typedef VMScriptCustom *
|
|
|
+ * @property {string[]} exclude
|
|
|
+ * @property {string[]} excludeMatch
|
|
|
+ * @property {string[]} include
|
|
|
+ * @property {string[]} match
|
|
|
+ * @property {boolean} origExclude
|
|
|
+ * @property {boolean} origExcludeMatch
|
|
|
+ * @property {boolean} origInclude
|
|
|
+ * @property {boolean} origMatch
|
|
|
+ * @property {Object} pathMap
|
|
|
+ * @property {VMScriptRunAt} runAt
|
|
|
+ */
|
|
|
+/** @typedef VMScriptMeta *
|
|
|
+ * @property {string} description
|
|
|
+ * @property {string} downloadURL
|
|
|
+ * @property {string[]} exclude
|
|
|
+ * @property {string[]} exclude-match
|
|
|
+ * @property {string[]} grant
|
|
|
+ * @property {string} homepageURL
|
|
|
+ * @property {string} icon
|
|
|
+ * @property {string[]} include
|
|
|
+ * @property {'auto' | 'page' | 'content'} inject-into
|
|
|
+ * @property {string[]} match
|
|
|
+ * @property {string} namespace
|
|
|
+ * @property {string} name
|
|
|
+ * @property {boolean} noframes
|
|
|
+ * @property {string[]} require
|
|
|
+ * @property {Object} resource
|
|
|
+ * @property {VMScriptRunAt} run-at
|
|
|
+ * @property {string} supportURL
|
|
|
+ * @property {string} version
|
|
|
+ */
|
|
|
+/** @typedef VMScriptProps *
|
|
|
+ * @property {number} id
|
|
|
+ * @property {number} lastModified
|
|
|
+ * @property {number} lastUpdated
|
|
|
+ * @property {number} position
|
|
|
+ * @property {string} uri
|
|
|
+ * @property {string} uuid
|
|
|
+ */
|
|
|
+/** @typedef {'document-start' | 'document-end' | 'document-idle'} VMScriptRunAt */
|