瀏覽代碼

Merge remote-tracking branch 'LOCAL-main/master' into vue3

# Conflicts:
#	yarn.lock
tophf 3 年之前
父節點
當前提交
f81801c067

+ 3 - 0
jsconfig.json

@@ -6,5 +6,8 @@
       "@/*": ["./src/*"]
     }
   },
+  "typeAcquisition": {
+    "include": ["./types.d.ts"]
+  },
   "exclude": ["node_modules", "dist"]
 }

+ 1 - 0
package.json

@@ -33,6 +33,7 @@
     "@gera2ld/plaid-webpack-vue3": "~2.5.7",
     "@types/chrome": "^0.0.191",
     "@types/firefox-webext-browser": "94.0.1",
+    "@violentmonkey/types": "0.1.4",
     "amo-upload": "^0.2.0",
     "babel-plugin-transform-modern-regexp": "^0.0.6",
     "cross-env": "^7.0.3",

+ 5 - 69
src/background/utils/db.js

@@ -14,9 +14,9 @@ import { setOption } from './options';
 import storage from './storage';
 
 export const store = {
-  /** @type VMScript[] */
+  /** @type {VMScript[]} */
   scripts: [],
-  /** @type Object<string,VMScript[]> */
+  /** @type {Object<string,VMScript[]>} */
   scriptMap: {},
   storeInfo: {
     id: 0,
@@ -102,7 +102,7 @@ preInitialize.push(async () => {
   const mods = [];
   const toRemove = [];
   const resUrls = new Set();
-  /** @this VMScriptCustom.pathMap */
+  /** @this {StringMap} */
   const rememberUrl = function _(url) {
     if (url && !isDataUri(url)) {
       resUrls.add(this[url] || url);
@@ -274,6 +274,7 @@ export function getScriptsByURL(url, isTop) {
 /**
  * @param {VMScript[]} scripts
  * @param {boolean} [sizing]
+ * @return {VMInjection.Env}
  */
 function getScriptEnv(scripts, sizing) {
   const disabledIds = [];
@@ -295,6 +296,7 @@ function getScriptEnv(scripts, sizing) {
     const { meta, custom } = script;
     const { pathMap = buildPathMap(script) } = custom;
     const runAt = `${custom.runAt || meta.runAt || ''}`.match(RUN_AT_RE)?.[1] || 'end';
+    /** @type {VMInjection.Env} */
     const env = sizing || runAt === 'start' || runAt === 'body' ? envStart : envDelayed;
     const { depsMap } = env;
     env.ids.push(id);
@@ -321,7 +323,6 @@ function getScriptEnv(scripts, sizing) {
         }
       }
     }
-    /** @namespace VMInjectedScript */
     env[ENV_SCRIPTS].push(sizing ? script : { ...script, runAt });
   });
   envStart.promise = readEnvironmentData(envStart);
@@ -721,68 +722,3 @@ export async function vacuum(data) {
   resolveSelf(result);
   return result;
 }
-
-/** @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
- * @property {Boolean | null} notifyUpdates - stored as 0 or 1 or null (default) which means "use global setting"
- */
-/** @typedef VMScriptCustom *
- * @property {string} name
- * @property {string} downloadURL
- * @property {string} homepageURL
- * @property {string} lastInstallURL
- * @property {string} updateURL
- * @property {'auto' | 'page' | 'content'} injectInto
- * @property {null | 1 | 0} noframes - null or absence == default (script's value)
- * @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[]} excludeMatch
- * @property {string[]} grant
- * @property {string} homepageURL
- * @property {string} icon
- * @property {string[]} include
- * @property {'auto' | 'page' | 'content'} injectInto
- * @property {string[]} match
- * @property {string} namespace
- * @property {string} name
- * @property {boolean} noframes
- * @property {string[]} require
- * @property {Object} resources
- * @property {VMScriptRunAt} runAt
- * @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-body' | 'document-end' | 'document-idle'
- } VMScriptRunAt
- */

+ 6 - 6
src/background/utils/icon.js

@@ -49,17 +49,17 @@ const KEY_IS_APPLIED = 'isApplied';
 const KEY_SHOW_BADGE = 'showBadge';
 const KEY_BADGE_COLOR = 'badgeColor';
 const KEY_BADGE_COLOR_BLOCKED = 'badgeColorBlocked';
-/** @type boolean */
+/** @type {boolean} */
 let isApplied;
-/** @type VMBadgeMode */
+/** @type {VMBadgeMode} */
 let showBadge;
-/** @type string */
+/** @type {string} */
 let badgeColor;
-/** @type string */
+/** @type {string} */
 let badgeColorBlocked;
-/** @type string */
+/** @type {string} */
 let titleBlacklisted;
-/** @type string */
+/** @type {string} */
 let titleNoninjectable;
 
 hookOptions((changes) => {

+ 42 - 37
src/background/utils/preinject.js

@@ -28,8 +28,8 @@ const TIME_KEEP_DATA = 5 * 60e3;
 const cache = initCache({
   lifetime: TIME_KEEP_DATA,
   onDispose: contentScriptsAPI && (async val => {
-    if (val && typeof val === 'object') {
-      const reg = (CSAPI_REG in val ? val : await val)[CSAPI_REG];
+    if (val) {
+      const reg = (val.then ? await val : val)[CSAPI_REG];
       if (reg) (await reg).unregister();
     }
   }),
@@ -52,7 +52,7 @@ let injectInto;
 let xhrInject;
 
 Object.assign(commands, {
-  /** @return {Promise<VMGetInjectedData>} */
+  /** @return {Promise<VMInjection>} */
   async GetInjected({ url, forceContent }, src) {
     const { frameId, tab } = src;
     const tabId = tab.id;
@@ -60,10 +60,10 @@ Object.assign(commands, {
     clearFrameData(tabId, frameId);
     const key = getKey(url, !frameId);
     const cacheVal = cache.pop(key) || prepare(key, url, tabId, frameId, forceContent);
-    /** @type VMGetInjectedDataContainer */
-    const data = cacheVal[INJECT] ? cacheVal : await cacheVal;
-    const inject = data[INJECT];
-    const feedback = data[FEEDBACK];
+    const bag = cacheVal[INJECT] ? cacheVal : await cacheVal;
+    /** @type {VMInjection} */
+    const inject = bag[INJECT];
+    const feedback = bag[FEEDBACK];
     if (feedback?.length) {
       // Injecting known content scripts without waiting for InjectionFeedback message.
       // Running in a separate task because it may take a long time to serialize data.
@@ -127,13 +127,13 @@ onStorageChanged(async ({ keys: dbKeys }) => {
   const raw = cache.getValues();
   const resolved = !raw.some(val => val?.then);
   const cacheValues = resolved ? raw : await Promise.all(raw);
-  const dirty = cacheValues.some(data => data[INJECT]
+  const dirty = cacheValues.some(bag => bag[INJECT]
     && dbKeys.some((key) => {
       const prefix = key.slice(0, key.indexOf(':') + 1);
       const prop = propsToClear[prefix];
       key = key.slice(prefix.length);
       return prop === true
-        || data[prop]?.includes(prefix === storage.value.prefix ? +key : key);
+        || bag[prop]?.includes(prefix === storage.value.prefix ? +key : key);
     }));
   if (dirty) {
     cache.destroy();
@@ -173,8 +173,6 @@ function onOptionChanged(changes) {
   });
 }
 
-/** @typedef {Promise<VMGetInjectedDataContainer>|VMGetInjectedDataContainer} VMGetInjected */
-
 function getKey(url, isTop) {
   return isTop ? url : `-${url}`;
 }
@@ -224,35 +222,34 @@ function onSendHeaders({ url, tabId, frameId }) {
 /** @param {chrome.webRequest.WebResponseHeadersDetails} info */
 function onHeadersReceived(info) {
   const key = getKey(info.url, !info.frameId);
-  const data = xhrInject && cache.get(key);
+  const bag = xhrInject && cache.get(key);
   // Proceeding only if prepareScripts has replaced promise in cache with the actual data
-  return data?.[INJECT] && prepareXhrBlob(info, data);
+  return bag?.[INJECT] && prepareXhrBlob(info, bag);
 }
 
 /**
  * @param {chrome.webRequest.WebResponseHeadersDetails} info
- * @param {VMGetInjectedDataContainer} data
+ * @param {VMInjection.Bag} bag
  */
-function prepareXhrBlob({ url, responseHeaders }, data) {
+function prepareXhrBlob({ url, responseHeaders }, bag) {
   if (url.startsWith('https:') && detectStrictCsp(responseHeaders)) {
-    forceContentInjection(data);
+    forceContentInjection(bag);
   }
   const blobUrl = URL.createObjectURL(new Blob([
-    JSON.stringify(data[INJECT]),
+    JSON.stringify(bag[INJECT]),
   ]));
   responseHeaders.push({
     name: 'Set-Cookie',
     value: `"${process.env.INIT_FUNC_NAME}"=${blobUrl.split('/').pop()}; SameSite=Lax`,
   });
   setTimeout(URL.revokeObjectURL, TIME_KEEP_DATA, blobUrl);
-  data[HEADERS] = true;
+  bag[HEADERS] = true;
   return { responseHeaders };
 }
 
 function prepare(key, url, tabId, frameId, forceContent) {
-  /** @namespace VMGetInjectedDataContainer */
+  /** @type {VMInjection.Bag} */
   const res = {
-    /** @namespace VMGetInjectedData */
     [INJECT]: {
       expose: !frameId
         && url.startsWith('https://')
@@ -264,30 +261,39 @@ function prepare(key, url, tabId, frameId, forceContent) {
     : res;
 }
 
+/**
+ * @param {VMInjection.Bag} res
+ * @param cacheKey
+ * @param url
+ * @param tabId
+ * @param frameId
+ * @param forceContent
+ * @return {Promise<any>}
+ */
 async function prepareScripts(res, cacheKey, url, tabId, frameId, forceContent) {
-  const data = getScriptsByURL(url, !frameId);
-  const { envDelayed, [ENV_SCRIPTS]: scripts } = Object.assign(data, await data.promise);
+  const bag = getScriptsByURL(url, !frameId);
+  const { envDelayed, [ENV_SCRIPTS]: scripts } = Object.assign(bag, await bag.promise);
   const isLate = forceContent != null;
-  data[FORCE_CONTENT] = forceContent; // used in prepareScript and isPageRealm
-  const feedback = scripts.map(prepareScript, data).filter(Boolean);
+  bag[FORCE_CONTENT] = forceContent; // used in prepareScript and isPageRealm
+  const feedback = scripts.map(prepareScript, bag).filter(Boolean);
   const more = envDelayed.promise;
   const envKey = getUniqId(`${tabId}:${frameId}:`);
+  /** @type {VMInjection} */
   const inject = res[INJECT];
-  /** @namespace VMGetInjectedData */
   Object.assign(inject, {
     [ENV_SCRIPTS]: scripts,
     [INJECT_INTO]: injectInto,
     [INJECT_PAGE]: !forceContent && (
-      scripts.some(isPageRealm, data)
-      || envDelayed[ENV_SCRIPTS].some(isPageRealm, data)
+      scripts.some(isPageRealm, bag)
+      || envDelayed[ENV_SCRIPTS].some(isPageRealm, bag)
     ),
-    cache: data.cache,
+    cache: bag.cache,
     feedId: {
       cacheKey, // InjectionFeedback cache key for cleanup when getDataFF outruns GetInjected
       envKey, // InjectionFeedback cache key for envDelayed
     },
     hasMore: !!more, // tells content bridge to expect envDelayed
-    ids: data.disabledIds, // content bridge adds the actually running ids and sends via SetPopup
+    ids: bag.disabledIds, // content bridge adds the actually running ids and sends via SetPopup
     info: {
       ua,
     },
@@ -302,7 +308,7 @@ async function prepareScripts(res, cacheKey, url, tabId, frameId, forceContent)
   return res;
 }
 
-/** @this {VMScriptByUrlData} */
+/** @this {VMInjection.Env} */
 function prepareScript(script) {
   const { custom, meta, props } = script;
   const { id } = props;
@@ -344,7 +350,7 @@ function prepareScript(script) {
     `\n//# sourceURL=${extensionRoot}${IS_FIREFOX ? '%20' : ''}${name}.user.js#${id}`,
   ]::trueJoin('');
   cache.put(dataKey, injectedCode, TIME_KEEP_DATA);
-  /** @namespace VMInjectedScript */
+  /** @type {VMInjection.Script} */
   Object.assign(script, {
     dataKey,
     displayName,
@@ -402,15 +408,14 @@ function detectStrictCsp(responseHeaders) {
   ));
 }
 
-/** @param {VMGetInjectedDataContainer} data */
-function forceContentInjection(data) {
-  /** @type VMGetInjectedData */
-  const inject = data[INJECT];
+/** @param {VMInjection.Bag} bag */
+function forceContentInjection(bag) {
+  const inject = bag[INJECT];
   inject[FORCE_CONTENT] = true;
   inject[ENV_SCRIPTS].forEach(scr => {
     // When script wants `page`, the result below will be `true` so the script goes into `failedIds`
     scr.code = !isContentRealm(scr, true) || '';
-    data[FEEDBACK].push([scr.dataKey, true]);
+    bag[FEEDBACK].push([scr.dataKey, true]);
   });
 }
 
@@ -421,7 +426,7 @@ function isContentRealm(scr, forceContent) {
   return realm === INJECT_CONTENT || forceContent && realm === INJECT_AUTO;
 }
 
-/** @this {VMScriptByUrlData} */
+/** @this {VMInjection.Env} */
 function isPageRealm(scr) {
   return !isContentRealm(scr, this[FORCE_CONTENT]);
 }

+ 3 - 18
src/background/utils/requests-core.js

@@ -6,22 +6,7 @@ import { extensionOrigin } from './init';
 let encoder;
 
 export const VM_VERIFY = getUniqId('VM-Verify');
-/** @typedef {{
-  anonymous: boolean,
-  blobbed: boolean,
-  cb: function(Object),
-  chunked: boolean,
-  coreId: number,
-  eventsToNotify: string[],
-  id: number,
-  noNativeCookie: boolean,
-  responseHeaders: string,
-  storeId: string,
-  tabId: number,
-  url: string,
-  xhr: XMLHttpRequest,
-}} VMHttpRequest */
-/** @type {Object<string,VMHttpRequest>} */
+/** @type {Object<string,GMReq.BG>} */
 export const requests = { __proto__: null };
 export const verify = { __proto__: null };
 export const FORBIDDEN_HEADER_RE = re`/
@@ -52,7 +37,7 @@ export const FORBIDDEN_HEADER_RE = re`/
   upgrade|
   via
 )$/ix`;
-/** @type chrome.webRequest.RequestFilter */
+/** @type {chrome.webRequest.RequestFilter} */
 const API_FILTER = {
   urls: ['<all_urls>'],
   types: ['xmlhttprequest'],
@@ -120,7 +105,7 @@ function onBeforeSendHeaders({ requestHeaders: headers, requestId, url }) {
 
 /**
  * @param {string} headerValue
- * @param {VMHttpRequest} req
+ * @param {GMReq.BG} req
  * @param {string} url
  */
 function setCookieInStore(headerValue, req, url) {

+ 11 - 5
src/background/utils/requests.js

@@ -8,10 +8,15 @@ import {
 } from './requests-core';
 
 Object.assign(commands, {
-  /** @return {void} */
+  /**
+   * @param {GMReq.Message.Web} opts
+   * @param {MessageSender} src
+   * @return {Promise<void>}
+   */
   HttpRequest(opts, src) {
     const { tab: { id: tabId }, frameId } = src;
     const { id, eventsToNotify } = opts;
+    /** @type {GMReq.BG} */
     requests[id] = {
       id,
       tabId,
@@ -55,7 +60,7 @@ function blob2objectUrl(response) {
   return url;
 }
 
-/** @param {VMHttpRequest} req */
+/** @param {GMReq.BG} req */
 function xhrCallbackWrapper(req) {
   let lastPromise = Promise.resolve();
   let contentType;
@@ -109,6 +114,7 @@ function xhrCallbackWrapper(req) {
         id,
         numChunks,
         type,
+        /** @type {VMScriptResponseObject} */
         data: shouldNotify && {
           finalUrl: req.url || xhr.responseURL,
           ...getResponseHeaders(),
@@ -142,8 +148,8 @@ function xhrCallbackWrapper(req) {
 }
 
 /**
- * @param {Object} opts
- * @param {chrome.runtime.MessageSender | browser.runtime.MessageSender} src
+ * @param {GMReq.Message.Web} opts
+ * @param {MessageSender} src
  * @param {function} cb
  */
 async function httpRequest(opts, src, cb) {
@@ -215,7 +221,7 @@ async function httpRequest(opts, src, cb) {
   xhr.send(body);
 }
 
-/** @param {VMHttpRequest} req */
+/** @param {GMReq.BG} req */
 function clearRequest({ id, coreId }) {
   delete verify[coreId];
   delete requests[id];

+ 1 - 2
src/background/utils/storage-fetch.js

@@ -55,8 +55,7 @@ export async function requestNewer(url, opts) {
     if (modOld || get) {
       const req = await request(url, !get ? { ...opts, method: 'HEAD' } : opts);
       const { headers } = req;
-      // headers does not exist when requesting a local file
-      const mod = headers && (
+      const mod = (
         headers.get('etag')
         || +new Date(headers.get('last-modified'))
         || +new Date(headers.get('date'))

+ 1 - 1
src/background/utils/tab-redirector.js

@@ -25,7 +25,7 @@ Object.assign(commands, {
       url === from
       || cache.has(`autoclose:${tabId}`)
       || /^(chrome:\/\/(newtab|startpage)\/|about:(home|newtab))$/.test(from));
-    /** @namespace VMConfirmCache */
+    /** @namespace VM.ConfirmCache */
     cache.put(`confirm-${confirmKey}`, { incognito, url, from, tabId, ff: ua.firefox });
     const confirmUrl = CONFIRM_URL_BASE + confirmKey;
     const { windowId } = canReplaceCurTab

+ 1 - 1
src/background/utils/values.js

@@ -64,7 +64,7 @@ export function clearValueOpener(tabId, frameId) {
 /**
  * @param {number} tabId
  * @param {number} frameId
- * @param {VMInjectedScript[]} injectedScripts
+ * @param {VMInjection.Script[]}
  */
 export function addValueOpener(tabId, frameId, injectedScripts) {
   injectedScripts.forEach(script => {

+ 0 - 1
src/common/cache.js

@@ -15,7 +15,6 @@ export default function initCache({
   let batchStartTime;
   // eslint-disable-next-line no-return-assign
   const getNow = () => batchStarted && batchStartTime || (batchStartTime = performance.now());
-  /** @namespace VMCache */
   const exports = {
     batch, get, getValues, pop, put, del, has, hit, destroy,
   };

+ 1 - 1
src/common/index.js

@@ -244,7 +244,7 @@ export function makeDataUri(raw, url) {
 }
 
 /**
- * @param {VMRequestResponse} response
+ * @param {VMReq.Response} response
  * @param {boolean} [noJoin]
  * @returns {string|string[]}
  */

+ 6 - 8
src/common/options-defaults.js

@@ -6,8 +6,7 @@ export default {
   // ignoreGrant: false,
   lastUpdate: 0,
   lastModified: 0,
-  /** @typedef {'unique' | 'total' | ''} VMBadgeMode */
-  /** @type VMBadgeMode */
+  /** @type {VMBadgeMode} */
   showBadge: 'unique',
   badgeColor: '#880088',
   badgeColorBlocked: '#888888',
@@ -30,19 +29,19 @@ export default {
   notifyUpdates: false,
   notifyUpdatesGlobal: false, // `true` ignores script.config.notifyUpdates
   version: null,
-  /** @type {'auto' | 'page' | 'content'} */
+  /** @type {VMScriptInjectInto} */
   defaultInjectInto: INJECT_AUTO,
   xhrInject: false,
   filters: {
     /** @type {'name' | 'code' | 'all'} */
     searchScope: 'name',
-    /** @type boolean */
+    /** @type {boolean} */
     showOrder: false,
     /** @type {'exec' | 'alpha' | 'update'} */
     sort: 'exec',
-    /** @type boolean */
+    /** @type {boolean} */
     viewSingleColumn: false,
-    /** @type boolean */
+    /** @type {boolean} */
     viewTable: false,
   },
   filtersPopup: {
@@ -76,7 +75,6 @@ export default {
 // ==/UserScript==
 `,
   showAdvanced: true,
-  /** @typedef {'' | 'dark' | 'light'} VMUiTheme */
-  /** @type VMUiTheme */
+  /** @type {'' | 'dark' | 'light'} */
   uiTheme: '',
 };

+ 1 - 12
src/common/ua.js

@@ -2,18 +2,7 @@
 // so we'll test for window.chrome.app which is only defined in Chrome
 // and for browser.runtime.getBrowserInfo in Firefox 51+
 
-/** @typedef UAExtras
- * @property {number|NaN} chrome - Chrome/ium version number
- * @property {number|NaN} firefox - derived from UA string initially, a real number when `ready`
- * @property {Promise<void>} ready - resolves when `browser` API returns real versions
- */
-/** @typedef UAInjected
- * @property {chrome.runtime.PlatformInfo.arch} arch
- * @property {chrome.runtime.PlatformInfo.os} os
- * @property {string} browserName
- * @property {string} browserVersion
- */
-/** @type {UAInjected & UAExtras} */
+/** @type {VMUserAgent} */
 const ua = {};
 export default ua;
 

+ 3 - 1
src/common/ui/code-trailing-spaces.js

@@ -7,12 +7,14 @@ const DEFAULTS = {
   [KILL_OPT]: true,
   [SHOW_OPT]: true,
 };
+/** Regexp's \s minus \r\n */
+const WS_RE = /[\u0020\f\t\v\u00a0\u1680\u2000-\u200a\u2028\u2029\u202f\u205f\u3000\ufeff]+$/gm;
 
 export const killTrailingSpaces = (cm, placeholders) => {
   const text = cm.getValue();
   const shouldKill = cm.options[KILL_OPT];
   const trimmed = shouldKill
-    ? text.replace(/\s+$/gm, '\n')
+    ? text.replace(WS_RE, '\n')
     : text;
   if (text !== trimmed) {
     cm.operation(() => {

+ 0 - 7
src/common/ui/code.vue

@@ -361,13 +361,6 @@ export default {
       const { search } = this;
       search.hasResult = !search.query || !!this.doSearchInternal({ ...opts, wrapAround: true });
     },
-    /**
-     * @typedef {Object} VMSearchOptions
-     * @property {boolean} [reversed]
-     * @property {boolean} [wrapAround]
-     * @property {boolean} [reuseCursor]
-     * @property {{line:number,ch:number}} [pos]
-     */
     /**
      * @param {VMSearchOptions} opts
      * @returns {?true}

+ 14 - 9
src/common/util.js

@@ -221,12 +221,23 @@ const binaryTypes = [
   'blob',
   'arraybuffer',
 ];
+
+/**
+ * @param {string} url
+ * @param {VMReq.Options} options
+ * @return {Promise<VMReq.Response>}
+ */
 export async function requestLocalFile(url, options = {}) {
   // only GET method is allowed for local files
   // headers is meaningless
   return new Promise((resolve, reject) => {
-    const result = {};
     const xhr = new XMLHttpRequest();
+    /** @type {VMReq.Response} */
+    const result = {
+      headers: {
+        get: name => xhr.getResponseHeader(name),
+      },
+    };
     const { responseType } = options;
     xhr.open('GET', url, true);
     if (binaryTypes.includes(responseType)) xhr.responseType = responseType;
@@ -280,17 +291,11 @@ const isLocalUrlRe = re`/^(
 export const isDataUri = url => /^data:/i.test(url);
 export const isRemote = url => url && !isLocalUrlRe.test(decodeURI(url));
 
-/** @typedef {{
-  url: string,
-  status: number,
-  headers: Headers,
-  data: string|ArrayBuffer|Blob|Object
-}} VMRequestResponse */
 /**
  * Make a request.
  * @param {string} url
- * @param {RequestInit} options
- * @return Promise<VMRequestResponse>
+ * @param {VMReq.Options} options
+ * @return {Promise<VMReq.Response>}
  */
 export async function request(url, options = {}) {
   // fetch does not support local file

+ 2 - 2
src/confirm/views/app.vue

@@ -136,7 +136,7 @@ export default {
         close: this.close,
       },
       confirmHotkey: CONFIRM_HOTKEY,
-      /** @type VMConfirmCache */
+      /** @type {VM.ConfirmCache} */
       info: {},
       deps: {}, // combines `this.require` and `this.resources` = all actually loaded deps
       descr: '',
@@ -238,7 +238,7 @@ export default {
       }
     },
     async parseMeta() {
-      /** @type {VMScriptMeta} */
+      /** @type {VMScript.meta} */
       const meta = await sendCmdDirectly('ParseMeta', this.code);
       const name = getLocaleString(meta, 'name');
       document.title = `${name.slice(0, MAX_TITLE_NAME_LEN)}${name.length > MAX_TITLE_NAME_LEN ? '...' : ''} - ${

+ 3 - 2
src/injected/content/bridge.js

@@ -50,11 +50,12 @@ const bridge = {
   ids: [], // all ids including the disabled ones for SetPopup
   runningIds: [],
   // userscripts running in the content script context are messaged via invokeGuest
-  /** @type Number[] */
+  /** @type {Number[]} */
   invokableIds: [],
   failedIds: [],
   cache: createNullObj(),
   pathMaps: createNullObj(),
+  /** @type {function(VMInjection)[]} */
   onScripts,
   allowCmd,
   /**
@@ -71,7 +72,7 @@ const bridge = {
     assignHandlers(bgHandlers, obj, force);
   },
   /**
-   * @param {VMInjectedScript | VMScript} script
+   * @param {VMInjection.Script} script
    */
   allowScript({ dataKey, meta }) {
     allowCmd('Run', dataKey);

+ 3 - 5
src/injected/content/inject.js

@@ -14,12 +14,12 @@ const VAULT_WRITER = `${IS_FIREFOX ? VM_UUID : INIT_FUNC_NAME}VW`;
 const VAULT_WRITER_ACK = `${VAULT_WRITER}+`;
 let contLists;
 let pgLists;
-/** @type {Object<string,VMInjectionRealm>} */
+/** @type {Object<string,VMRealmData>} */
 let realms;
 /** @type {?boolean} */
 let pageInjectable;
 let frameEventWnd;
-/** @type ShadowRoot */
+/** @type {ShadowRoot} */
 let injectedRoot;
 
 // https://bugzil.la/1408996
@@ -121,16 +121,14 @@ export function injectPageSandbox(contentId, webId) {
 /**
  * @param {string} contentId
  * @param {string} webId
- * @param {VMGetInjectedData} data
+ * @param {VMInjection} data
  * @param {boolean} isXml
  */
 export async function injectScripts(contentId, webId, data, isXml) {
   const { hasMore, info } = data;
   realms = {
     __proto__: null,
-    /** @namespace VMInjectionRealm */
     [INJECT_CONTENT]: {
-      /** @namespace VMRunAtLists */
       lists: contLists = { start: [], body: [], end: [], idle: [] },
       is: 0,
       info,

+ 35 - 2
src/injected/content/requests.js

@@ -12,13 +12,19 @@ const getBlobType = describeProperty(SafeBlob[PROTO], 'type').get;
 const getReaderResult = describeProperty(SafeFileReader[PROTO], 'result').get;
 const readAsDataURL = SafeFileReader[PROTO].readAsDataURL;
 const fdAppend = SafeFormData[PROTO].append;
-
+/** @type {GMReq.Content} */
 const requests = createNullObj();
 let downloadChain = promiseResolve();
 
 // TODO: extract all prop names used across files into consts.js to ensure sameness
 bridge.addHandlers({
+  /**
+   * @param {GMReq.Message.Web} msg
+   * @param {VMScriptInjectInto} realm
+   * @returns {Promise<void>}
+   */
   async HttpRequest(msg, realm) {
+    /** @type {GMReq.Content} */
     requests[msg.id] = createNullObj({
       realm,
       wantsBlob: msg.xhrType === 'blob',
@@ -37,6 +43,10 @@ bridge.addHandlers({
 });
 
 bridge.addBackgroundHandlers({
+  /**
+   * @param {GMReq.Message.BG} msg
+   * @returns {Promise<void>}
+   */
   async HttpRequested(msg) {
     const { id, chunk } = msg;
     const req = requests[id];
@@ -84,6 +94,11 @@ bridge.addBackgroundHandlers({
   },
 });
 
+/**
+ * @param {GMReq.Content} req
+ * @param {string} url
+ * @returns {Promise<Blob|ArrayBuffer>}
+ */
 async function importBlob(req, url) {
   const data = await (await safeFetch(url))::(req.wantsBlob ? getBlob : getArrayBuffer)();
   sendCmd('RevokeBlob', url);
@@ -110,7 +125,12 @@ async function revokeBlobAfterTimeout(url) {
   revokeObjectURL(url);
 }
 
-/** ArrayBuffer/Blob in Chrome incognito is transferred in string chunks */
+/**
+ * ArrayBuffer/Blob in Chrome incognito is transferred in string chunks
+ * @param {GMReq.Content} req
+ * @param {GMReq.Message.BG} msg
+ * @return {Promise<Blob|ArrayBuffer>}
+ */
 function receiveAllChunks(req, msg) {
   pickIntoNullObj(req, msg, ['dataSize', 'contentType']);
   req.arr = new SafeUint8Array(req.dataSize);
@@ -120,6 +140,10 @@ function receiveAllChunks(req, msg) {
     : finishChunks(req);
 }
 
+/**
+ * @param {GMReq.Content} req
+ * @param {GMReq.Message.Chunk} chunk
+ */
 function receiveChunk(req, { data, pos, last }) {
   processChunk(req, data, pos);
   if (last) {
@@ -129,6 +153,11 @@ function receiveChunk(req, { data, pos, last }) {
   }
 }
 
+/**
+ * @param {GMReq.Content} req
+ * @param {string} data
+ * @param {number} pos
+ */
 function processChunk(req, data, pos) {
   const { arr } = req;
   data = safeAtob(data);
@@ -137,6 +166,10 @@ function processChunk(req, data, pos) {
   }
 }
 
+/**
+ * @param {GMReq.Content} req
+ * @return {Blob|ArrayBuffer}
+ */
 function finishChunks(req) {
   const { arr } = req;
   delete req.arr;

+ 1 - 1
src/injected/web/bridge.js

@@ -1,7 +1,7 @@
 const handlers = createNullObj();
 const callbacks = createNullObj();
 /**
- * @property {UAInjected} ua
+ * @property {VMScriptGMInfoPlatform} ua
  */
 const bridge = {
   __proto__: null,

+ 2 - 6
src/injected/web/gm-api-wrapper.js

@@ -21,7 +21,7 @@ let gmApi;
 let componentUtils;
 
 /**
- * @param {VMScript & VMInjectedScript} script
+ * @param {VMInjection.Script} script
  * @returns {Object}
  */
 export function makeGmApiWrapper(script) {
@@ -37,7 +37,6 @@ export function makeGmApiWrapper(script) {
   }
   const { id } = script.props;
   const resources = createNullObj(meta.resources);
-  /** @namespace VMInjectedScript.Context */
   const context = {
     id,
     script,
@@ -148,9 +147,6 @@ function makeGmInfo(script, resources) {
 function makeGmMethodCaller(gmMethod, context, isAsync) {
   // keeping the native console.log intact
   if (gmMethod === gmApi.GM_log) return gmMethod;
-  if (isAsync) {
-    /** @namespace VMInjectedScript.Context */
-    context = assign({ __proto__: null, async: true }, context);
-  }
+  if (isAsync) context = assign({ __proto__: null, async: true }, context);
   return vmOwnFunc(gmMethod::bind(context));
 }

+ 7 - 0
src/injected/web/gm-api.js

@@ -99,6 +99,7 @@ export function makeGmApi() {
     },
     GM_download(arg1, name) {
       // not using ... as it calls Babel's polyfill that calls unsafe Object.xxx
+      /** @type {VMScriptGMDownloadOptions} */
       const opts = createNullObj();
       let onload;
       if (isString(arg1)) {
@@ -205,6 +206,12 @@ function webAddElement(parent, tag, attrs, context) {
   ));
 }
 
+/**
+ * @param {GMContext} context
+ * @param name
+ * @param isBlob
+ * @param isBlobAuto
+ */
 function getResource(context, name, isBlob, isBlobAuto) {
   let res;
   const { id, resCache, resources } = context;

+ 1 - 1
src/injected/web/gm-values.js

@@ -35,7 +35,7 @@ export function loadValues(id) {
  * @param {?} val
  * @param {?string} raw
  * @param {?string} oldRaw
- * @param {VMInjectedScript.Context} context
+ * @param {GMContext} context
  * @return {void|Promise<void>}
  */
 export function dumpValue(id, key, val, raw, oldRaw, context) {

+ 26 - 2
src/injected/web/requests.js

@@ -1,15 +1,22 @@
 import bridge from './bridge';
 
+/** @type {Object<string,GMReq.Web>} */
 const idMap = createNullObj();
 
 bridge.addHandlers({
+  /** @param {GMReq.Message.BG} msg */
   HttpRequested(msg) {
     const req = idMap[msg.id];
     if (req) callback(req, msg);
   },
 });
 
-/** `opts` must already have a null proto */
+/**
+ * @param {GMReq.UserOpts} opts - must already have a null proto
+ * @param {GMContext} context
+ * @param {string} fileName
+ * @return {VMScriptXHRControl}
+ */
 export function onRequestCreate(opts, context, fileName) {
   if (process.env.DEBUG) throwIfProtoPresent(opts);
   let { url } = opts;
@@ -29,6 +36,7 @@ export function onRequestCreate(opts, context, fileName) {
   }
   const scriptId = context.id;
   const id = safeGetUniqId('VMxhr');
+  /** @type {GMReq.Web} */
   const req = {
     __proto__: null,
     id,
@@ -43,6 +51,11 @@ export function onRequestCreate(opts, context, fileName) {
   };
 }
 
+/**
+ * @param {GMReq.Web} req
+ * @param {GMReq.Message.BG} msg
+ * @returns {string|number|boolean|Array|Object|Document|Blob|ArrayBuffer}
+ */
 function parseData(req, msg) {
   let res = req.raw;
   switch (req.opts.responseType) {
@@ -64,6 +77,7 @@ function parseData(req, msg) {
 /**
  * Not using RegExp because it internally depends on proto stuff that can be easily broken,
  * and safe-guarding all of it is ridiculously disproportional.
+ * @param {GMReq.Message.BG} msg
  */
 function getContentType(msg) {
   const type = msg.contentType || '';
@@ -77,7 +91,11 @@ function getContentType(msg) {
   return type::slice(0, i);
 }
 
-// request object functions
+/**
+ * @param {GMReq.Web} req
+ * @param {GMReq.Message.BG} msg
+ * @returns {*}
+ */
 function callback(req, msg) {
   const { opts } = req;
   const cb = opts[`on${msg.type}`];
@@ -109,12 +127,18 @@ function callback(req, msg) {
   if (msg.type === 'loadend') delete idMap[req.id];
 }
 
+/**
+ * @param {GMReq.Web} req
+ * @param {GMContext} context
+ * @param {string} fileName
+ */
 function start(req, context, fileName) {
   const { id, opts, scriptId } = req;
   // withCredentials is for GM4 compatibility and used only if `anonymous` is not set,
   // it's true by default per the standard/historical behavior of gmxhr
   const { data, withCredentials = true, anonymous = !withCredentials } = opts;
   idMap[id] = req;
+  /** @type {GMReq.Message.Web} */
   bridge.post('HttpRequest', createNullObj({
     id,
     scriptId,

+ 1 - 1
src/options/views/tab-settings/vm-editor.vue

@@ -105,7 +105,7 @@ export default {
       showMessage({ text: this.$refs.editor.error || this.i18n('msgSavedEditorOptions') });
     },
     toggleBoolean(event) {
-      const el = /** @type HTMLTextAreaElement */ event.target;
+      const el = /** @type {HTMLTextAreaElement} */ event.target;
       const { selectionStart: start, selectionEnd: end, value } = el;
       const toggled = { false: 'true', true: 'false' }[value.slice(start, end)];
       // FF can't run execCommand on textarea, https://bugzil.la/1220696#c24

+ 1 - 1
src/options/views/tab-settings/vm-import.vue

@@ -148,7 +148,7 @@ async function importBackup(file) {
     if (!meta || !opts) return;
     const ovr = opts.override || {};
     reports[0].text = 'Tampermonkey';
-    /** @type VMScript */
+    /** @type {VMScript} */
     vm.scripts[name] = {
       config: {
         enabled: settings.enabled !== false ? 1 : 0,

+ 1 - 2
src/popup/views/app.vue

@@ -75,7 +75,7 @@
         <icon name="arrow" class="icon-collapse"></icon>
         <div class="flex-auto" v-text="scope.title" :data-totals="scope.totals" />
       </div>
-      <div class="submenu" ref="scriptList" tabindex="-1" autofocus>
+      <div class="submenu" ref="scriptList" tabindex="-1">
         <div
           v-for="(item, index) in scope.list"
           :key="index"
@@ -506,7 +506,6 @@ export default {
     },
   },
   mounted() {
-    // Enable scrolling via Home/End/PgUp/PgDn
     this::focusMe();
     keyboardService.enable();
     this.disposeList = [

+ 278 - 0
src/types.d.ts

@@ -0,0 +1,278 @@
+//#region Generic
+
+declare type NumBool = 0 | 1
+/** null means "default" or "inherit from global" */
+declare type NumBoolNull = 0 | 1 | null
+declare type StringMap = Object<string,string>
+
+//#endregion Generic
+//#region GM-specific
+
+/**
+ * Script context object used by GM### API
+ */
+declare interface GMContext {
+  async?: boolean
+  dataKey: string
+  id: number
+  resCache: StringMap
+  resources: StringMap
+  script: VMScript
+}
+/**
+ * GM_xmlhttpRequest paraphernalia
+ */
+declare namespace GMReq {
+  type EventType = keyof XMLHttpRequestEventMap;
+  type UserOpts = VMScriptGMDownloadOptions | VMScriptGMXHRDetails;
+  declare interface BG {
+    anonymous: boolean
+    blobbed: boolean
+    cb: function(Object)
+    chunked: boolean
+    coreId: number
+    eventsToNotify: string[]
+    frameId: number
+    id: string
+    noNativeCookie: boolean
+    responseHeaders: string
+    storeId: string
+    tabId: number
+    url: string
+    xhr: XMLHttpRequest
+  }
+  declare interface Content {
+    realm: VMScriptInjectInto
+    wantsBlob: boolean
+    eventsToNotify: EventType[]
+    fileName: string
+    arr?: Uint8Array
+    resolve?: function
+    dataSize?: number
+    contentType?: string
+    gotChunks?: boolean
+  }
+  declare interface Web {
+    id: string
+    scriptId: number
+    opts: UserOpts
+    raw: string | Blob | ArrayBuffer
+    response: string | Blob | ArrayBuffer
+    headers: string
+    text?: string
+  }
+  declare namespace Message {
+    type Chunk = {
+      pos: number
+      data: string
+      last: boolean
+    }
+    /** From background */
+    declare interface BG {
+      blobbed: boolean
+      chunked: boolean
+      contentType: string
+      data: VMScriptResponseObject
+      dataSize: number
+      id: string
+      numChunks?: number
+      type: EventType
+      chunk?: Chunk
+    }
+    /** From web/content bridge */
+    declare interface Web {
+      id: string
+      scriptId: number
+      anonymous: boolean
+      fileName: string
+      data: Array
+      eventsToNotify: EventType[]
+      headers?: StringMap
+      method?: string
+      overrideMimeType?: string
+      password?: string
+      timeout?: number
+      url: string
+      user?: string
+      xhrType: XMLHttpRequestResponseType
+    }
+  }
+}
+
+//#endregion Generic
+//#region VM-specific
+
+declare type VMBadgeMode = 'unique' | 'total' | ''
+/**
+ * Internal script representation
+ */
+declare interface VMScript {
+  config: VMScript.Config
+  custom: VMScript.Custom
+  meta: VMScript.Meta
+  props: VMScript.Props
+}
+declare namespace VMScript {
+  declare type Config = {
+    enabled: NumBool
+    removed: NumBool
+    shouldUpdate: NumBool
+    notifyUpdates?: NumBoolNull
+  }
+  declare type Custom = {
+    name?: string
+    downloadURL:? string
+    homepageURL?: string
+    lastInstallURL?: string
+    updateURL?: string
+    injectInto?: VMScriptInjectInto
+    noframes?: NumBoolNull
+    exclude?: string[]
+    excludeMatch?: string[]
+    include?: string[]
+    match?: string[]
+    origExclude: boolean
+    origExcludeMatch: boolean
+    origInclude: boolean
+    origMatch: boolean
+    pathMap?: StringMap
+    runAt?: VMScriptRunAt
+  }
+  declare type Meta = {
+    description?: string
+    downloadURL?: string
+    exclude: string[]
+    excludeMatch: string[]
+    grant: string[]
+    homepageURL?: string
+    icon?: string
+    include: string[]
+    injectInto?: VMScriptInjectInto
+    match: string[]
+    namespace?: string
+    name: string
+    noframes?: boolean
+    require: string[]
+    resources: StringMap
+    runAt?: VMScriptRunAt
+    supportURL?: string
+    unwrap?: boolean
+    version?: string
+  }
+  declare type Props = {
+    id: number
+    lastModified: number
+    lastUpdated: number
+    position: number
+    uri: string
+    uuid: string
+  }
+}
+/**
+ * Injection data sent to the content bridge
+ */
+declare interface VMInjection {
+  expose: string | false
+  scripts: VMInjection.Script[]
+  injectInto: VMScriptInjectInto
+  injectPage: boolean
+  cache: StringMap
+  feedId: {
+    /** InjectionFeedback cache key for cleanup when getDataFF outruns GetInjected */
+    cacheKey: string
+    /** InjectionFeedback cache key for envDelayed */
+    envKey: string
+  }
+  /** tells content bridge to expect envDelayed */
+  hasMore: boolean
+  /** content bridge adds the actually running ids and sends via SetPopup */
+  ids: number[]
+  info: VMInjection.Info
+  isPopupShown?: boolean
+}
+/**
+ * Injection paraphernalia in the background script
+ */
+declare namespace VMInjection {
+  declare interface Env {
+    cache: StringMap
+    cacheKeys: string[]
+    code: StringMap
+    depsMap: Object<string,number[]>
+    /** Only present in envStart */
+    disabledIds?: number[]
+    /** Only present in envStart */
+    envDelayed?: Env
+    ids: number[]
+    promise: Promise<Env>
+    reqKeys: string[]
+    require: StringMap
+    scripts: VMScript[]
+    sizing?: boolean
+    value: Object<string,StringMap>
+    valueIds: number[]
+  }
+  /**
+   * Contains the injected data and non-injected auxiliaries
+   */
+  declare interface Bag {
+    inject: VMInjection
+    feedback: (string|number)[] | false
+    csar: Promise<browser.contentScripts.RegisteredContentScript>
+  }
+  declare interface Info {
+    ua: VMScriptGMInfoPlatform
+  }
+  /**
+   * Script prepared for injection
+   */
+  declare interface Script extends VMScript {
+    dataKey: string
+    displayName: string
+    code: string
+    metaStr: string
+    runAt?: 'start' | 'body' | 'end' | 'idle'
+    values?: StringMap
+  }
+}
+declare interface VMRealmData {
+  lists: {
+    start: VMScript[]
+    body: VMScript[]
+    end: VMScript[]
+    idle: VMScript[]
+  }
+  is: boolean
+  info: Sent
+}
+/**
+ * Internal request()
+ */
+declare namespace VMReq {
+  interface Options extends RequestInit {
+    /** @implements XMLHttpRequestResponseType */
+    responseType: '' | 'arraybuffer' | 'blob' | 'json' | 'text'
+  }
+  declare interface Response {
+    url: string,
+    status: number,
+    headers: Headers,
+    data: string | ArrayBuffer | Blob | Object
+  }
+}
+declare type VMSearchOptions = {
+  reversed?: boolean
+  wrapAround?: chrome.tabs.Tab
+  reuseCursor?: boolean
+  pos?: { line: number, ch: number }
+}
+declare interface VMUserAgent extends VMScriptGMInfoPlatform {
+  /** Chrome/ium version number */
+  chrome: number | NaN
+  /** derived from UA string initially, a real number when `ready` */
+  firefox: number | NaN
+  /** resolves when `browser` API returns real versions */
+  ready: Promise<void>
+}
+
+//#endregion Generic

+ 5 - 0
yarn.lock

@@ -2079,6 +2079,11 @@
   dependencies:
     "@babel/runtime" "^7.16.7"
 
+"@violentmonkey/[email protected]":
+  version "0.1.4"
+  resolved "https://registry.yarnpkg.com/@violentmonkey/types/-/types-0.1.4.tgz#90411ed402e5ed16ee76cd2cb47c884db59eb348"
+  integrity sha512-ckaN0UbxRYNyJH+UuyeRgTpkwAZWtR5g1VXnd8ufWtQQmJkrlOFPGazIp3F+CuJx/6VhLH4Nn+3GdjyE6WrsUA==
+
 "@volar/[email protected]":
   version "0.40.13"
   resolved "https://registry.yarnpkg.com/@volar/code-gen/-/code-gen-0.40.13.tgz#cd69a67b11462b93d79ea2139f9f1e0a76e15111"