فهرست منبع

feat: list scripts that failed to compile in popup

tophf 3 سال پیش
والد
کامیت
bb59ed1c1b

+ 3 - 4
src/background/utils/db.js

@@ -257,7 +257,7 @@ export function getScriptsByURL(url, isTop, errors) {
  * @return {Promise<VMInjection.Env>}
  */
 async function getScriptEnv(scripts) {
-  const disabledIds = [];
+  const allIds = {};
   const [envStart, envDelayed] = [0, 1].map(() => ({
     depsMap: {},
     [ENV_SCRIPTS]: [],
@@ -268,8 +268,7 @@ async function getScriptEnv(scripts) {
   }
   scripts.forEach((script) => {
     const { id } = script.props;
-    if (!script.config.enabled) {
-      disabledIds.push(id);
+    if (!(allIds[id] = +!!script.config.enabled)) {
       return;
     }
     const { meta, custom } = script;
@@ -310,7 +309,7 @@ async function getScriptEnv(scripts) {
   if (envDelayed.ids.length) {
     envDelayed.promise = makePause().then(() => readEnvironmentData(envDelayed));
   }
-  return Object.assign(envStart, { disabledIds, envDelayed });
+  return Object.assign(envStart, { allIds, envDelayed });
 }
 
 async function readEnvironmentData(env) {

+ 1 - 1
src/background/utils/popup-tracker.js

@@ -9,7 +9,7 @@ export const popupTabs = {}; // { tabId: 1 }
 addPublicCommands({
   async SetPopup(data, src) {
     if (popupTabs[src.tab.id]) return;
-    Object.assign(data, await getData({ ids: data.ids }));
+    Object.assign(data, await getData({ ids: Object.keys(data.ids) }));
     cache.put('SetPopup', Object.assign({ [src.frameId]: [data, src] }, cache.get('SetPopup')));
   },
 });

+ 5 - 6
src/background/utils/preinject.js

@@ -1,6 +1,6 @@
 import { getScriptName, getScriptPrettyUrl, getUniqId, sendTabCmd } from '@/common';
 import {
-  INJECT_AUTO, INJECT_CONTENT, INJECT_MAPPING, INJECT_PAGE,
+  INJECT_AUTO, INJECT_CONTENT, INJECT_INTO, INJECT_MAPPING, INJECT_PAGE,
   FEEDBACK, FORCE_CONTENT, METABLOCK_RE, MORE, NEWLINE_END_RE,
 } from '@/common/consts';
 import initCache from '@/common/cache';
@@ -39,7 +39,6 @@ const cache = initCache({
   },
 });
 const INJECT = 'inject';
-const INJECT_INTO = 'injectInto';
 // KEY_XXX for hooked options
 const KEY_EXPOSE = 'expose';
 const KEY_DEF_INJECT_INTO = 'defaultInjectInto';
@@ -281,7 +280,7 @@ function prepare(key, url, tabId, frameId, forceContent) {
 async function prepareScripts(res, cacheKey, url, tabId, frameId, forceContent) {
   const errors = [];
   const bag = await getScriptsByURL(url, !frameId, errors);
-  const { envDelayed, disabledIds: ids, [ENV_SCRIPTS]: scripts } = bag;
+  const { envDelayed, allIds, [ENV_SCRIPTS]: scripts } = bag;
   bag[FORCE_CONTENT] = forceContent; // used in prepareScript and isPageRealm
   propsToClear::forEachValue(val => {
     if (val !== true) res[val] = bag[val];
@@ -301,11 +300,11 @@ async function prepareScripts(res, cacheKey, url, tabId, frameId, forceContent)
     ),
     [MORE]: moreKey,
     cache: bag.cache,
-    ids, // content bridge adds the actually running ids and sends via SetPopup
+    ids: allIds,
     info: {
       ua,
     },
-    errors: errors.filter(err => !ids.includes(+err.slice(err.lastIndexOf('#') + 1))).join('\n'),
+    errors: errors.filter(err => allIds[err.split('#').pop()]).join('\n'),
   });
   res[FEEDBACK] = feedback;
   res[CSAPI_REG] = contentScriptsAPI && !xhrInject
@@ -462,7 +461,7 @@ 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`
+    // When script wants `page`, the result below will be `true` so the script has a "bad realm" id
     const failed = !isContentRealm(scr, true);
     scr.code = failed || '';
     bag[FEEDBACK].push([

+ 3 - 1
src/common/consts.js

@@ -3,7 +3,7 @@
 export const INJECT_AUTO = 'auto';
 export const INJECT_PAGE = 'page';
 export const INJECT_CONTENT = 'content';
-
+export const INJECT_INTO = 'injectInto';
 export const INJECT_MAPPING = {
   __proto__: null,
   // `auto` tries to provide `window` from the real page as `unsafeWindow`
@@ -13,6 +13,8 @@ export const INJECT_MAPPING = {
   // inject into content context only
   [INJECT_CONTENT]: [INJECT_CONTENT],
 };
+export const ID_BAD_REALM = -1;
+export const ID_INJECTING = 2;
 
 // Allow metadata lines to start with WHITESPACE? '//' SPACE
 // Allow anything to follow the predefined text of the metaStart/End

+ 5 - 6
src/injected/content/bridge.js

@@ -16,12 +16,11 @@ const assignHandlers = (dest, src, force) => {
  */
 const bridge = {
   __proto__: null,
-  ids: [], // all ids including the disabled ones for SetPopup
-  runningIds: [],
-  // userscripts running in the content script context are messaged via invokeGuest
-  /** @type {Number[]} */
-  invokableIds: [],
-  failedIds: [],
+  /**
+   * -1 = bad realm, 0 = disabled, 1 = enabled, 2 = starting, 'page' | 'content' = running
+   * @type {{ [id: string]: -1 | 0 | 1 | 2 | 'page' | 'content' }}
+   */
+  ids: createNullObj(),
   cache: createNullObj(),
   pathMaps: createNullObj(),
   /** @type {function(VMInjection)[]} */

+ 4 - 6
src/injected/content/cmd-run.js

@@ -1,8 +1,9 @@
 import bridge from './bridge';
 import { sendCmd } from './util';
-import { INJECT_CONTENT } from '../util';
+import { INJECT_PAGE } from '../util';
 
-const { runningIds } = bridge;
+const { ids } = bridge;
+const runningIds = [];
 const resolvedPromise = promiseResolve();
 let badgePromise;
 let numBadgesSent = 0;
@@ -10,10 +11,7 @@ let bfCacheWired;
 
 export function Run(id, realm) {
   safePush(runningIds, id);
-  safePush(bridge.ids, id);
-  if (realm === INJECT_CONTENT) {
-    safePush(bridge.invokableIds, id);
-  }
+  ids[id] = realm || INJECT_PAGE;
   if (!badgePromise) {
     badgePromise = resolvedPromise::then(throttledSetBadge);
   }

+ 2 - 6
src/injected/content/gm-api-content.js

@@ -1,5 +1,6 @@
 import bridge from './bridge';
 import { decodeResource, elemByTag, makeElem, nextTask, sendCmd } from './util';
+import { INJECT_INTO } from '../util';
 
 const menus = createNullObj();
 let setPopupThrottle;
@@ -61,11 +62,6 @@ export async function sendSetPopup(isDelayed) {
       await setPopupThrottle;
       setPopupThrottle = null;
     }
-    sendCmd('SetPopup', createNullObj({ menus }, bridge, [
-      'ids',
-      'injectInto',
-      'runningIds',
-      'failedIds',
-    ]));
+    sendCmd('SetPopup', createNullObj({ menus }, bridge, ['ids', INJECT_INTO]));
   }
 }

+ 6 - 11
src/injected/content/index.js

@@ -6,10 +6,10 @@ import './notifications';
 import './requests';
 import './tabs';
 import { sendCmd } from './util';
-import { isEmpty, FORCE_CONTENT, INJECT_CONTENT } from '../util';
+import { isEmpty, FORCE_CONTENT, INJECT_CONTENT, INJECT_INTO } from '../util';
 import { Run } from './cmd-run';
 
-const { invokableIds } = bridge;
+const { ids } = bridge;
 
 // Make sure to call obj::method() in code that may run after INJECT_CONTENT userscripts
 async function init() {
@@ -33,10 +33,8 @@ async function init() {
       ? await getDataFF(dataPromise)
       : await dataPromise
   );
-  pickIntoNullObj(bridge, data, [
-    'ids',
-    'injectInto',
-  ]);
+  assign(ids, data.ids);
+  bridge[INJECT_INTO] = data[INJECT_INTO];
   if (data.expose && !isXml && injectPageSandbox(contentId, webId)) {
     bridge.addHandlers({ GetScriptVer: true }, true);
     bridge.post('Expose');
@@ -50,16 +48,13 @@ async function init() {
 }
 
 bridge.addBackgroundHandlers({
-  Command(data) {
-    const realm = invokableIds::includes(data.id) && INJECT_CONTENT;
-    bridge.post('Command', data, realm);
-  },
+  Command: data => bridge.post('Command', data, ids[data.id]),
   Run: id => Run(id, INJECT_CONTENT),
   UpdatedValues(data) {
     const dataPage = createNullObj();
     const dataContent = createNullObj();
     objectKeys(data)::forEach((id) => {
-      (invokableIds::includes(+id) ? dataContent : dataPage)[id] = data[id];
+      (ids[id] === INJECT_CONTENT ? dataContent : dataPage)[id] = data[id];
     });
     if (!isEmpty(dataPage)) bridge.post('UpdatedValues', dataPage);
     if (!isEmpty(dataContent)) bridge.post('UpdatedValues', dataContent, INJECT_CONTENT);

+ 21 - 6
src/injected/content/inject.js

@@ -2,7 +2,7 @@ import bridge from './bridge';
 import { elemByTag, makeElem, nextTask, onElement, sendCmd } from './util';
 import {
   bindEvents, fireBridgeEvent,
-  INJECT_CONTENT, INJECT_MAPPING, INJECT_PAGE,
+  ID_BAD_REALM, ID_INJECTING, INJECT_CONTENT, INJECT_INTO, INJECT_MAPPING, INJECT_PAGE,
   MORE, FEEDBACK, FORCE_CONTENT,
 } from '../util';
 import { Run } from './cmd-run';
@@ -13,6 +13,7 @@ import { Run } from './cmd-run';
  * INIT_FUNC_NAME ids even though we change it now with each release. */
 const VAULT_WRITER = `${IS_FIREFOX ? VM_UUID : INIT_FUNC_NAME}VW`;
 const VAULT_WRITER_ACK = `${VAULT_WRITER}+`;
+const tardyQueue = [];
 let contLists;
 let pgLists;
 /** @type {Object<string,VMRealmData>} */
@@ -153,7 +154,7 @@ export async function injectScripts(contentId, webId, data, isXml) {
   }
   const feedback = data.scripts.map((script) => {
     const { id } = script.props;
-    const realm = INJECT_MAPPING[script.injectInto].find(key => (
+    const realm = INJECT_MAPPING[script[INJECT_INTO]].find(key => (
       key === INJECT_CONTENT || pageInjectable
     ));
     const { runAt } = script;
@@ -165,8 +166,7 @@ export async function injectScripts(contentId, webId, data, isXml) {
       realmData.is = true;
       if (pathMap) bridge.pathMaps[id] = pathMap;
     } else {
-      bridge.failedIds.push(id);
-      bridge.ids.push(id);
+      bridge.ids[id] = ID_BAD_REALM;
     }
     return [
       script.dataKey,
@@ -218,8 +218,7 @@ async function injectDelayedScripts(contentId, webId, { cache, scripts }) {
     } else if (pageInjectable) {
       safePush(pgLists[runAt], script);
     } else {
-      safePush(bridge.failedIds, id);
-      safePush(bridge.ids, id);
+      bridge.ids[id] = ID_BAD_REALM;
     }
   });
   if (document::getReadyState() === 'loading') {
@@ -310,6 +309,8 @@ function injectAll(runAt) {
       if (realm === INJECT_PAGE && !IS_FIREFOX) {
         injectList(runAt);
       }
+      safePush(tardyQueue, items);
+      nextTask()::then(tardyQueueCheck);
     }
   }
   if (runAt !== 'start' && contLists[runAt].length) {
@@ -344,6 +345,20 @@ function setupContentInvoker(contentId, webId) {
   };
 }
 
+/**
+ * Chrome doesn't fire a syntax error event, so we'll mark ids that didn't start yet
+ * as "still starting", so the popup can show them accordingly.
+ */
+function tardyQueueCheck() {
+  for (const items of tardyQueue) {
+    for (const script of items) {
+      const id = script.props.id;
+      if (bridge.ids[id] === 1) bridge.ids[id] = ID_INJECTING;
+    }
+  }
+  tardyQueue.length = 0;
+}
+
 function tellBridgeToWriteVault(vaultId, wnd) {
   const { post } = bridge;
   if (post) { // may be absent if this page doesn't have scripts

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

@@ -2,6 +2,7 @@ import bridge from './bridge';
 import { makeGmApi } from './gm-api';
 import { makeGlobalWrapper } from './gm-global-wrapper';
 import { makeComponentUtils } from './util';
+import { INJECT_INTO } from '../util';
 
 /** Name in Greasemonkey4 -> name in GM */
 const GM4_ALIAS = {
@@ -91,7 +92,7 @@ function makeGmInfo(gmInfo, meta, resources) {
   });
   // No __proto__:null because these are standard objects for userscripts
   setOwnProp(meta, 'resources', resourcesArr);
-  setOwnProp(gmInfo, 'injectInto', bridge.mode);
+  setOwnProp(gmInfo, INJECT_INTO, bridge.mode);
   setOwnProp(gmInfo, 'script', meta);
   return gmInfo;
 }

+ 15 - 10
src/popup/index.js

@@ -1,6 +1,8 @@
 import '@/common/browser';
 import { sendCmdDirectly } from '@/common';
-import { INJECT_PAGE } from '@/common/consts';
+import {
+  ID_BAD_REALM, ID_INJECTING, INJECT_CONTENT, INJECT_INTO, INJECT_PAGE,
+} from '@/common/consts';
 import handlers from '@/common/handlers';
 import { loadScriptIcon } from '@/common/load-script-icon';
 import { forEachValue, mapEntry } from '@/common/object';
@@ -20,8 +22,9 @@ Object.assign(handlers, {
      * because we only show the iframe menu for unique scripts that don't run in the main page */
     const isTop = src.frameId === 0;
     if (!isTop) await mutex.ready;
-    const ids = data.ids.filter(id => !store.scriptIds.includes(id));
-    store.scriptIds.push(...ids);
+    const idMap = data.ids::mapEntry(null, (id, val) => store.idMap[id] !== val && id);
+    const ids = Object.keys(idMap).map(Number);
+    Object.assign(store.idMap, idMap);
     if (isTop) {
       mutex.resolve();
       store.commands = data.menus::mapEntry(Object.keys);
@@ -36,16 +39,18 @@ Object.assign(handlers, {
       metas.forEach(script => {
         loadScriptIcon(script, data);
         const { id } = script.props;
-        script.runs = data.runningIds.includes(id);
+        const state = idMap[id];
+        const badRealm = state === ID_BAD_REALM;
+        const renderedScript = scope.find(({ props }) => props.id === id);
+        if (renderedScript) script = renderedScript;
+        else scope.push(script);
+        script.runs = state === INJECT_CONTENT || state === INJECT_PAGE;
         script.pageUrl = src.url; // each frame has its own URL
-        if (data.failedIds.includes(id)) {
-          script.failed = true;
-          if (!store.injectionFailure) {
-            store.injectionFailure = { fixable: data.injectInto === INJECT_PAGE };
-          }
+        script.failed = badRealm || state === ID_INJECTING;
+        if (badRealm && !store.injectionFailure) {
+          store.injectionFailure = { fixable: data[INJECT_INTO] === INJECT_PAGE };
         }
       });
-      scope.push(...metas);
     }
   },
 });

+ 1 - 1
src/popup/utils/index.js

@@ -3,7 +3,7 @@ import { reactive } from 'vue';
 export const store = reactive({
   scripts: [],
   frameScripts: [],
-  scriptIds: [],
+  idMap: {},
   commands: [],
   domain: '',
   injectionFailure: null,

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

@@ -402,7 +402,7 @@ export default {
     checkReload() {
       if (options.get('autoReload')) {
         browser.tabs.reload(store.currentTab.id);
-        store.scriptIds.length = 0;
+        store.idMap = {};
         store.scripts.length = 0;
         store.frameScripts.length = 0;
         mutex.init();

+ 1 - 1
src/types.d.ts

@@ -212,7 +212,7 @@ declare namespace VMInjection {
     disabledIds?: number[];
     /** Only present in envStart */
     envDelayed?: Env;
-    ids: number[];
+    ids: { [id: string]: NumBool };
     promise: Promise<Env>;
     reqKeys: string[];
     require: StringMap;