refactor: move classes instantiated at CC start to scripts

Bug: twpowertools:176
Change-Id: I5ac38aa432ee85bd276ee1c867492100d82c1438
diff --git a/src/common/architecture/entrypoint/EntrypointScriptRunner.ts b/src/common/architecture/entrypoint/EntrypointScriptRunner.ts
index d47e947..02ce31e 100644
--- a/src/common/architecture/entrypoint/EntrypointScriptRunner.ts
+++ b/src/common/architecture/entrypoint/EntrypointScriptRunner.ts
@@ -1,19 +1,25 @@
 import Features from '../../../features/Features';
+import StandaloneScripts from '../../../scripts/Scripts';
+import ScriptProvider from '../scripts/ScriptProvider';
 import ScriptRunner from '../scripts/ScriptRunner';
 import { Context } from './Context';
 
 export default class EntrypointScriptRunner {
-  private features: Features;
   private scriptRunner: ScriptRunner;
 
   constructor(public context: Context) {
-    this.features = new Features();
     this.scriptRunner = new ScriptRunner();
+    this.addScriptProvider(new Features());
+    this.addScriptProvider(new StandaloneScripts());
+  }
+
+  addScriptProvider(scriptProvider: ScriptProvider): EntrypointScriptRunner {
+    const scripts = scriptProvider.getScripts(this.context);
+    this.scriptRunner.add(...scripts);
+    return this;
   }
 
   run() {
-    const scripts = this.features.getScripts(this.context);
-    this.scriptRunner.add(...scripts);
     this.scriptRunner.run();
   }
 }
diff --git a/src/common/architecture/scripts/ScriptFilterListProvider.ts b/src/common/architecture/scripts/ScriptFilterListProvider.ts
new file mode 100644
index 0000000..275a08c
--- /dev/null
+++ b/src/common/architecture/scripts/ScriptFilterListProvider.ts
@@ -0,0 +1,19 @@
+import { Context } from '../entrypoint/Context';
+import Script from './Script';
+import ScriptProvider from './ScriptProvider';
+
+export default abstract class ScriptFilterListProvider
+  implements ScriptProvider
+{
+  protected abstract getUnfilteredScriptsList(): Script[];
+
+  getScripts(context: Context) {
+    const scripts = this.getUnfilteredScriptsList();
+    return scripts.filter(
+      (script) =>
+        script.page === context.page &&
+        script.environment === context.environment &&
+        script.runPhase === context.runPhase,
+    );
+  }
+}
diff --git a/src/common/architecture/scripts/ScriptProvider.ts b/src/common/architecture/scripts/ScriptProvider.ts
new file mode 100644
index 0000000..e18e405
--- /dev/null
+++ b/src/common/architecture/scripts/ScriptProvider.ts
@@ -0,0 +1,6 @@
+import { Context } from "../entrypoint/Context";
+import Script from "./Script";
+
+export default interface ScriptProvider {
+  getScripts(context: Context): Script[];
+}
diff --git a/src/contentScripts/communityConsole/start.js b/src/contentScripts/communityConsole/start.js
index 67142ca..33d2039 100644
--- a/src/contentScripts/communityConsole/start.js
+++ b/src/contentScripts/communityConsole/start.js
@@ -1,8 +1,5 @@
 import {injectScript, injectStylesheet} from '../../common/contentScriptsUtils.js';
-import MWI18nServer from '../../common/mainWorldI18n/Server.js';
-import MWOptionsWatcherServer from '../../common/mainWorldOptionsWatcher/Server.js';
 import {getOptions} from '../../common/optionsUtils.js';
-import {kCSTarget, kMWTarget} from '../../xhrInterceptor/responseModifiers/index.js';
 
 import ExtraInfo from './extraInfo/index.js';
 import FlattenThreadsReplyActionHandler from './flattenThreads/replyActionHandler.js';
@@ -12,15 +9,6 @@
 const SMEI_NESTED_REPLIES = 15;
 const SMEI_RCE_THREAD_INTEROP = 22;
 
-// The servers should be available as soon as possible, since e.g. the XHRProxy
-// already sends a request to the optionsWatcher server as soon as it is
-// constructed.
-new MWOptionsWatcherServer(kCSTarget, kMWTarget);
-new MWI18nServer();
-
-injectScript(
-    chrome.runtime.getURL('xhrInterceptorInject.bundle.js'),
-    /* prepend = */ true);
 injectScript(chrome.runtime.getURL('extraInfoInject.bundle.js'));
 
 getOptions(null).then(options => {
diff --git a/src/features/Features.ts b/src/features/Features.ts
index 7b563bd..6eab170 100644
--- a/src/features/Features.ts
+++ b/src/features/Features.ts
@@ -1,34 +1,20 @@
 import Feature from '../common/architecture/features/Feature';
-import ScriptRunner from '../common/architecture/scripts/ScriptRunner';
 import AutoRefreshFeature from './autoRefresh/autoRefresh.feature';
 import InfiniteScrollFeature from './infiniteScroll/infiniteScroll.feature';
-import { Context } from '../common/architecture/entrypoint/Context';
+import ScriptFilterListProvider from '../common/architecture/scripts/ScriptFilterListProvider';
 
 export type ConcreteFeatureClass = { new (): Feature };
 
-export default class Features {
+export default class Features extends ScriptFilterListProvider {
   private features: ConcreteFeatureClass[] = [
     AutoRefreshFeature,
     InfiniteScrollFeature,
   ];
   private initializedFeatures: Feature[];
 
-  getScriptRunner(context: Context) {
-    const scripts = this.getScripts(context);
-    const scriptRunner = new ScriptRunner();
-    scriptRunner.add(...scripts);
-    return scriptRunner;
-  }
-
-  getScripts(context: Context) {
+  protected getUnfilteredScriptsList() {
     const features = this.getFeatures();
-    const allScripts = features.map((feature) => feature.getScripts()).flat(1);
-    return allScripts.filter(
-      (script) =>
-        script.page === context.page &&
-        script.environment === context.environment &&
-        script.runPhase === context.runPhase,
-    );
+    return features.map((feature) => feature.getScripts()).flat(1);
   }
 
   private getFeatures() {
diff --git a/src/scripts/Scripts.ts b/src/scripts/Scripts.ts
new file mode 100644
index 0000000..dca90d6
--- /dev/null
+++ b/src/scripts/Scripts.ts
@@ -0,0 +1,21 @@
+import Script, { ConcreteScript } from '../common/architecture/scripts/Script';
+import ScriptFilterListProvider from '../common/architecture/scripts/ScriptFilterListProvider';
+import MWI18nServerScript from './mainWorldServers/MWI18nServerScript.script';
+import MWOptionsWatcherServerScript from './mainWorldServers/MWOptionsWatcherServerScript.script';
+import XHRInterceptorScript from './xhrInterceptor/xhrInterceptor.script';
+
+export default class StandaloneScripts extends ScriptFilterListProvider {
+  private scripts: ConcreteScript[] = [
+    MWI18nServerScript,
+    MWOptionsWatcherServerScript,
+    XHRInterceptorScript,
+  ];
+  private initializedScripts: Script[];
+
+  protected getUnfilteredScriptsList() {
+    if (this.initializedScripts === undefined) {
+      this.initializedScripts = this.scripts.map((script) => new script());
+    }
+    return this.initializedScripts;
+  }
+}
diff --git a/src/scripts/mainWorldServers/MWI18nServerScript.script.ts b/src/scripts/mainWorldServers/MWI18nServerScript.script.ts
new file mode 100644
index 0000000..dbd87fe
--- /dev/null
+++ b/src/scripts/mainWorldServers/MWI18nServerScript.script.ts
@@ -0,0 +1,17 @@
+import Script, { ScriptEnvironment, ScriptPage, ScriptRunPhase } from "../../common/architecture/scripts/Script"
+import MWI18nServer from "../../common/mainWorldI18n/Server";
+
+export default class MWI18nServerScript extends Script {
+  // The server should be available as soon as possible, since e.g. the
+  // XHRProxy already sends a request to the optionsWatcher server as soon as it
+  // is constructed.
+  priority = 1;
+
+  page = ScriptPage.CommunityConsole;
+  environment = ScriptEnvironment.ContentScript;
+  runPhase = ScriptRunPhase.Start;
+
+  execute() {
+    new MWI18nServer();
+  }
+}
diff --git a/src/scripts/mainWorldServers/MWOptionsWatcherServerScript.script.ts b/src/scripts/mainWorldServers/MWOptionsWatcherServerScript.script.ts
new file mode 100644
index 0000000..a7aa405
--- /dev/null
+++ b/src/scripts/mainWorldServers/MWOptionsWatcherServerScript.script.ts
@@ -0,0 +1,18 @@
+import Script, { ScriptEnvironment, ScriptPage, ScriptRunPhase } from "../../common/architecture/scripts/Script"
+import MWOptionsWatcherServer from "../../common/mainWorldOptionsWatcher/Server"
+import { kCSTarget, kMWTarget } from "../../xhrInterceptor/responseModifiers"
+
+export default class MWOptionsWatcherServerScript extends Script {
+  // The server should be available as soon as possible, since e.g. the
+  // XHRProxy already sends a request to the optionsWatcher server as soon as it
+  // is constructed.
+  priority = 0;
+
+  page = ScriptPage.CommunityConsole;
+  environment = ScriptEnvironment.ContentScript;
+  runPhase = ScriptRunPhase.Start;
+
+  execute() {
+    new MWOptionsWatcherServer(kCSTarget, kMWTarget);
+  }
+}
diff --git a/src/scripts/xhrInterceptor/xhrInterceptor.script.ts b/src/scripts/xhrInterceptor/xhrInterceptor.script.ts
new file mode 100644
index 0000000..551c85f
--- /dev/null
+++ b/src/scripts/xhrInterceptor/xhrInterceptor.script.ts
@@ -0,0 +1,21 @@
+import Script, {
+  ScriptEnvironment,
+  ScriptPage,
+  ScriptRunPhase,
+} from '../../common/architecture/scripts/Script';
+import { injectScript } from '../../common/contentScriptsUtils';
+
+export default class XHRInterceptorScript extends Script {
+  priority = 10;
+
+  page = ScriptPage.CommunityConsole;
+  environment = ScriptEnvironment.ContentScript;
+  runPhase = ScriptRunPhase.Start;
+
+  execute() {
+    injectScript(
+      chrome.runtime.getURL('xhrInterceptorInject.bundle.js'),
+      /* prepend = */ true,
+    );
+  }
+}