From bcb8fbb9ac80c2fb36d10da300b2174d13a5a269 Mon Sep 17 00:00:00 2001
From: Paul <paul.marechal@ericsson.com>
Date: Tue, 8 Jun 2021 19:35:48 -0400
Subject: [PATCH] application-package: refine config typings

Rework how the different application configurations are defined to allow
partial setting when calling configuration providers.

Use default values when setting a partial configuration through the
configuration providers.
---
 .../src/application-props.ts                  | 223 +++++++++++-------
 ...ontend-application-config-provider.spec.ts |  49 ++++
 .../frontend-application-config-provider.ts   |   6 +-
 .../electron-main-application.ts              |   2 +-
 ...ackend-application-config-provider.spec.ts |  29 +++
 .../backend-application-config-provider.ts    |   6 +-
 6 files changed, 221 insertions(+), 94 deletions(-)
 create mode 100644 packages/core/src/browser/frontend-application-config-provider.spec.ts
 create mode 100644 packages/core/src/node/backend-application-config-provider.spec.ts

diff --git a/dev-packages/application-package/src/application-props.ts b/dev-packages/application-package/src/application-props.ts
index 1c78feacccb0b..a6e0701728cb3 100644
--- a/dev-packages/application-package/src/application-props.ts
+++ b/dev-packages/application-package/src/application-props.ts
@@ -16,6 +16,129 @@
 
 import type { BrowserWindowConstructorOptions } from 'electron';
 
+/** deepmerge/dist/umd */
+const merge = require('deepmerge/dist/umd');
+export { merge };
+
+export type RequiredRecursive<T> = {
+    [K in keyof T]-?: T[K] extends object ? RequiredRecursive<T[K]> : T[K]
+};
+
+/**
+ * Base configuration for the Theia application.
+ */
+export interface ApplicationConfig {
+    // eslint-disable-next-line @typescript-eslint/no-explicit-any
+    readonly [key: string]: any;
+}
+
+export type ElectronFrontendApplicationConfig = RequiredRecursive<ElectronFrontendApplicationConfig.Partial>;
+export namespace ElectronFrontendApplicationConfig {
+    export const DEFAULT: ElectronFrontendApplicationConfig = {
+        disallowReloadKeybinding: false,
+        windowOptions: {}
+    };
+    export interface Partial {
+
+        /**
+         * If set to `true`, reloading the current browser window won't be possible with the `Ctrl/Cmd + R` keybinding.
+         *
+         * Has no effect if not in an electron environment.
+         *
+         * Defaults to `false`.
+         */
+        readonly disallowReloadKeybinding?: boolean;
+
+        /**
+         * Override or add properties to the electron `windowOptions`.
+         *
+         * Defaults to `{}`.
+         */
+        readonly windowOptions?: BrowserWindowConstructorOptions;
+    }
+}
+
+/**
+ * Application configuration for the frontend. The following properties will be injected into the `index.html`.
+ */
+export type FrontendApplicationConfig = RequiredRecursive<FrontendApplicationConfig.Partial>;
+export namespace FrontendApplicationConfig {
+    export const DEFAULT: FrontendApplicationConfig = {
+        applicationName: 'Eclipse Theia',
+        defaultTheme: 'dark',
+        defaultIconTheme: 'none',
+        electron: ElectronFrontendApplicationConfig.DEFAULT
+    };
+    export interface Partial extends ApplicationConfig {
+
+        /**
+         * The default theme for the application.
+         *
+         * Defaults to `dark`.
+         */
+        readonly defaultTheme?: string;
+
+        /**
+         * The default icon theme for the application.
+         *
+         * Defaults to `none`.
+         */
+        readonly defaultIconTheme?: string;
+
+        /**
+         * The name of the application.
+         *
+         * Defaults to `Eclipse Theia`.
+         */
+        readonly applicationName?: string;
+
+        /**
+         * Electron specific configuration.
+         *
+         * Defaults to `ElectronFrontendApplicationConfig.DEFAULT`.
+         */
+        readonly electron?: ElectronFrontendApplicationConfig.Partial;
+    }
+}
+
+/**
+ * Application configuration for the backend.
+ */
+export type BackendApplicationConfig = RequiredRecursive<BackendApplicationConfig.Partial>;
+export namespace BackendApplicationConfig {
+    export const DEFAULT: BackendApplicationConfig = {
+        singleInstance: false,
+    };
+    export interface Partial extends ApplicationConfig {
+
+        /**
+         * If true and in Electron mode, only one instance of the application is allowed to run at a time.
+         *
+         * Defaults to `false`.
+         */
+        readonly singleInstance?: boolean;
+    }
+}
+
+/**
+ * Configuration for the generator.
+ */
+export type GeneratorConfig = RequiredRecursive<GeneratorConfig.Partial>;
+export namespace GeneratorConfig {
+    export const DEFAULT: GeneratorConfig = {
+        preloadTemplate: ''
+    };
+    export interface Partial {
+
+        /**
+         * Template to use for extra preload content markup (file path or HTML).
+         *
+         * Defaults to `''`.
+         */
+        readonly preloadTemplate?: string;
+    }
+}
+
 export interface NpmRegistryProps {
 
     /**
@@ -52,116 +175,42 @@ export interface ApplicationProps extends NpmRegistryProps {
     /**
      * Frontend related properties.
      */
-    readonly frontend: Readonly<{ config: FrontendApplicationConfig }>;
+    readonly frontend: {
+        readonly config: FrontendApplicationConfig
+    };
 
     /**
      * Backend specific properties.
      */
-    readonly backend: Readonly<{ config: BackendApplicationConfig }>;
+    readonly backend: {
+        readonly config: BackendApplicationConfig
+    };
 
     /**
      * Generator specific properties.
      */
-    readonly generator: Readonly<{ config: GeneratorConfig }>;
+    readonly generator: {
+        readonly config: GeneratorConfig
+    };
 }
 export namespace ApplicationProps {
+    export type Target = keyof typeof ApplicationTarget;
     export enum ApplicationTarget {
         browser = 'browser',
         electron = 'electron'
     };
-
-    export type Target = keyof typeof ApplicationTarget;
-
     export const DEFAULT: ApplicationProps = {
         ...NpmRegistryProps.DEFAULT,
         target: 'browser',
         backend: {
-            config: {}
+            config: BackendApplicationConfig.DEFAULT
         },
         frontend: {
-            config: {
-                applicationName: 'Eclipse Theia',
-                defaultTheme: 'dark',
-                defaultIconTheme: 'none'
-            }
+            config: FrontendApplicationConfig.DEFAULT
         },
         generator: {
-            config: {
-                preloadTemplate: ''
-            }
+            config: GeneratorConfig.DEFAULT
         }
     };
 
 }
-
-/**
- * Base configuration for the Theia application.
- */
-export interface ApplicationConfig {
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    readonly [key: string]: any;
-}
-
-/**
- * Application configuration for the frontend. The following properties will be injected into the `index.html`.
- */
-export interface FrontendApplicationConfig extends ApplicationConfig {
-
-    /**
-     * The default theme for the application. If not given, defaults to `dark`. If invalid theme is given, also defaults to `dark`.
-     */
-    readonly defaultTheme: string;
-
-    /**
-     * The default icon theme for the application. If not given, defaults to `none`. If invalid theme is given, also defaults to `none`.
-     */
-    readonly defaultIconTheme: string;
-
-    /**
-     * The name of the application. `Eclipse Theia` by default.
-     */
-    readonly applicationName: string;
-
-    /**
-     * Electron specific configuration.
-     */
-    readonly electron?: Readonly<ElectronFrontendApplicationConfig>;
-}
-
-export interface ElectronFrontendApplicationConfig {
-
-    /**
-     * If set to `true`, reloading the current browser window won't be possible with the `Ctrl/Cmd + R` keybinding.
-     * It is `false` by default. Has no effect if not in an electron environment.
-     */
-    readonly disallowReloadKeybinding?: boolean;
-
-    /**
-     * Override or add properties to the electron `windowOptions`.
-     */
-    readonly windowOptions?: BrowserWindowConstructorOptions;
-}
-
-/**
- * Application configuration for the backend.
- */
-export interface BackendApplicationConfig extends ApplicationConfig {
-
-    /**
-     * If true and in Electron mode, only one instance of the application is allowed to run at a time.
-     */
-    singleInstance?: boolean;
-
-}
-
-/**
- * Configuration for the generator.
- */
-export interface GeneratorConfig {
-
-    /**
-     * Template to use for extra preload content markup (file path or HTML)
-     */
-    readonly preloadTemplate: string;
-
-}
diff --git a/packages/core/src/browser/frontend-application-config-provider.spec.ts b/packages/core/src/browser/frontend-application-config-provider.spec.ts
new file mode 100644
index 0000000000000..6521730f00afe
--- /dev/null
+++ b/packages/core/src/browser/frontend-application-config-provider.spec.ts
@@ -0,0 +1,49 @@
+/********************************************************************************
+ * Copyright (C) 2021 Ericsson and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { enableJSDOM } from '../browser/test/jsdom';
+let disableJSDOM = enableJSDOM();
+
+import { FrontendApplicationConfig } from '@theia/application-package/lib/';
+import { expect } from 'chai';
+import { FrontendApplicationConfigProvider } from './frontend-application-config-provider';
+
+disableJSDOM();
+
+const { DEFAULT } = FrontendApplicationConfig;
+
+describe('FrontendApplicationConfigProvider', function (): void {
+
+    before(() => disableJSDOM = enableJSDOM());
+    after(() => disableJSDOM());
+
+    it('should use defaults when calling `set`', function (): void {
+        FrontendApplicationConfigProvider.set({
+            applicationName: DEFAULT.applicationName + ' Something Else',
+            electron: {
+                disallowReloadKeybinding: !DEFAULT.electron.disallowReloadKeybinding
+            }
+        });
+        const config = FrontendApplicationConfigProvider.get();
+        // custom values
+        expect(config.applicationName).not.equal(DEFAULT.applicationName);
+        expect(config.electron.disallowReloadKeybinding).not.equal(DEFAULT.electron.disallowReloadKeybinding);
+        // defaults
+        expect(config.defaultIconTheme).equal(DEFAULT.defaultIconTheme);
+        expect(config.defaultTheme).equal(DEFAULT.defaultTheme);
+        expect(config.electron.windowOptions).deep.equal(DEFAULT.electron.windowOptions);
+    });
+});
diff --git a/packages/core/src/browser/frontend-application-config-provider.ts b/packages/core/src/browser/frontend-application-config-provider.ts
index d422dd2169993..381da426dfce3 100644
--- a/packages/core/src/browser/frontend-application-config-provider.ts
+++ b/packages/core/src/browser/frontend-application-config-provider.ts
@@ -14,7 +14,7 @@
  * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
  ********************************************************************************/
 
-import { FrontendApplicationConfig } from '@theia/application-package/lib/application-props';
+import { FrontendApplicationConfig, merge } from '@theia/application-package/lib/application-props';
 
 export class FrontendApplicationConfigProvider {
 
@@ -28,14 +28,14 @@ export class FrontendApplicationConfigProvider {
         return config;
     }
 
-    static set(config: FrontendApplicationConfig): void {
+    static set(config: FrontendApplicationConfig.Partial): void {
         if (FrontendApplicationConfigProvider.doGet() !== undefined) {
             throw new Error('The configuration is already set.');
         }
         // eslint-disable-next-line @typescript-eslint/no-explicit-any
         const globalObject = window as any;
         const key = FrontendApplicationConfigProvider.KEY;
-        globalObject[key] = config;
+        globalObject[key] = merge(FrontendApplicationConfig.DEFAULT, config);
     }
 
     private static doGet(): FrontendApplicationConfig | undefined {
diff --git a/packages/core/src/electron-main/electron-main-application.ts b/packages/core/src/electron-main/electron-main-application.ts
index 06de8e06bc87d..65b801e85e914 100644
--- a/packages/core/src/electron-main/electron-main-application.ts
+++ b/packages/core/src/electron-main/electron-main-application.ts
@@ -225,7 +225,7 @@ export class ElectronMainApplication {
     }
 
     protected async getDefaultBrowserWindowOptions(): Promise<TheiaBrowserWindowOptions> {
-        const windowOptionsFromConfig = this.config.electron?.windowOptions || {};
+        const windowOptionsFromConfig = this.config.electron.windowOptions;
         let windowState: TheiaBrowserWindowOptions | undefined = this.electronStore.get('windowstate', undefined);
         if (!windowState) {
             windowState = this.getDefaultWindowState();
diff --git a/packages/core/src/node/backend-application-config-provider.spec.ts b/packages/core/src/node/backend-application-config-provider.spec.ts
new file mode 100644
index 0000000000000..b321e422ccee3
--- /dev/null
+++ b/packages/core/src/node/backend-application-config-provider.spec.ts
@@ -0,0 +1,29 @@
+/********************************************************************************
+ * Copyright (C) 2021 Ericsson and others.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the Eclipse
+ * Public License v. 2.0 are satisfied: GNU General Public License, version 2
+ * with the GNU Classpath Exception which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ ********************************************************************************/
+
+import { BackendApplicationConfig } from '@theia/application-package/lib/';
+import { expect } from 'chai';
+import { BackendApplicationConfigProvider } from './backend-application-config-provider';
+
+const { DEFAULT } = BackendApplicationConfig;
+
+describe('BackendApplicationConfigProvider', function (): void {
+    it('should use defaults when calling `set`', function (): void {
+        BackendApplicationConfigProvider.set({});
+        const config = BackendApplicationConfigProvider.get();
+        expect(config.singleInstance).equal(DEFAULT.singleInstance);
+    });
+});
diff --git a/packages/core/src/node/backend-application-config-provider.ts b/packages/core/src/node/backend-application-config-provider.ts
index 319a769107d9b..2d0446b711866 100644
--- a/packages/core/src/node/backend-application-config-provider.ts
+++ b/packages/core/src/node/backend-application-config-provider.ts
@@ -14,7 +14,7 @@
  * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
  ********************************************************************************/
 
-import { BackendApplicationConfig } from '@theia/application-package/lib/application-props';
+import { BackendApplicationConfig, merge } from '@theia/application-package/lib/application-props';
 
 export class BackendApplicationConfigProvider {
 
@@ -28,14 +28,14 @@ export class BackendApplicationConfigProvider {
         return config;
     }
 
-    static set(config: BackendApplicationConfig): void {
+    static set(config: BackendApplicationConfig.Partial): void {
         if (BackendApplicationConfigProvider.doGet() !== undefined) {
             throw new Error('The configuration is already set.');
         }
         // eslint-disable-next-line @typescript-eslint/no-explicit-any
         const globalObject = global as any;
         const key = BackendApplicationConfigProvider.KEY;
-        globalObject[key] = config;
+        globalObject[key] = merge(BackendApplicationConfig.DEFAULT, config);
     }
 
     private static doGet(): BackendApplicationConfig | undefined {