1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Extract minimal dependencies over a god class to satisfy Interface Segregation Principle and make stubbing in unit testing easier

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne Savolainen 2021-12-28 11:21:40 +02:00
parent 13873f5d85
commit 908a40975e
No known key found for this signature in database
GPG Key ID: 5F465B5672372402
6 changed files with 142 additions and 38 deletions

View File

@ -22,17 +22,21 @@ import { getInjectable } from "@ogre-tools/injectable";
import { lifecycleEnum } from "@ogre-tools/injectable"; import { lifecycleEnum } from "@ogre-tools/injectable";
import { ExtensionDiscovery } from "./extension-discovery"; import { ExtensionDiscovery } from "./extension-discovery";
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable"; import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
import extensionInstallerInjectable from "../extension-installer/extension-installer.injectable";
import isCompatibleExtensionInjectable from "./is-compatible-extension/is-compatible-extension.injectable"; import isCompatibleExtensionInjectable from "./is-compatible-extension/is-compatible-extension.injectable";
import isCompatibleBundledExtensionInjectable from "./is-compatible-bundled-extension/is-compatible-bundled-extension.injectable"; import isCompatibleBundledExtensionInjectable from "./is-compatible-bundled-extension/is-compatible-bundled-extension.injectable";
import extensionsStoreInjectable from "../extensions-store/extensions-store.injectable"; import extensionsStoreInjectable from "../extensions-store/extensions-store.injectable";
import extensionInstallationStateStoreInjectable from "../extension-installation-state-store/extension-installation-state-store.injectable"; import extensionInstallationStateStoreInjectable from "../extension-installation-state-store/extension-installation-state-store.injectable";
import installExtensionInjectable
from "../extension-installer/install-extension/install-extension.injectable";
import extensionPackageRootDirectoryInjectable
from "../extension-installer/extension-package-root-directory/extension-package-root-directory.injectable";
import installExtensionsInjectable
from "../extension-installer/install-extensions/install-extensions.injectable";
const extensionDiscoveryInjectable = getInjectable({ const extensionDiscoveryInjectable = getInjectable({
instantiate: (di) => instantiate: (di) =>
new ExtensionDiscovery({ new ExtensionDiscovery({
extensionLoader: di.inject(extensionLoaderInjectable), extensionLoader: di.inject(extensionLoaderInjectable),
extensionInstaller: di.inject(extensionInstallerInjectable),
extensionsStore: di.inject(extensionsStoreInjectable), extensionsStore: di.inject(extensionsStoreInjectable),
extensionInstallationStateStore: di.inject( extensionInstallationStateStore: di.inject(
@ -44,6 +48,10 @@ const extensionDiscoveryInjectable = getInjectable({
), ),
isCompatibleExtension: di.inject(isCompatibleExtensionInjectable), isCompatibleExtension: di.inject(isCompatibleExtensionInjectable),
installExtension: di.inject(installExtensionInjectable),
installExtensions: di.inject(installExtensionsInjectable),
extensionPackageRootDirectory: di.inject(extensionPackageRootDirectoryInjectable),
}), }),
lifecycle: lifecycleEnum.singleton, lifecycle: lifecycleEnum.singleton,

View File

@ -28,7 +28,8 @@ import { Console } from "console";
import { AppPaths } from "../../common/app-paths"; import { AppPaths } from "../../common/app-paths";
import { getDiForUnitTesting } from "../getDiForUnitTesting"; import { getDiForUnitTesting } from "../getDiForUnitTesting";
import extensionDiscoveryInjectable from "./extension-discovery.injectable"; import extensionDiscoveryInjectable from "./extension-discovery.injectable";
import extensionInstallerInjectable from "../extension-installer/extension-installer.injectable"; import extensionPackageRootDirectoryInjectable from "../extension-installer/extension-package-root-directory/extension-package-root-directory.injectable";
import installExtensionInjectable from "../extension-installer/install-extension/install-extension.injectable";
jest.setTimeout(60_000); jest.setTimeout(60_000);
@ -63,16 +64,12 @@ describe("ExtensionDiscovery", () => {
beforeEach(() => { beforeEach(() => {
const di = getDiForUnitTesting(); const di = getDiForUnitTesting();
const extensionInstallerStub = { di.override(installExtensionInjectable, () => () => Promise.resolve());
installPackages: () => Promise.resolve(),
npm: () => Promise.resolve(),
extensionPackagesRoot: "some-extension-packages-root",
npmPath: "some-npm-path",
installPackage: jest.fn(),
};
// @ts-ignore di.override(
di.override(extensionInstallerInjectable, () => extensionInstallerStub); extensionPackageRootDirectoryInjectable,
() => "some-extension-packages-root",
);
extensionDiscovery = di.inject(extensionDiscoveryInjectable); extensionDiscovery = di.inject(extensionDiscoveryInjectable);
}); });
@ -80,7 +77,8 @@ describe("ExtensionDiscovery", () => {
describe("with mockFs", () => { describe("with mockFs", () => {
beforeEach(() => { beforeEach(() => {
mockFs({ mockFs({
[`${os.homedir()}/.k8slens/extensions/my-extension/package.json`]: JSON.stringify({ [`${os.homedir()}/.k8slens/extensions/my-extension/package.json`]:
JSON.stringify({
name: "my-extension", name: "my-extension",
}), }),
}); });
@ -110,10 +108,12 @@ describe("ExtensionDiscovery", () => {
await extensionDiscovery.watchExtensions(); await extensionDiscovery.watchExtensions();
extensionDiscovery.events.on("add", extension => { extensionDiscovery.events.on("add", (extension) => {
expect(extension).toEqual({ expect(extension).toEqual({
absolutePath: expect.any(String), absolutePath: expect.any(String),
id: path.normalize("some-extension-packages-root/node_modules/my-extension/package.json"), id: path.normalize(
"some-extension-packages-root/node_modules/my-extension/package.json",
),
isBundled: false, isBundled: false,
isEnabled: false, isEnabled: false,
isCompatible: false, isCompatible: false,
@ -128,11 +128,16 @@ describe("ExtensionDiscovery", () => {
done(); done();
}); });
addHandler(path.join(extensionDiscovery.localFolderPath, "/my-extension/package.json")); addHandler(
path.join(
extensionDiscovery.localFolderPath,
"/my-extension/package.json",
),
);
}); });
}); });
it("doesn't emit add for added file under extension", async done => { it("doesn't emit add for added file under extension", async (done) => {
let addHandler: (filePath: string) => void; let addHandler: (filePath: string) => void;
const mockWatchInstance: any = { const mockWatchInstance: any = {
@ -156,7 +161,12 @@ describe("ExtensionDiscovery", () => {
extensionDiscovery.events.on("add", onAdd); extensionDiscovery.events.on("add", onAdd);
addHandler(path.join(extensionDiscovery.localFolderPath, "/my-extension/node_modules/dep/package.json")); addHandler(
path.join(
extensionDiscovery.localFolderPath,
"/my-extension/node_modules/dep/package.json",
),
);
setTimeout(() => { setTimeout(() => {
expect(onAdd).not.toHaveBeenCalled(); expect(onAdd).not.toHaveBeenCalled();

View File

@ -34,23 +34,25 @@ import {
} from "../../common/ipc"; } from "../../common/ipc";
import { toJS } from "../../common/utils"; import { toJS } from "../../common/utils";
import logger from "../../main/logger"; import logger from "../../main/logger";
import type { ExtensionInstaller } from "../extension-installer/extension-installer";
import type { ExtensionsStore } from "../extensions-store/extensions-store"; import type { ExtensionsStore } from "../extensions-store/extensions-store";
import type { ExtensionLoader } from "../extension-loader"; import type { ExtensionLoader } from "../extension-loader";
import type { LensExtensionId, LensExtensionManifest } from "../lens-extension"; import type { LensExtensionId, LensExtensionManifest } from "../lens-extension";
import { isProduction } from "../../common/vars"; import { isProduction } from "../../common/vars";
import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store"; import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store";
import type { PackageJson } from "type-fest";
interface Dependencies { interface Dependencies {
extensionLoader: ExtensionLoader; extensionLoader: ExtensionLoader;
extensionInstaller: ExtensionInstaller;
extensionsStore: ExtensionsStore; extensionsStore: ExtensionsStore;
extensionInstallationStateStore: ExtensionInstallationStateStore; extensionInstallationStateStore: ExtensionInstallationStateStore;
isCompatibleBundledExtension: (manifest: LensExtensionManifest) => boolean; isCompatibleBundledExtension: (manifest: LensExtensionManifest) => boolean;
isCompatibleExtension: (manifest: LensExtensionManifest) => boolean; isCompatibleExtension: (manifest: LensExtensionManifest) => boolean;
installExtension: (name: string) => Promise<void>;
installExtensions: (packageJsonPath: string, packagesJson: PackageJson) => Promise<void>
extensionPackageRootDirectory: string;
} }
export interface InstalledExtension { export interface InstalledExtension {
@ -124,11 +126,11 @@ export class ExtensionDiscovery {
} }
get packageJsonPath(): string { get packageJsonPath(): string {
return path.join(this.dependencies.extensionInstaller.extensionPackagesRoot, manifestFilename); return path.join(this.dependencies.extensionPackageRootDirectory, manifestFilename);
} }
get inTreeTargetPath(): string { get inTreeTargetPath(): string {
return path.join(this.dependencies.extensionInstaller.extensionPackagesRoot, "extensions"); return path.join(this.dependencies.extensionPackageRootDirectory, "extensions");
} }
get inTreeFolderPath(): string { get inTreeFolderPath(): string {
@ -136,7 +138,7 @@ export class ExtensionDiscovery {
} }
get nodeModulesPath(): string { get nodeModulesPath(): string {
return path.join(this.dependencies.extensionInstaller.extensionPackagesRoot, "node_modules"); return path.join(this.dependencies.extensionPackageRootDirectory, "node_modules");
} }
/** /**
@ -222,7 +224,7 @@ export class ExtensionDiscovery {
await fse.remove(extension.manifestPath); await fse.remove(extension.manifestPath);
// Install dependencies for the new extension // Install dependencies for the new extension
await this.installPackage(extension.absolutePath); await this.dependencies.installExtension(extension.absolutePath);
this.extensions.set(extension.id, extension); this.extensions.set(extension.id, extension);
logger.info(`${logModule} Added extension ${extension.manifest.name}`); logger.info(`${logModule} Added extension ${extension.manifest.name}`);
@ -309,15 +311,12 @@ export class ExtensionDiscovery {
this.loadStarted = true; this.loadStarted = true;
const extensionPackagesRoot =
this.dependencies.extensionInstaller.extensionPackagesRoot;
logger.info( logger.info(
`${logModule} loading extensions from ${extensionPackagesRoot}`, `${logModule} loading extensions from ${this.dependencies.extensionPackageRootDirectory}`,
); );
// fs.remove won't throw if path is missing // fs.remove won't throw if path is missing
await fse.remove(path.join(extensionPackagesRoot, "package-lock.json")); await fse.remove(path.join(this.dependencies.extensionPackageRootDirectory, "package-lock.json"));
try { try {
// Verify write access to static/extensions, which is needed for symlinking // Verify write access to static/extensions, which is needed for symlinking
@ -413,7 +412,7 @@ export class ExtensionDiscovery {
for (const extension of userExtensions) { for (const extension of userExtensions) {
if ((await fse.pathExists(extension.manifestPath)) === false) { if ((await fse.pathExists(extension.manifestPath)) === false) {
try { try {
await this.installPackage(extension.absolutePath); await this.dependencies.installExtension(extension.absolutePath);
} catch (error) { } catch (error) {
const message = error.message || error || "unknown error"; const message = error.message || error || "unknown error";
const { name, version } = extension.manifest; const { name, version } = extension.manifest;
@ -436,11 +435,7 @@ export class ExtensionDiscovery {
extensions.map(extension => [extension.manifest.name, extension.absolutePath]), extensions.map(extension => [extension.manifest.name, extension.absolutePath]),
); );
return this.dependencies.extensionInstaller.installPackages(packageJsonPath, { dependencies }); return this.dependencies.installExtensions(packageJsonPath, { dependencies });
}
async installPackage(name: string): Promise<void> {
return this.dependencies.extensionInstaller.installPackage(name);
} }
async loadBundledExtensions(): Promise<InstalledExtension[]> { async loadBundledExtensions(): Promise<InstalledExtension[]> {

View File

@ -0,0 +1,31 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import extensionInstallerInjectable from "../extension-installer.injectable";
const extensionPackageRootDirectoryInjectable = getInjectable({
instantiate: (di) =>
di.inject(extensionInstallerInjectable).extensionPackagesRoot,
lifecycle: lifecycleEnum.singleton,
});
export default extensionPackageRootDirectoryInjectable;

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import extensionInstallerInjectable from "../extension-installer.injectable";
const installExtensionInjectable = getInjectable({
instantiate: (di) => di.inject(extensionInstallerInjectable).installPackage,
lifecycle: lifecycleEnum.singleton,
});
export default installExtensionInjectable;

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) 2021 OpenLens Authors
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of
* this software and associated documentation files (the "Software"), to deal in
* the Software without restriction, including without limitation the rights to
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
* the Software, and to permit persons to whom the Software is furnished to do so,
* subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import extensionInstallerInjectable from "../extension-installer.injectable";
const installExtensionsInjectable = getInjectable({
instantiate: (di) => di.inject(extensionInstallerInjectable).installPackages,
lifecycle: lifecycleEnum.singleton,
});
export default installExtensionsInjectable;