From e7b10519f7ad01242afc8c460c267982917baf4c Mon Sep 17 00:00:00 2001
From: Jeremy Rifkin <51220084+jeremy-rifkin@users.noreply.github.com>
Date: Sat, 3 Dec 2022 19:37:57 -0500
Subject: [PATCH] Tsify options-handler (#4396)

* First pass conversion of options-handler to ts

* Small tweaks

* Fix tests
---
 lib/compiler-finder.ts                        |  20 +-
 ...{options-handler.js => options-handler.ts} | 199 ++++++++++++++----
 types/cache.interfaces.ts                     |   8 +-
 3 files changed, 168 insertions(+), 59 deletions(-)
 rename lib/{options-handler.js => options-handler.ts} (68%)

diff --git a/lib/compiler-finder.ts b/lib/compiler-finder.ts
index 332842441..64a57154e 100644
--- a/lib/compiler-finder.ts
+++ b/lib/compiler-finder.ts
@@ -37,26 +37,12 @@ import {Language} from '../types/languages.interfaces';
 import {InstanceFetcher} from './aws';
 import {CompileHandler} from './handlers/compile';
 import {logger} from './logger';
-import {ClientOptionsHandler} from './options-handler';
+import {ClientOptionsHandler, OptionHandlerArguments} from './options-handler';
 import {CompilerProps, RawPropertiesGetter} from './properties';
 import {PropertyGetter, PropertyValue, Widen} from './properties.interfaces';
 
 const sleep = promisify(setTimeout);
 
-export type CompilerFinderArguments = {
-    rootDir: string;
-    env: string[];
-    hostname: string[];
-    port: number;
-    gitReleaseName: string;
-    releaseBuildNumber: string;
-    wantedLanguages: string | null;
-    doCache: boolean;
-    fetchCompilersFromRemote: boolean;
-    ensureNoCompilerClash: boolean;
-    suppressConsoleLog: boolean;
-};
-
 /***
  * Finds and initializes the compilers stored on the properties files
  */
@@ -64,7 +50,7 @@ export class CompilerFinder {
     compilerProps: CompilerProps['get'];
     ceProps: PropertyGetter;
     awsProps: PropertyGetter;
-    args: CompilerFinderArguments;
+    args: OptionHandlerArguments;
     compileHandler: CompileHandler;
     languages: Record<string, Language>;
     awsPoller: InstanceFetcher | null = null;
@@ -74,7 +60,7 @@ export class CompilerFinder {
         compileHandler: CompileHandler,
         compilerProps: CompilerProps,
         awsProps: PropertyGetter,
-        args: CompilerFinderArguments,
+        args: OptionHandlerArguments,
         optionsHandler: ClientOptionsHandler,
     ) {
         this.compilerProps = compilerProps.get.bind(compilerProps);
diff --git a/lib/options-handler.js b/lib/options-handler.ts
similarity index 68%
rename from lib/options-handler.js
rename to lib/options-handler.ts
index 2bb78a522..0210b380b 100755
--- a/lib/options-handler.js
+++ b/lib/options-handler.ts
@@ -29,14 +29,96 @@ import fs from 'fs-extra';
 import semverParser from 'semver';
 import _ from 'underscore';
 
+import {LanguageKey} from '../types/languages.interfaces';
+
 import {logger} from './logger';
-import {getToolTypeByKey} from './tooling';
+import {CompilerProps} from './properties';
+import {PropertyGetter, PropertyValue} from './properties.interfaces';
+import {Source} from './sources';
+import {BaseTool, getToolTypeByKey} from './tooling';
+import {ToolTypeKey} from './tooling/base-tool.interface';
 import {asSafeVer, getHash, splitArguments, splitIntoArray} from './utils';
 
+// TODO: There is surely a better name for this type. Used both here and in the compiler finder.
+export type OptionHandlerArguments = {
+    rootDir: string;
+    env: string[];
+    hostname: string[];
+    port: number;
+    gitReleaseName: string;
+    releaseBuildNumber: string;
+    wantedLanguages: string | null;
+    doCache: boolean;
+    fetchCompilersFromRemote: boolean;
+    ensureNoCompilerClash: boolean;
+    suppressConsoleLog: boolean;
+};
+
+type OptionsType = {
+    googleAnalyticsAccount: string;
+    googleAnalyticsEnabled: boolean;
+    sharingEnabled: boolean;
+    githubEnabled: boolean;
+    showSponsors: boolean;
+    gapiKey: string;
+    googleShortLinkRewrite: string[];
+    urlShortenService: string;
+    defaultSource: string;
+    compilers: never[];
+    libs: Record<any, any>;
+    remoteLibs: Record<any, any>;
+    tools: Record<any, any>;
+    defaultLibs: string;
+    defaultCompiler: string;
+    compileOptions: string;
+    supportsBinary: boolean;
+    supportsExecute: boolean;
+    supportsLibraryCodeFilter: boolean;
+    languages: Record<string, any>;
+    sources: {
+        name: string;
+        urlpart: string;
+    }[];
+    sentryDsn: string;
+    sentryEnvironment: string | number | true;
+    release: string;
+    gitReleaseCommit: string;
+    cookieDomainRe: string;
+    localStoragePrefix: PropertyValue;
+    cvCompilerCountMax: number;
+    defaultFontScale: number;
+    doCache: boolean;
+    thirdPartyIntegrationEnabled: boolean;
+    statusTrackingEnabled: boolean;
+    policies: {
+        cookies: {
+            enabled: boolean;
+            key: string;
+        };
+        privacy: {
+            enabled: boolean;
+            key: string;
+        };
+    };
+    motdUrl: string;
+    pageloadUrl: string;
+};
+
 /***
  * Handles the setup of the options object passed on each page request
  */
 export class ClientOptionsHandler {
+    compilerProps: CompilerProps['get'];
+    ceProps: PropertyGetter;
+    supportsBinary: boolean;
+    supportsExecutePerLanguage: boolean;
+    supportsExecute: boolean;
+    supportsLibraryCodeFilterPerLanguage: boolean;
+    supportsLibraryCodeFilter: boolean;
+    remoteLibs: Record<any, any>;
+    options: OptionsType;
+    optionsJSON: string;
+    optionsHash: string;
     /***
      *
      * @param {Object[]} fileSources - Files to show in the Load/Save pane
@@ -45,7 +127,7 @@ export class ClientOptionsHandler {
      * @param {CompilerProps} compilerProps
      * @param {Object} defArgs - Compiler Explorer arguments
      */
-    constructor(fileSources, compilerProps, defArgs) {
+    constructor(fileSources: Source[], compilerProps: CompilerProps, defArgs: OptionHandlerArguments) {
         this.compilerProps = compilerProps.get.bind(compilerProps);
         this.ceProps = compilerProps.ceProps;
         const ceProps = compilerProps.ceProps;
@@ -68,8 +150,9 @@ export class ClientOptionsHandler {
         this.supportsLibraryCodeFilterPerLanguage = this.compilerProps(languages, 'supportsLibraryCodeFilter', false);
         this.supportsLibraryCodeFilter = Object.values(this.supportsLibraryCodeFilterPerLanguage).some(value => value);
 
-        const libs = this.parseLibraries(this.compilerProps(languages, 'libs'));
-        const tools = this.parseTools(this.compilerProps(languages, 'tools'));
+        // TODO: Shouldn't have to cast here
+        const libs = this.parseLibraries(this.compilerProps(languages, 'libs') as any);
+        const tools = this.parseTools(this.compilerProps(languages, 'tools') as any);
 
         this.remoteLibs = {};
 
@@ -122,37 +205,43 @@ export class ClientOptionsHandler {
             motdUrl: ceProps('motdUrl', ''),
             pageloadUrl: ceProps('pageloadUrl', ''),
         };
+        // Will be immediately replaced with actual values
+        this.optionsJSON = '';
+        this.optionsHash = '';
         this._updateOptionsHash();
     }
 
-    parseTools(baseTools) {
-        const tools = {};
+    parseTools(baseTools: Record<string, string>) {
+        const tools: Record<string, Record<string, BaseTool>> = {};
         for (const [lang, forLang] of Object.entries(baseTools)) {
             if (lang && forLang) {
                 tools[lang] = {};
                 for (const tool of forLang.split(':')) {
                     const toolBaseName = `tools.${tool}`;
-                    const className = this.compilerProps(lang, toolBaseName + '.class');
+                    const className = this.compilerProps<string>(lang, toolBaseName + '.class');
                     const Tool = getToolTypeByKey(className);
 
-                    const toolPath = this.compilerProps(lang, toolBaseName + '.exe');
+                    const toolPath = this.compilerProps<string>(lang, toolBaseName + '.exe');
                     if (fs.existsSync(toolPath)) {
                         tools[lang][tool] = new Tool(
                             {
                                 id: tool,
-                                name: this.compilerProps(lang, toolBaseName + '.name'),
-                                type: this.compilerProps(lang, toolBaseName + '.type'),
+                                name: this.compilerProps<string>(lang, toolBaseName + '.name'),
+                                type: this.compilerProps<string>(lang, toolBaseName + '.type') as ToolTypeKey,
                                 exe: toolPath,
-                                exclude: splitIntoArray(this.compilerProps(lang, toolBaseName + '.exclude')),
-                                includeKey: this.compilerProps(lang, toolBaseName + '.includeKey'),
-                                options: splitArguments(this.compilerProps(lang, toolBaseName + '.options')),
-                                args: this.compilerProps(lang, toolBaseName + '.args'),
-                                languageId: this.compilerProps(lang, toolBaseName + '.languageId'),
-                                stdinHint: this.compilerProps(lang, toolBaseName + '.stdinHint'),
-                                monacoStdin: this.compilerProps(lang, toolBaseName + '.monacoStdin'),
-                                icon: this.compilerProps(lang, toolBaseName + '.icon'),
-                                darkIcon: this.compilerProps(lang, toolBaseName + '.darkIcon'),
-                                compilerLanguage: lang,
+                                exclude: splitIntoArray(this.compilerProps<string>(lang, toolBaseName + '.exclude')),
+                                includeKey: this.compilerProps<string>(lang, toolBaseName + '.includeKey'),
+                                options: splitArguments(this.compilerProps<string>(lang, toolBaseName + '.options')),
+                                args: this.compilerProps<string>(lang, toolBaseName + '.args'),
+                                languageId: this.compilerProps<string>(
+                                    lang,
+                                    toolBaseName + '.languageId',
+                                ) as LanguageKey,
+                                stdinHint: this.compilerProps<string>(lang, toolBaseName + '.stdinHint'),
+                                monacoStdin: this.compilerProps<string>(lang, toolBaseName + '.monacoStdin'),
+                                icon: this.compilerProps<string>(lang, toolBaseName + '.icon'),
+                                darkIcon: this.compilerProps<string>(lang, toolBaseName + '.darkIcon'),
+                                compilerLanguage: lang as LanguageKey,
                             },
                             {
                                 ceProps: this.ceProps,
@@ -168,43 +257,67 @@ export class ClientOptionsHandler {
         return tools;
     }
 
-    parseLibraries(baseLibs) {
-        const libraries = {};
+    parseLibraries(baseLibs: Record<string, string>) {
+        type VersionInfo = {
+            version: string;
+            staticliblink: string[];
+            alias: string[];
+            dependencies: string[];
+            path: string[];
+            libpath: string[];
+            liblink: string[];
+            lookupversion?: PropertyValue;
+            options: string[];
+            hidden: boolean;
+        };
+        type Library = {
+            name: string;
+            url: string;
+            description: string;
+            staticliblink: string[];
+            liblink: string[];
+            dependencies: string[];
+            versions: Record<string, VersionInfo>;
+            examples: string[];
+            options: string[];
+        };
+        // Record language -> {Record lib name -> lib}
+        const libraries: Record<string, Record<string, Library>> = {};
         for (const [lang, forLang] of Object.entries(baseLibs)) {
             if (lang && forLang) {
                 libraries[lang] = {};
                 for (const lib of forLang.split(':')) {
                     const libBaseName = `libs.${lib}`;
                     libraries[lang][lib] = {
-                        name: this.compilerProps(lang, libBaseName + '.name'),
-                        url: this.compilerProps(lang, libBaseName + '.url'),
-                        description: this.compilerProps(lang, libBaseName + '.description'),
-                        staticliblink: splitIntoArray(this.compilerProps(lang, libBaseName + '.staticliblink')),
-                        liblink: splitIntoArray(this.compilerProps(lang, libBaseName + '.liblink')),
-                        dependencies: splitIntoArray(this.compilerProps(lang, libBaseName + '.dependencies')),
+                        name: this.compilerProps<string>(lang, libBaseName + '.name'),
+                        url: this.compilerProps<string>(lang, libBaseName + '.url'),
+                        description: this.compilerProps<string>(lang, libBaseName + '.description'),
+                        staticliblink: splitIntoArray(this.compilerProps<string>(lang, libBaseName + '.staticliblink')),
+                        liblink: splitIntoArray(this.compilerProps<string>(lang, libBaseName + '.liblink')),
+                        dependencies: splitIntoArray(this.compilerProps<string>(lang, libBaseName + '.dependencies')),
                         versions: {},
-                        examples: splitIntoArray(this.compilerProps(lang, libBaseName + '.examples')),
+                        examples: splitIntoArray(this.compilerProps<string>(lang, libBaseName + '.examples')),
                         options: splitArguments(this.compilerProps(lang, libBaseName + '.options', '')),
                     };
                     const listedVersions = `${this.compilerProps(lang, libBaseName + '.versions')}`;
                     if (listedVersions) {
                         for (const version of listedVersions.split(':')) {
                             const libVersionName = libBaseName + `.versions.${version}`;
-                            const versionObject = {
-                                version: this.compilerProps(lang, libVersionName + '.version'),
+                            const versionObject: VersionInfo = {
+                                version: this.compilerProps<string>(lang, libVersionName + '.version'),
                                 staticliblink: splitIntoArray(
-                                    this.compilerProps(lang, libVersionName + '.staticliblink'),
+                                    this.compilerProps<string>(lang, libVersionName + '.staticliblink'),
                                     libraries[lang][lib].staticliblink,
                                 ),
-                                alias: splitIntoArray(this.compilerProps(lang, libVersionName + '.alias')),
+                                alias: splitIntoArray(this.compilerProps<string>(lang, libVersionName + '.alias')),
                                 dependencies: splitIntoArray(
-                                    this.compilerProps(lang, libVersionName + '.dependencies'),
+                                    this.compilerProps<string>(lang, libVersionName + '.dependencies'),
                                     libraries[lang][lib].dependencies,
                                 ),
                                 path: [],
                                 libpath: [],
                                 liblink: splitIntoArray(
-                                    this.compilerProps(lang, libVersionName + '.liblink'),
+                                    this.compilerProps<string>(lang, libVersionName + '.liblink'),
                                     libraries[lang][lib].liblink,
                                 ),
                                 // Library options might get overridden later
@@ -217,19 +330,19 @@ export class ClientOptionsHandler {
                                 versionObject.lookupversion = lookupversion;
                             }
 
-                            const includes = this.compilerProps(lang, libVersionName + '.path');
+                            const includes = this.compilerProps<string>(lang, libVersionName + '.path');
                             if (includes) {
                                 versionObject.path = includes.split(path.delimiter);
                             } else {
                                 logger.warn(`Library ${lib} ${version} (${lang}) has no include paths`);
                             }
 
-                            const libpath = this.compilerProps(lang, libVersionName + '.libpath');
+                            const libpath = this.compilerProps<string>(lang, libVersionName + '.libpath');
                             if (libpath) {
                                 versionObject.libpath = libpath.split(path.delimiter);
                             }
 
-                            const options = this.compilerProps(lang, libVersionName + '.options');
+                            const options = this.compilerProps<string>(lang, libVersionName + '.options');
                             if (options !== undefined) {
                                 versionObject.options = splitArguments(options);
                             }
@@ -245,7 +358,11 @@ export class ClientOptionsHandler {
         for (const langGroup of Object.values(libraries)) {
             for (const libGroup of Object.values(langGroup)) {
                 const versions = Object.values(libGroup.versions);
-                versions.sort((a, b) => semverParser.compare(asSafeVer(a.semver), asSafeVer(b.semver), true));
+                // TODO: A and B don't contain any property called semver here. It's probably leftover from old code
+                // and should be removed in the future.
+                versions.sort((a, b) =>
+                    semverParser.compare(asSafeVer((a as any).semver), asSafeVer((b as any).semver), true),
+                );
                 let order = 0;
                 // Set $order to index on array. As group is an array, iteration order is guaranteed.
                 for (const lib of versions) {
@@ -309,7 +426,7 @@ export class ClientOptionsHandler {
         await this.getRemoteLibraries(language, remote.target);
     }
 
-    async setCompilers(compilers) {
+    async setCompilers(compilers: any[]) {
         const forbiddenKeys = new Set([
             'exe',
             'versionFlag',
@@ -322,7 +439,7 @@ export class ClientOptionsHandler {
             'isSemVer',
         ]);
         const copiedCompilers = JSON.parse(JSON.stringify(compilers));
-        let semverGroups = {};
+        const semverGroups: Record<string, any> = {};
         // Reset the supportsExecute flag in case critical compilers change
 
         for (const key of Object.keys(this.options.languages)) {
diff --git a/types/cache.interfaces.ts b/types/cache.interfaces.ts
index 66758c72c..114355360 100644
--- a/types/cache.interfaces.ts
+++ b/types/cache.interfaces.ts
@@ -28,4 +28,10 @@ export type GetResult = {
 };
 
 // Something that can be used as a value and passed to cache functions. A simple JSON-able type.
-export type CacheableValue = string | number | boolean | {[x: string]: CacheableValue} | Array<CacheableValue>;
+export type CacheableValue =
+    | string
+    | number
+    | boolean
+    | undefined
+    | {[x: string]: CacheableValue}
+    | Array<CacheableValue>;
-- 
GitLab