Cascade different refactorings to eliminate global shared state from app paths

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne SavolainenandMikko Aspiala committed 2022-01-05 13:26:49 +02:00
1 parent 908a40975e
commit 9d33fff906
336 files changed
+9105 -3307

No files matched your search

@@ -22,12 +22,12 @@
import type { ExtensionLoader } from "../extension-loader";
import { Console } from "console";
import { stdout, stderr } from "process";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import extensionLoaderInjectable from "../extension-loader/extension-loader.injectable";
import { AppPaths } from "../../common/app-paths";
import { runInAction } from "mobx";
import updateExtensionsStateInjectable
from "../extension-loader/update-extensions-state/update-extensions-state.injectable";
import { getDisForUnitTesting } from "../../test-utils/get-dis-for-unit-testing";
import mockFs from "mock-fs";
console = new Console(stdout, stderr);
@@ -122,21 +122,26 @@ jest.mock(
},
);
// TODO: Remove explicit global initialization at unclear time window
AppPaths.init();
describe("ExtensionLoader", () => {
let extensionLoader: ExtensionLoader;
let updateExtensionStateMock: jest.Mock;
beforeEach(() => {
const di = getDiForUnitTesting();
beforeEach(async () => {
const dis = getDisForUnitTesting({ doGeneralOverrides: true });
mockFs();
updateExtensionStateMock = jest.fn();
di.override(updateExtensionsStateInjectable, () => updateExtensionStateMock);
dis.mainDi.override(updateExtensionsStateInjectable, () => updateExtensionStateMock);
extensionLoader = di.inject(extensionLoaderInjectable);
await dis.runSetups();
extensionLoader = dis.mainDi.inject(extensionLoaderInjectable);
});
afterEach(() => {
mockFs.restore();
});
it("renderer updates extension after ipc broadcast", async done => {
+2 -2
View File
@@ -19,5 +19,5 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
export { appEventBus } from "../../common/event-bus";
export type { AppEvent } from "../../common/event-bus";
export { appEventBus } from "../../common/app-event-bus/event-bus";
export type { AppEvent } from "../../common/app-event-bus/event-bus";
@@ -26,12 +26,9 @@ import isCompatibleExtensionInjectable from "./is-compatible-extension/is-compat
import isCompatibleBundledExtensionInjectable from "./is-compatible-bundled-extension/is-compatible-bundled-extension.injectable";
import extensionsStoreInjectable from "../extensions-store/extensions-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";
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({
instantiate: (di) =>
@@ -51,7 +48,10 @@ const extensionDiscoveryInjectable = getInjectable({
installExtension: di.inject(installExtensionInjectable),
installExtensions: di.inject(installExtensionsInjectable),
extensionPackageRootDirectory: di.inject(extensionPackageRootDirectoryInjectable),
extensionPackageRootDirectory: di.inject(
extensionPackageRootDirectoryInjectable,
),
}),
lifecycle: lifecycleEnum.singleton,
@@ -25,11 +25,10 @@ import path from "path";
import type { ExtensionDiscovery } from "./extension-discovery";
import os from "os";
import { Console } from "console";
import { AppPaths } from "../../common/app-paths";
import { getDiForUnitTesting } from "../getDiForUnitTesting";
import extensionDiscoveryInjectable from "./extension-discovery.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";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
jest.setTimeout(60_000);
@@ -53,16 +52,22 @@ jest.mock("electron", () => ({
},
}));
AppPaths.init();
console = new Console(process.stdout, process.stderr); // fix mockFS
const mockedWatch = watch as jest.MockedFunction<typeof watch>;
describe("ExtensionDiscovery", () => {
let extensionDiscovery: ExtensionDiscovery;
beforeEach(() => {
const di = getDiForUnitTesting();
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
mockFs({
[`${os.homedir()}/.k8slens/extensions/my-extension/package.json`]:
JSON.stringify({
name: "my-extension",
}),
});
di.override(installExtensionInjectable, () => () => Promise.resolve());
@@ -71,70 +76,62 @@ describe("ExtensionDiscovery", () => {
() => "some-extension-packages-root",
);
await di.runSetups();
extensionDiscovery = di.inject(extensionDiscoveryInjectable);
});
describe("with mockFs", () => {
beforeEach(() => {
mockFs({
[`${os.homedir()}/.k8slens/extensions/my-extension/package.json`]:
JSON.stringify({
name: "my-extension",
}),
});
});
afterEach(() => {
mockFs.restore();
});
afterEach(() => {
mockFs.restore();
});
it("emits add for added extension", async (done) => {
let addHandler: (filePath: string) => void;
it("emits add for added extension", async (done) => {
let addHandler: (filePath: string) => void;
const mockWatchInstance: any = {
on: jest.fn((event: string, handler: typeof addHandler) => {
if (event === "add") {
addHandler = handler;
}
const mockWatchInstance: any = {
on: jest.fn((event: string, handler: typeof addHandler) => {
if (event === "add") {
addHandler = handler;
}
return mockWatchInstance;
}),
};
return mockWatchInstance;
}),
};
mockedWatch.mockImplementationOnce(() => mockWatchInstance as any);
mockedWatch.mockImplementationOnce(() => mockWatchInstance as any);
// Need to force isLoaded to be true so that the file watching is started
extensionDiscovery.isLoaded = true;
// Need to force isLoaded to be true so that the file watching is started
extensionDiscovery.isLoaded = true;
await extensionDiscovery.watchExtensions();
await extensionDiscovery.watchExtensions();
extensionDiscovery.events.on("add", (extension) => {
expect(extension).toEqual({
absolutePath: expect.any(String),
id: path.normalize(
"some-extension-packages-root/node_modules/my-extension/package.json",
),
isBundled: false,
isEnabled: false,
isCompatible: false,
manifest: {
name: "my-extension",
},
manifestPath: path.normalize(
"some-extension-packages-root/node_modules/my-extension/package.json",
),
});
done();
});
addHandler(
path.join(
extensionDiscovery.localFolderPath,
"/my-extension/package.json",
extensionDiscovery.events.on("add", (extension) => {
expect(extension).toEqual({
absolutePath: expect.any(String),
id: path.normalize(
"some-extension-packages-root/node_modules/my-extension/package.json",
),
);
isBundled: false,
isEnabled: false,
isCompatible: false,
manifest: {
name: "my-extension",
},
manifestPath: path.normalize(
"some-extension-packages-root/node_modules/my-extension/package.json",
),
});
done();
});
addHandler(
path.join(
extensionDiscovery.localFolderPath,
"/my-extension/package.json",
),
);
});
it("doesn't emit add for added file under extension", async (done) => {
@@ -20,9 +20,16 @@
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { ExtensionInstaller } from "./extension-installer";
import extensionPackageRootDirectoryInjectable from "./extension-package-root-directory/extension-package-root-directory.injectable";
const extensionInstallerInjectable = getInjectable({
instantiate: () => new ExtensionInstaller(),
instantiate: (di) =>
new ExtensionInstaller({
extensionPackageRootDirectory: di.inject(
extensionPackageRootDirectoryInjectable,
),
}),
lifecycle: lifecycleEnum.singleton,
});
@@ -24,20 +24,21 @@ import child_process from "child_process";
import fs from "fs-extra";
import path from "path";
import logger from "../../main/logger";
import { extensionPackagesRoot } from "../extension-loader";
import type { PackageJson } from "type-fest";
const logModule = "[EXTENSION-INSTALLER]";
interface Dependencies {
extensionPackageRootDirectory: string
}
/**
* Installs dependencies for extensions
*/
export class ExtensionInstaller {
private installLock = new AwaitLock();
get extensionPackagesRoot() {
return extensionPackagesRoot();
}
constructor(private dependencies: Dependencies) {}
get npmPath() {
return __non_webpack_require__.resolve("npm/bin/npm-cli");
@@ -46,7 +47,7 @@ export class ExtensionInstaller {
/**
* Write package.json to the file system and execute npm install for it.
*/
async installPackages(packageJsonPath: string, packagesJson: PackageJson): Promise<void> {
installPackages = async (packageJsonPath: string, packagesJson: PackageJson): Promise<void> => {
// Mutual exclusion to install packages in sequence
await this.installLock.acquireAsync();
@@ -56,34 +57,34 @@ export class ExtensionInstaller {
mode: 0o600,
});
logger.info(`${logModule} installing dependencies at ${extensionPackagesRoot()}`);
logger.info(`${logModule} installing dependencies at ${this.dependencies.extensionPackageRootDirectory}`);
await this.npm(["install", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"]);
logger.info(`${logModule} dependencies installed at ${extensionPackagesRoot()}`);
logger.info(`${logModule} dependencies installed at ${this.dependencies.extensionPackageRootDirectory}`);
} finally {
this.installLock.release();
}
}
};
/**
* Install single package using npm
*/
async installPackage(name: string): Promise<void> {
installPackage = async (name: string): Promise<void> => {
// Mutual exclusion to install packages in sequence
await this.installLock.acquireAsync();
try {
logger.info(`${logModule} installing package from ${name} to ${extensionPackagesRoot()}`);
logger.info(`${logModule} installing package from ${name} to ${this.dependencies.extensionPackageRootDirectory}`);
await this.npm(["install", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock", "--no-save", name]);
logger.info(`${logModule} package ${name} installed to ${extensionPackagesRoot()}`);
logger.info(`${logModule} package ${name} installed to ${this.dependencies.extensionPackageRootDirectory}`);
} finally {
this.installLock.release();
}
}
};
private npm(args: string[]): Promise<void> {
return new Promise((resolve, reject) => {
const child = child_process.fork(this.npmPath, args, {
cwd: extensionPackagesRoot(),
cwd: this.dependencies.extensionPackageRootDirectory,
silent: true,
env: {},
});
@@ -19,12 +19,12 @@
* 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";
import directoryForUserDataInjectable
from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
const extensionPackageRootDirectoryInjectable = getInjectable({
instantiate: (di) =>
di.inject(extensionInstallerInjectable).extensionPackagesRoot,
instantiate: (di) => di.inject(directoryForUserDataInjectable),
lifecycle: lifecycleEnum.singleton,
});
@@ -23,7 +23,7 @@ import extensionInstallerInjectable from "../extension-installer.injectable";
const installExtensionsInjectable = getInjectable({
instantiate: (di) => di.inject(extensionInstallerInjectable).installPackages,
lifecycle: lifecycleEnum.singleton,
});
@@ -0,0 +1,33 @@
/**
* 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 { createExtensionInstance } from "./create-extension-instance";
import fileSystemProvisionerStoreInjectable from "./file-system-provisioner-store/file-system-provisioner-store.injectable";
const createExtensionInstanceInjectable = getInjectable({
instantiate: (di) => createExtensionInstance({
fileSystemProvisionerStore: di.inject(fileSystemProvisionerStoreInjectable),
}),
lifecycle: lifecycleEnum.singleton,
});
export default createExtensionInstanceInjectable;
@@ -18,40 +18,19 @@
* 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 glob from "glob";
import { memoize } from "lodash/fp";
import type { LensExtensionConstructor } from "../../lens-extension";
import type { InstalledExtension } from "../../extension-discovery/extension-discovery";
import {
createContainer,
ConfigurableDependencyInjectionContainer,
} from "@ogre-tools/injectable";
import { setLegacyGlobalDiForExtensionApi } from "./as-legacy-global-function-for-extension-api/legacy-global-di-for-extension-api";
LensExtensionDependencies,
setLensExtensionDependencies,
} from "../../lens-extension-set-dependencies";
export const getDiForUnitTesting = () => {
const di: ConfigurableDependencyInjectionContainer = createContainer();
export const createExtensionInstance =
(dependencies: LensExtensionDependencies) =>
(ExtensionClass: LensExtensionConstructor, extension: InstalledExtension) => {
const instance = new ExtensionClass(extension);
setLegacyGlobalDiForExtensionApi(di);
getInjectableFilePaths()
.map(key => {
// eslint-disable-next-line @typescript-eslint/no-var-requires
const injectable = require(key).default;
instance[setLensExtensionDependencies](dependencies);
return {
id: key,
...injectable,
aliases: [injectable, ...(injectable.aliases || [])],
};
})
.forEach(injectable => di.register(injectable));
di.preventSideEffects();
return di;
};
const getInjectableFilePaths = memoize(() => [
...glob.sync("./**/*.injectable.{ts,tsx}", { cwd: __dirname }),
]);
return instance;
};
@@ -0,0 +1,32 @@
/**
* 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 path from "path";
import directoryForUserDataInjectable from "../../../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
const directoryForExtensionDataInjectable = getInjectable({
instantiate: (di) =>
path.join(di.inject(directoryForUserDataInjectable), "extension_data"),
lifecycle: lifecycleEnum.singleton,
});
export default directoryForExtensionDataInjectable;
@@ -0,0 +1,36 @@
/**
* 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 { FileSystemProvisionerStore } from "./file-system-provisioner-store";
import directoryForExtensionDataInjectable from "./directory-for-extension-data/directory-for-extension-data.injectable";
const fileSystemProvisionerStoreInjectable = getInjectable({
instantiate: (di) =>
FileSystemProvisionerStore.createInstance({
directoryForExtensionData: di.inject(
directoryForExtensionDataInjectable,
),
}),
lifecycle: lifecycleEnum.singleton,
});
export default fileSystemProvisionerStoreInjectable;
@@ -0,0 +1,86 @@
/**
* 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 { randomBytes } from "crypto";
import { SHA256 } from "crypto-js";
import fse from "fs-extra";
import { action, makeObservable, observable } from "mobx";
import path from "path";
import { BaseStore } from "../../../../common/base-store";
import type { LensExtensionId } from "../../../lens-extension";
import { toJS } from "../../../../common/utils";
interface FSProvisionModel {
extensions: Record<string, string>; // extension names to paths
}
interface Dependencies {
directoryForExtensionData: string
}
export class FileSystemProvisionerStore extends BaseStore<FSProvisionModel> {
readonly displayName = "FilesystemProvisionerStore";
registeredExtensions = observable.map<LensExtensionId, string>();
constructor(private dependencies: Dependencies) {
super({
configName: "lens-filesystem-provisioner-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
});
makeObservable(this);
this.load();
}
/**
* This function retrieves the saved path to the folder which the extension
* can saves files to. If the folder is not present then it is created.
* @param extensionName the name of the extension requesting the path
* @returns path to the folder that the extension can safely write files to.
*/
async requestDirectory(extensionName: string): Promise<string> {
if (!this.registeredExtensions.has(extensionName)) {
const salt = randomBytes(32).toString("hex");
const hashedName = SHA256(`${extensionName}/${salt}`).toString();
const dirPath = path.resolve(this.dependencies.directoryForExtensionData, hashedName);
this.registeredExtensions.set(extensionName, dirPath);
}
const dirPath = this.registeredExtensions.get(extensionName);
await fse.ensureDir(dirPath);
return dirPath;
}
@action
protected fromStore({ extensions }: FSProvisionModel = { extensions: {}}): void {
this.registeredExtensions.merge(extensions);
}
toJSON(): FSProvisionModel {
return toJS({
extensions: Object.fromEntries(this.registeredExtensions),
});
}
}
@@ -21,11 +21,14 @@
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { ExtensionLoader } from "./extension-loader";
import updateExtensionsStateInjectable from "./update-extensions-state/update-extensions-state.injectable";
import createExtensionInstanceInjectable
from "./create-extension-instance/create-extension-instance.injectable";
const extensionLoaderInjectable = getInjectable({
instantiate: (di) =>
new ExtensionLoader({
updateExtensionsState: di.inject(updateExtensionsStateInjectable),
createExtensionInstance: di.inject(createExtensionInstanceInjectable),
}),
lifecycle: lifecycleEnum.singleton,
@@ -24,7 +24,6 @@ import { EventEmitter } from "events";
import { isEqual } from "lodash";
import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx";
import path from "path";
import { AppPaths } from "../../common/app-paths";
import { broadcastMessage, ipcMainOn, ipcRendererOn, requestMain, ipcMainHandle } from "../../common/ipc";
import { Disposer, toJS } from "../../common/utils";
import logger from "../../main/logger";
@@ -35,14 +34,16 @@ import type { LensRendererExtension } from "../lens-renderer-extension";
import * as registries from "../registries";
import type { LensExtensionState } from "../extensions-store/extensions-store";
export function extensionPackagesRoot() {
return path.join(AppPaths.get("userData"));
}
const logModule = "[EXTENSIONS-LOADER]";
interface Dependencies {
updateExtensionsState: (extensionsState: Record<LensExtensionId, LensExtensionState>) => void
createExtensionInstance: (ExtensionClass: LensExtensionConstructor, extension: InstalledExtension) => LensExtension,
}
export interface ExtensionLoading {
isBundled: boolean,
loaded: Promise<void>
}
/**
@@ -81,6 +82,7 @@ export class ExtensionLoader {
constructor(protected dependencies : Dependencies) {
makeObservable(this);
observe(this.instances, change => {
switch (change.type) {
case "add":
@@ -260,7 +262,7 @@ export class ExtensionLoader {
this.autoInitExtensions(() => Promise.resolve([]));
}
loadOnClusterManagerRenderer() {
loadOnClusterManagerRenderer = () => {
logger.debug(`${logModule}: load on main renderer (cluster manager)`);
return this.autoInitExtensions(async (extension: LensRendererExtension) => {
@@ -286,9 +288,9 @@ export class ExtensionLoader {
return removeItems;
});
}
};
loadOnClusterRenderer(entity: KubernetesCluster) {
loadOnClusterRenderer = (entity: KubernetesCluster) => {
logger.debug(`${logModule}: load on cluster renderer (dashboard)`);
this.autoInitExtensions(async (extension: LensRendererExtension) => {
@@ -316,12 +318,12 @@ export class ExtensionLoader {
return removeItems;
});
}
};
protected autoInitExtensions(register: (ext: LensExtension) => Promise<Disposer[]>) {
const loadingExtensions: { isBundled: boolean, loaded: Promise<void> }[] = [];
const loadingExtensions: ExtensionLoading[] = [];
reaction(() => this.toJSON(), installedExtensions => {
reaction(() => this.toJSON(), async installedExtensions => {
for (const [extId, extension] of installedExtensions) {
const alreadyInit = this.instances.has(extId) || this.nonInstancesByName.has(extension.manifest.name);
@@ -334,7 +336,10 @@ export class ExtensionLoader {
continue;
}
const instance = new LensExtensionClass(extension);
const instance = this.dependencies.createExtensionInstance(
LensExtensionClass,
extension,
);
const loaded = instance.enable(register).catch((err) => {
logger.error(`${logModule}: failed to enable`, { ext: extension, err });
@@ -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 directoryForUserDataInjectable
from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
const extensionPackagesRootInjectable = getInjectable({
instantiate: (di) => di.inject(directoryForUserDataInjectable),
lifecycle: lifecycleEnum.singleton,
});
export default extensionPackagesRootInjectable;
@@ -59,9 +59,9 @@ export class ExtensionsStore extends BaseStore<LensExtensionsStoreModel> {
}
@action
mergeState(extensionsState: Record<LensExtensionId, LensExtensionState>) {
mergeState = (extensionsState: Record<LensExtensionId, LensExtensionState>) => {
this.state.merge(extensionsState);
}
};
@action
protected fromStore({ extensions }: LensExtensionsStoreModel) {
@@ -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 type { FileSystemProvisionerStore } from "./extension-loader/create-extension-instance/file-system-provisioner-store/file-system-provisioner-store";
// This symbol encapsulates setting of dependencies to only happen locally in Lens Core
// and not by e.g. authors of extensions
export const setLensExtensionDependencies = Symbol("set-lens-extension-dependencies");
export interface LensExtensionDependencies {
fileSystemProvisionerStore: FileSystemProvisionerStore
}
+11 -2
View File
@@ -21,11 +21,14 @@
import type { InstalledExtension } from "./extension-discovery/extension-discovery";
import { action, observable, makeObservable, computed } from "mobx";
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
import logger from "../main/logger";
import type { ProtocolHandlerRegistration } from "./registries";
import type { PackageJson } from "type-fest";
import { Disposer, disposer } from "../common/utils";
import {
LensExtensionDependencies,
setLensExtensionDependencies,
} from "./lens-extension-set-dependencies";
export type LensExtensionId = string; // path to manifest (package.json)
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
@@ -75,6 +78,12 @@ export class LensExtension {
return this.manifest.description;
}
private dependencies: LensExtensionDependencies;
[setLensExtensionDependencies] = (dependencies: LensExtensionDependencies) => {
this.dependencies = dependencies;
};
/**
* getExtensionFileFolder returns the path to an already created folder. This
* folder is for the sole use of this extension.
@@ -83,7 +92,7 @@ export class LensExtension {
* folder name.
*/
async getExtensionFileFolder(): Promise<string> {
return FilesystemProvisionerStore.getInstance().requestDirectory(this.id);
return this.dependencies.fileSystemProvisionerStore.requestDirectory(this.id);
}
@action
@@ -26,10 +26,10 @@ import React from "react";
import fse from "fs-extra";
import { Console } from "console";
import { stderr, stdout } from "process";
import { TerminalStore } from "../../../renderer/components/dock/terminal.store";
import { ThemeStore } from "../../../renderer/theme.store";
import { UserStore } from "../../../common/user-store";
import { AppPaths } from "../../../common/app-paths";
import { getDisForUnitTesting } from "../../../test-utils/get-dis-for-unit-testing";
import mockFs from "mock-fs";
jest.mock("electron", () => ({
app: {
@@ -47,14 +47,18 @@ jest.mock("electron", () => ({
},
}));
AppPaths.init();
console = new Console(stdout, stderr);
let ext: LensExtension = null;
describe("page registry tests", () => {
beforeEach(async () => {
const dis = getDisForUnitTesting({ doGeneralOverrides: true });
mockFs();
await dis.runSetups();
ext = new LensExtension({
manifest: {
name: "foo-bar",
@@ -69,7 +73,6 @@ describe("page registry tests", () => {
});
UserStore.createInstance();
ThemeStore.createInstance();
TerminalStore.createInstance();
ClusterPageRegistry.createInstance();
GlobalPageRegistry.createInstance().add({
id: "page-with-params",
@@ -105,10 +108,10 @@ describe("page registry tests", () => {
afterEach(() => {
GlobalPageRegistry.resetInstance();
ClusterPageRegistry.resetInstance();
TerminalStore.resetInstance();
ThemeStore.resetInstance();
UserStore.resetInstance();
fse.remove("tmp");
mockFs.restore();
});
describe("getPageUrl", () => {
+9 -2
View File
@@ -71,5 +71,12 @@ export * from "../../renderer/components/+events/kube-event-details";
// specific exports
export * from "../../renderer/components/status-brick";
export { terminalStore, createTerminalTab, TerminalStore } from "../../renderer/components/dock/terminal.store";
export { logTabStore } from "../../renderer/components/dock/log-tab.store";
// Mikko
// export { terminalStore, TerminalStore } from "../../renderer/components/dock/terminal-store/terminal.store";
//
// // Mikko
// export { createTerminalTab } from "../../renderer/components/dock/terminal-store/terminal.store";
//
// // Mikko
// export { logTabStore } from "../../renderer/components/dock/log-tab-store/log-tab.store";
+1 -1
View File
@@ -86,7 +86,7 @@ export type { NetworkPolicyStore } from "../../renderer/components/+network-poli
export type { PersistentVolumesStore } from "../../renderer/components/+storage-volumes/volumes.store";
export type { VolumeClaimStore } from "../../renderer/components/+storage-volume-claims/volume-claim.store";
export type { StorageClassStore } from "../../renderer/components/+storage-classes/storage-class.store";
export type { NamespaceStore } from "../../renderer/components/+namespaces/namespace.store";
export type { NamespaceStore } from "../../renderer/components/+namespaces/namespace-store/namespace.store";
export type { ServiceAccountsStore } from "../../renderer/components/+user-management/+service-accounts/store";
export type { RolesStore } from "../../renderer/components/+user-management/+roles/store";
export type { RoleBindingsStore } from "../../renderer/components/+user-management/+role-bindings/store";