mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Making apiBase injectable (#6022)
* Making apiBase injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert all of Helm functions to be DI Signed-off-by: Sebastian Malton <sebastian@malton.name> * Make PortForward's use of apiBase fully injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert all metric requests to be injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Replace resource applier with injectables Signed-off-by: Sebastian Malton <sebastian@malton.name> * Switch KubeJsonApi.forCluster to be injectable but do not use Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert the rest of shell sessions to be DI-ed - This is a prerequesit for using the new createKubeJsonApiForClusterInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Use new createKubeJsonApiForClusterInjectable for openNodeShellSession Signed-off-by: Sebastian Malton <sebastian@malton.name> * Make KubeconfigDialog injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove jest-fetch-mock and make fetch injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests with new global override Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add new injectable for create KubeJsonApi and JsonApi instances Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix showing-details-for-helm-release behavioural tests - Remove HelmChartStore in favour of all injectables - Create a model for UpgradeChartDockTab Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix show details and updating helm releases tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix residual typing issues related to metrics Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix crash on load due to circular dependency Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix create resource tab not working Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove legacy apiBase global Signed-off-by: Sebastian Malton <sebastian@malton.name> * Introduce and use isDebuggingInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Introduce and use windowLocationInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove global legacy apiKube Signed-off-by: Sebastian Malton <sebastian@malton.name> * Improve injectable filenames compared to the injectables inside Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove modifying input in requestActivePortForwardInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Introduce and use get(Milli)SecondsFromUnixEpochInjectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Switch to non-reactive way of gettting possible helm release versions Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix typo Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix bug in KubeApi constructor Signed-off-by: Sebastian Malton <sebastian@malton.name> * Convert all KubeApi related tests to use asyncFn Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix unit tests after introducing new injectables that have side effects Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix bad rebase causing tests to fail Signed-off-by: Sebastian Malton <sebastian@malton.name> * Improve expects for multiple field values Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix crash will looking up api refs Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix breaking change on KubeApi.list Signed-off-by: Sebastian Malton <sebastian@malton.name> * Better fix for formatting urls Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove injectable for time since we should just use useMockTime Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add happy path behavioural tests for upgrade chart tab Signed-off-by: Sebastian Malton <sebastian@malton.name> * Remove debug message Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix showing-details-for-helm-release tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix installing-helm-chart-from-new-tab tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests relating to hosted cluster id Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots to recent changes in master Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> * Reupdated upgrade chart new tab test snapshots Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix flakiness in unit test when using <Animated> Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix flakiness and improve tests for DeleteClusterDialog Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix kubeconfig-sync tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix <Extensions> tests by removing mockFs and making everything injectable Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix build issues Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix getElectronAppPathInjectable override not returning absolute paths - Also fixes the listing-active-helm-repos-in-prefs tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Replace all uses of getAbsolutePath with joinPaths as it is more correct and less confusing Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix opening application window tests by making override properly absolute Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots relating no longer using getAbsolutePath Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix and add behavioural tests for RenderDelay Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix extension discovery tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix test flakiness because of path side effects, propagate uses to as many places Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix extension-discovery tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Add global override to fix some tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Rewrite and fix implementation of KubeconfigManager and its tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests by global override pathExists Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix unit tests failing on windows by using injectable verions of path functions Signed-off-by: Sebastian Malton <sebastian@malton.name> * Attempt to fix test timeout by using runInAction Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots after rebase Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots after rebase Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix tests after rebase Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix setupIpcMainHandlers usage Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshots Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Sebastian Malton <sebastian@malton.name> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com> Co-authored-by: Iku-turso <mikko.aspiala@gmail.com>
This commit is contained in:
367 files changed
+16701
-5302
No files matched your search
@@ -9,18 +9,28 @@
|
||||
|
||||
export { ResourceStack } from "../../common/k8s/resource-stack";
|
||||
import apiManagerInjectable from "../../common/k8s-api/api-manager/manager.injectable";
|
||||
import createKubeApiForClusterInjectable from "../../common/k8s-api/create-kube-api-for-cluster.injectable";
|
||||
import createKubeApiForRemoteClusterInjectable from "../../common/k8s-api/create-kube-api-for-remote-cluster.injectable";
|
||||
import { asLegacyGlobalFunctionForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-function-for-extension-api";
|
||||
import { asLegacyGlobalForExtensionApi } from "../as-legacy-globals-for-extension-api/as-legacy-global-object-for-extension-api";
|
||||
|
||||
export const apiManager = asLegacyGlobalForExtensionApi(apiManagerInjectable);
|
||||
export const forCluster = asLegacyGlobalFunctionForExtensionApi(createKubeApiForClusterInjectable);
|
||||
export const forRemoteCluster = asLegacyGlobalFunctionForExtensionApi(createKubeApiForRemoteClusterInjectable);
|
||||
|
||||
export {
|
||||
KubeApi,
|
||||
forCluster,
|
||||
forRemoteCluster,
|
||||
type ILocalKubeApiConfig,
|
||||
type IRemoteKubeApiConfig,
|
||||
type IKubeApiCluster,
|
||||
} from "../../common/k8s-api/kube-api";
|
||||
export { KubeApi } from "../../common/k8s-api/kube-api";
|
||||
|
||||
/**
|
||||
* @deprecated This type is unused
|
||||
*/
|
||||
export interface IKubeApiCluster {
|
||||
metadata: {
|
||||
uid: string;
|
||||
};
|
||||
}
|
||||
|
||||
export type { CreateKubeApiForRemoteClusterConfig as IRemoteKubeApiConfig } from "../../common/k8s-api/create-kube-api-for-remote-cluster.injectable";
|
||||
export type { CreateKubeApiForLocalClusterConfig as ILocalKubeApiConfig } from "../../common/k8s-api/create-kube-api-for-cluster.injectable";
|
||||
|
||||
export {
|
||||
KubeObject,
|
||||
|
||||
@@ -16,6 +16,19 @@ import readJsonFileInjectable from "../../common/fs/read-json-file.injectable";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
import pathExistsInjectable from "../../common/fs/path-exists.injectable";
|
||||
import watchInjectable from "../../common/fs/watch/watch.injectable";
|
||||
import accessPathInjectable from "../../common/fs/access-path.injectable";
|
||||
import copyInjectable from "../../common/fs/copy.injectable";
|
||||
import ensureDirInjectable from "../../common/fs/ensure-dir.injectable";
|
||||
import isProductionInjectable from "../../common/vars/is-production.injectable";
|
||||
import lstatInjectable from "../../common/fs/lstat.injectable";
|
||||
import readDirectoryInjectable from "../../common/fs/read-directory.injectable";
|
||||
import fileSystemSeparatorInjectable from "../../common/path/separator.injectable";
|
||||
import getBasenameOfPathInjectable from "../../common/path/get-basename.injectable";
|
||||
import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable";
|
||||
import getRelativePathInjectable from "../../common/path/get-relative-path.injectable";
|
||||
import joinPathsInjectable from "../../common/path/join-paths.injectable";
|
||||
import removePathInjectable from "../../common/fs/remove-path.injectable";
|
||||
import homeDirectoryPathInjectable from "../../common/os/home-directory-path.injectable";
|
||||
|
||||
const extensionDiscoveryInjectable = getInjectable({
|
||||
id: "extension-discovery",
|
||||
@@ -33,6 +46,19 @@ const extensionDiscoveryInjectable = getInjectable({
|
||||
pathExists: di.inject(pathExistsInjectable),
|
||||
watch: di.inject(watchInjectable),
|
||||
logger: di.inject(loggerInjectable),
|
||||
accessPath: di.inject(accessPathInjectable),
|
||||
copy: di.inject(copyInjectable),
|
||||
removePath: di.inject(removePathInjectable),
|
||||
ensureDirectory: di.inject(ensureDirInjectable),
|
||||
isProduction: di.inject(isProductionInjectable),
|
||||
lstat: di.inject(lstatInjectable),
|
||||
readDirectory: di.inject(readDirectoryInjectable),
|
||||
fileSystemSeparator: di.inject(fileSystemSeparatorInjectable),
|
||||
getBasenameOfPath: di.inject(getBasenameOfPathInjectable),
|
||||
getDirnameOfPath: di.inject(getDirnameOfPathInjectable),
|
||||
getRelativePath: di.inject(getRelativePathInjectable),
|
||||
joinPaths: di.inject(joinPathsInjectable),
|
||||
homeDirectoryPath: di.inject(homeDirectoryPathInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -4,37 +4,40 @@
|
||||
*/
|
||||
|
||||
import type { FSWatcher } from "chokidar";
|
||||
import path from "path";
|
||||
import os from "os";
|
||||
import { Console } from "console";
|
||||
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
|
||||
import extensionDiscoveryInjectable from "../extension-discovery/extension-discovery.injectable";
|
||||
import type { ExtensionDiscovery } from "../extension-discovery/extension-discovery";
|
||||
import installExtensionInjectable from "../extension-installer/install-extension/install-extension.injectable";
|
||||
import directoryForUserDataInjectable from "../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import mockFs from "mock-fs";
|
||||
import { delay } from "../../renderer/utils";
|
||||
import { observable, when } from "mobx";
|
||||
import { observable, runInAction, when } from "mobx";
|
||||
import readJsonFileInjectable from "../../common/fs/read-json-file.injectable";
|
||||
import pathExistsInjectable from "../../common/fs/path-exists.injectable";
|
||||
import watchInjectable from "../../common/fs/watch/watch.injectable";
|
||||
import extensionApiVersionInjectable from "../../common/vars/extension-api-version.injectable";
|
||||
|
||||
console = new Console(process.stdout, process.stderr); // fix mockFS
|
||||
import removePathInjectable from "../../common/fs/remove-path.injectable";
|
||||
import type { JoinPaths } from "../../common/path/join-paths.injectable";
|
||||
import joinPathsInjectable from "../../common/path/join-paths.injectable";
|
||||
import homeDirectoryPathInjectable from "../../common/os/home-directory-path.injectable";
|
||||
|
||||
describe("ExtensionDiscovery", () => {
|
||||
let extensionDiscovery: ExtensionDiscovery;
|
||||
let readJsonFileMock: jest.Mock;
|
||||
let pathExistsMock: jest.Mock;
|
||||
let watchMock: jest.Mock;
|
||||
let joinPaths: JoinPaths;
|
||||
let homeDirectoryPath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
|
||||
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
|
||||
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
|
||||
di.override(installExtensionInjectable, () => () => Promise.resolve());
|
||||
di.override(extensionApiVersionInjectable, () => "5.0.0");
|
||||
|
||||
joinPaths = di.inject(joinPathsInjectable);
|
||||
homeDirectoryPath = di.inject(homeDirectoryPathInjectable);
|
||||
|
||||
readJsonFileMock = jest.fn();
|
||||
di.override(readJsonFileInjectable, () => readJsonFileMock);
|
||||
|
||||
@@ -44,21 +47,17 @@ describe("ExtensionDiscovery", () => {
|
||||
watchMock = jest.fn();
|
||||
di.override(watchInjectable, () => watchMock);
|
||||
|
||||
mockFs();
|
||||
di.override(removePathInjectable, () => async () => {}); // allow deleting files for now
|
||||
|
||||
extensionDiscovery = di.inject(extensionDiscoveryInjectable);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockFs.restore();
|
||||
});
|
||||
|
||||
it("emits add for added extension", async () => {
|
||||
const letTestFinish = observable.box(false);
|
||||
let addHandler!: (filePath: string) => void;
|
||||
|
||||
readJsonFileMock.mockImplementation((p) => {
|
||||
expect(p).toBe(path.join(os.homedir(), ".k8slens/extensions/my-extension/package.json"));
|
||||
expect(p).toBe(joinPaths(homeDirectoryPath, ".k8slens/extensions/my-extension/package.json"));
|
||||
|
||||
return {
|
||||
name: "my-extension",
|
||||
@@ -89,7 +88,7 @@ describe("ExtensionDiscovery", () => {
|
||||
extensionDiscovery.events.on("add", extension => {
|
||||
expect(extension).toEqual({
|
||||
absolutePath: expect.any(String),
|
||||
id: path.normalize("some-directory-for-user-data/node_modules/my-extension/package.json"),
|
||||
id: "/some-directory-for-user-data/node_modules/my-extension/package.json",
|
||||
isBundled: false,
|
||||
isEnabled: false,
|
||||
isCompatible: true,
|
||||
@@ -100,12 +99,12 @@ describe("ExtensionDiscovery", () => {
|
||||
lens: "5.0.0",
|
||||
},
|
||||
},
|
||||
manifestPath: path.normalize("some-directory-for-user-data/node_modules/my-extension/package.json"),
|
||||
manifestPath: "/some-directory-for-user-data/node_modules/my-extension/package.json",
|
||||
});
|
||||
letTestFinish.set(true);
|
||||
runInAction(() => letTestFinish.set(true));
|
||||
});
|
||||
|
||||
addHandler(path.join(extensionDiscovery.localFolderPath, "/my-extension/package.json"));
|
||||
addHandler(joinPaths(extensionDiscovery.localFolderPath, "/my-extension/package.json"));
|
||||
await when(() => letTestFinish.get());
|
||||
});
|
||||
|
||||
@@ -133,7 +132,7 @@ describe("ExtensionDiscovery", () => {
|
||||
|
||||
extensionDiscovery.events.on("add", onAdd);
|
||||
|
||||
addHandler(path.join(extensionDiscovery.localFolderPath, "/my-extension/node_modules/dep/package.json"));
|
||||
addHandler(joinPaths(extensionDiscovery.localFolderPath, "/my-extension/node_modules/dep/package.json"));
|
||||
|
||||
await delay(10);
|
||||
|
||||
|
||||
@@ -5,16 +5,12 @@
|
||||
|
||||
import { ipcRenderer } from "electron";
|
||||
import { EventEmitter } from "events";
|
||||
import fse from "fs-extra";
|
||||
import { makeObservable, observable, reaction, when } from "mobx";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import { broadcastMessage, ipcMainHandle, ipcRendererOn } from "../../common/ipc";
|
||||
import { isErrnoException, toJS } from "../../common/utils";
|
||||
import type { ExtensionsStore } from "../extensions-store/extensions-store";
|
||||
import type { ExtensionLoader } from "../extension-loader";
|
||||
import type { LensExtensionId, LensExtensionManifest } from "../lens-extension";
|
||||
import { isProduction } from "../../common/vars";
|
||||
import type { ExtensionInstallationStateStore } from "../extension-installation-state-store/extension-installation-state-store";
|
||||
import type { PackageJson } from "type-fest";
|
||||
import { extensionDiscoveryStateChannel } from "../../common/ipc/extension-handling";
|
||||
@@ -23,20 +19,46 @@ import type { ReadJson } from "../../common/fs/read-json-file.injectable";
|
||||
import type { Logger } from "../../common/logger";
|
||||
import type { PathExists } from "../../common/fs/path-exists.injectable";
|
||||
import type { Watch } from "../../common/fs/watch/watch.injectable";
|
||||
import type { Stats } from "fs";
|
||||
import { constants } from "fs";
|
||||
import type { LStat } from "../../common/fs/lstat.injectable";
|
||||
import type { ReadDirectory } from "../../common/fs/read-directory.injectable";
|
||||
import type { EnsureDirectory } from "../../common/fs/ensure-dir.injectable";
|
||||
import type { AccessPath } from "../../common/fs/access-path.injectable";
|
||||
import type { Copy } from "../../common/fs/copy.injectable";
|
||||
import type { JoinPaths } from "../../common/path/join-paths.injectable";
|
||||
import type { GetBasenameOfPath } from "../../common/path/get-basename.injectable";
|
||||
import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable";
|
||||
import type { GetRelativePath } from "../../common/path/get-relative-path.injectable";
|
||||
import type { RemovePath } from "../../common/fs/remove-path.injectable";
|
||||
import type TypedEventEmitter from "typed-emitter";
|
||||
|
||||
interface Dependencies {
|
||||
extensionLoader: ExtensionLoader;
|
||||
extensionsStore: ExtensionsStore;
|
||||
extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
readonly extensionLoader: ExtensionLoader;
|
||||
readonly extensionsStore: ExtensionsStore;
|
||||
readonly extensionInstallationStateStore: ExtensionInstallationStateStore;
|
||||
readonly extensionPackageRootDirectory: string;
|
||||
readonly staticFilesDirectory: string;
|
||||
readonly logger: Logger;
|
||||
readonly isProduction: boolean;
|
||||
readonly fileSystemSeparator: string;
|
||||
readonly homeDirectoryPath: string;
|
||||
isCompatibleExtension: (manifest: LensExtensionManifest) => boolean;
|
||||
installExtension: (name: string) => Promise<void>;
|
||||
installExtensions: (packageJsonPath: string, packagesJson: PackageJson) => Promise<void>;
|
||||
extensionPackageRootDirectory: string;
|
||||
staticFilesDirectory: string;
|
||||
readJsonFile: ReadJson;
|
||||
pathExists: PathExists;
|
||||
removePath: RemovePath;
|
||||
lstat: LStat;
|
||||
watch: Watch;
|
||||
logger: Logger;
|
||||
readDirectory: ReadDirectory;
|
||||
ensureDirectory: EnsureDirectory;
|
||||
accessPath: AccessPath;
|
||||
copy: Copy;
|
||||
joinPaths: JoinPaths;
|
||||
getBasenameOfPath: GetBasenameOfPath;
|
||||
getDirnameOfPath: GetDirnameOfPath;
|
||||
getRelativePath: GetRelativePath;
|
||||
}
|
||||
|
||||
export interface InstalledExtension {
|
||||
@@ -67,12 +89,17 @@ interface ExtensionDiscoveryChannelMessage {
|
||||
* Returns true if the lstat is for a directory-like file (e.g. isDirectory or symbolic link)
|
||||
* @param lstat the stats to compare
|
||||
*/
|
||||
const isDirectoryLike = (lstat: fse.Stats) => lstat.isDirectory() || lstat.isSymbolicLink();
|
||||
const isDirectoryLike = (lstat: Stats) => lstat.isDirectory() || lstat.isSymbolicLink();
|
||||
|
||||
interface LoadFromFolderOptions {
|
||||
isBundled?: boolean;
|
||||
}
|
||||
|
||||
interface ExtensionDiscoveryEvents {
|
||||
add: (ext: InstalledExtension) => void;
|
||||
remove: (extId: LensExtensionId) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Discovers installed bundled and local extensions from the filesystem.
|
||||
* Also watches for added and removed local extensions by watching the directory.
|
||||
@@ -96,30 +123,30 @@ export class ExtensionDiscovery {
|
||||
return when(() => this.isLoaded);
|
||||
}
|
||||
|
||||
public events = new EventEmitter();
|
||||
public readonly events: TypedEventEmitter<ExtensionDiscoveryEvents> = new EventEmitter();
|
||||
|
||||
constructor(protected readonly dependencies: Dependencies) {
|
||||
makeObservable(this);
|
||||
}
|
||||
|
||||
get localFolderPath(): string {
|
||||
return path.join(os.homedir(), ".k8slens", "extensions");
|
||||
return this.dependencies.joinPaths(this.dependencies.homeDirectoryPath, ".k8slens", "extensions");
|
||||
}
|
||||
|
||||
get packageJsonPath(): string {
|
||||
return path.join(this.dependencies.extensionPackageRootDirectory, manifestFilename);
|
||||
return this.dependencies.joinPaths(this.dependencies.extensionPackageRootDirectory, manifestFilename);
|
||||
}
|
||||
|
||||
get inTreeTargetPath(): string {
|
||||
return path.join(this.dependencies.extensionPackageRootDirectory, "extensions");
|
||||
return this.dependencies.joinPaths(this.dependencies.extensionPackageRootDirectory, "extensions");
|
||||
}
|
||||
|
||||
get inTreeFolderPath(): string {
|
||||
return path.resolve(this.dependencies.staticFilesDirectory, "../extensions");
|
||||
return this.dependencies.joinPaths(this.dependencies.staticFilesDirectory, "../extensions");
|
||||
}
|
||||
|
||||
get nodeModulesPath(): string {
|
||||
return path.join(this.dependencies.extensionPackageRootDirectory, "node_modules");
|
||||
return this.dependencies.joinPaths(this.dependencies.extensionPackageRootDirectory, "node_modules");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,24 +211,24 @@ export class ExtensionDiscovery {
|
||||
|
||||
handleWatchFileAdd = async (manifestPath: string): Promise<void> => {
|
||||
// e.g. "foo/package.json"
|
||||
const relativePath = path.relative(this.localFolderPath, manifestPath);
|
||||
const relativePath = this.dependencies.getRelativePath(this.localFolderPath, manifestPath);
|
||||
|
||||
// Converts "foo/package.json" to ["foo", "package.json"], where length of 2 implies
|
||||
// that the added file is in a folder under local folder path.
|
||||
// This safeguards against a file watch being triggered under a sub-directory which is not an extension.
|
||||
const isUnderLocalFolderPath = relativePath.split(path.sep).length === 2;
|
||||
const isUnderLocalFolderPath = relativePath.split(this.dependencies.fileSystemSeparator).length === 2;
|
||||
|
||||
if (path.basename(manifestPath) === manifestFilename && isUnderLocalFolderPath) {
|
||||
if (this.dependencies.getBasenameOfPath(manifestPath) === manifestFilename && isUnderLocalFolderPath) {
|
||||
try {
|
||||
this.dependencies.extensionInstallationStateStore.setInstallingFromMain(manifestPath);
|
||||
const absPath = path.dirname(manifestPath);
|
||||
const absPath = this.dependencies.getDirnameOfPath(manifestPath);
|
||||
|
||||
// this.loadExtensionFromPath updates this.packagesJson
|
||||
const extension = await this.loadExtensionFromFolder(absPath);
|
||||
|
||||
if (extension) {
|
||||
// Remove a broken symlink left by a previous installation if it exists.
|
||||
await fse.remove(extension.manifestPath);
|
||||
await this.dependencies.removePath(extension.manifestPath);
|
||||
|
||||
// Install dependencies for the new extension
|
||||
await this.dependencies.installExtension(extension.absolutePath);
|
||||
@@ -226,8 +253,8 @@ export class ExtensionDiscovery {
|
||||
handleWatchUnlinkEvent = async (filePath: string): Promise<void> => {
|
||||
// Check that the removed path is directly under this.localFolderPath
|
||||
// Note that the watcher can create unlink events for subdirectories of the extension
|
||||
const extensionFolderName = path.basename(filePath);
|
||||
const expectedPath = path.relative(this.localFolderPath, filePath);
|
||||
const extensionFolderName = this.dependencies.getBasenameOfPath(filePath);
|
||||
const expectedPath = this.dependencies.getRelativePath(this.localFolderPath, filePath);
|
||||
|
||||
if (expectedPath !== extensionFolderName) {
|
||||
return;
|
||||
@@ -264,7 +291,7 @@ export class ExtensionDiscovery {
|
||||
* @param name e.g. "@mirantis/lens-extension-cc"
|
||||
*/
|
||||
removeSymlinkByPackageName(name: string): Promise<void> {
|
||||
return fse.remove(this.getInstalledPath(name));
|
||||
return this.dependencies.removePath(this.getInstalledPath(name));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -286,7 +313,7 @@ export class ExtensionDiscovery {
|
||||
await this.removeSymlinkByPackageName(manifest.name);
|
||||
|
||||
// fs.remove does nothing if the path doesn't exist anymore
|
||||
await fse.remove(absolutePath);
|
||||
await this.dependencies.removePath(absolutePath);
|
||||
}
|
||||
|
||||
async load(): Promise<Map<LensExtensionId, InstalledExtension>> {
|
||||
@@ -301,34 +328,29 @@ export class ExtensionDiscovery {
|
||||
`${logModule} loading extensions from ${this.dependencies.extensionPackageRootDirectory}`,
|
||||
);
|
||||
|
||||
// fs.remove won't throw if path is missing
|
||||
await fse.remove(path.join(this.dependencies.extensionPackageRootDirectory, "package-lock.json"));
|
||||
await this.dependencies.removePath(this.dependencies.joinPaths(this.dependencies.extensionPackageRootDirectory, "package-lock.json"));
|
||||
|
||||
try {
|
||||
// Verify write access to static/extensions, which is needed for symlinking
|
||||
await fse.access(this.inTreeFolderPath, fse.constants.W_OK);
|
||||
const canWriteToInTreeFolder = await this.dependencies.accessPath(this.inTreeFolderPath, constants.W_OK);
|
||||
|
||||
if (canWriteToInTreeFolder) {
|
||||
// Set bundled folder path to static/extensions
|
||||
this.bundledFolderPath = this.inTreeFolderPath;
|
||||
} catch {
|
||||
// If there is error accessing static/extensions, we need to copy in-tree extensions so that we can symlink them properly on "npm install".
|
||||
// The error can happen if there is read-only rights to static/extensions, which would fail symlinking.
|
||||
|
||||
} else {
|
||||
// Remove e.g. /Users/<username>/Library/Application Support/LensDev/extensions
|
||||
await fse.remove(this.inTreeTargetPath);
|
||||
await this.dependencies.removePath(this.inTreeTargetPath);
|
||||
|
||||
// Create folder e.g. /Users/<username>/Library/Application Support/LensDev/extensions
|
||||
await fse.ensureDir(this.inTreeTargetPath);
|
||||
await this.dependencies.ensureDirectory(this.inTreeTargetPath);
|
||||
|
||||
// Copy static/extensions to e.g. /Users/<username>/Library/Application Support/LensDev/extensions
|
||||
await fse.copy(this.inTreeFolderPath, this.inTreeTargetPath);
|
||||
await this.dependencies.copy(this.inTreeFolderPath, this.inTreeTargetPath);
|
||||
|
||||
// Set bundled folder path to e.g. /Users/<username>/Library/Application Support/LensDev/extensions
|
||||
this.bundledFolderPath = this.inTreeTargetPath;
|
||||
}
|
||||
|
||||
await fse.ensureDir(this.nodeModulesPath);
|
||||
await fse.ensureDir(this.localFolderPath);
|
||||
await this.dependencies.ensureDirectory(this.nodeModulesPath);
|
||||
await this.dependencies.ensureDirectory(this.localFolderPath);
|
||||
|
||||
const extensions = await this.ensureExtensions();
|
||||
|
||||
@@ -342,7 +364,7 @@ export class ExtensionDiscovery {
|
||||
* e.g. "/Users/<username>/Library/Application Support/Lens/node_modules/@publisher/extension"
|
||||
*/
|
||||
protected getInstalledPath(name: string): string {
|
||||
return path.join(this.nodeModulesPath, name);
|
||||
return this.dependencies.joinPaths(this.nodeModulesPath, name);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -350,7 +372,7 @@ export class ExtensionDiscovery {
|
||||
* e.g. "/Users/<username>/Library/Application Support/Lens/node_modules/@publisher/extension/package.json"
|
||||
*/
|
||||
protected getInstalledManifestPath(name: string): string {
|
||||
return path.join(this.getInstalledPath(name), manifestFilename);
|
||||
return this.dependencies.joinPaths(this.getInstalledPath(name), manifestFilename);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -362,10 +384,11 @@ export class ExtensionDiscovery {
|
||||
const manifest = await this.dependencies.readJsonFile(manifestPath) as unknown as LensExtensionManifest;
|
||||
const id = this.getInstalledManifestPath(manifest.name);
|
||||
const isEnabled = this.dependencies.extensionsStore.isEnabled({ id, isBundled });
|
||||
const extensionDir = path.dirname(manifestPath);
|
||||
const packedName = manifest.name.replaceAll("@", "").replaceAll("/", "-");
|
||||
const npmPackage = path.join(extensionDir, `${packedName}-${manifest.version}.tgz`);
|
||||
const absolutePath = (isProduction && await this.dependencies.pathExists(npmPackage)) ? npmPackage : extensionDir;
|
||||
const extensionDir = this.dependencies.getDirnameOfPath(manifestPath);
|
||||
const npmPackage = this.dependencies.joinPaths(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
||||
const absolutePath = this.dependencies.isProduction && await this.dependencies.pathExists(npmPackage)
|
||||
? npmPackage
|
||||
: extensionDir;
|
||||
const isCompatible = isBundled || this.dependencies.isCompatibleExtension(manifest);
|
||||
|
||||
return {
|
||||
@@ -416,10 +439,10 @@ export class ExtensionDiscovery {
|
||||
async loadBundledExtensions(): Promise<InstalledExtension[]> {
|
||||
const extensions: InstalledExtension[] = [];
|
||||
const folderPath = this.bundledFolderPath;
|
||||
const paths = await fse.readdir(folderPath);
|
||||
const paths = await this.dependencies.readDirectory(folderPath);
|
||||
|
||||
for (const fileName of paths) {
|
||||
const absPath = path.resolve(folderPath, fileName);
|
||||
const absPath = this.dependencies.joinPaths(folderPath, fileName);
|
||||
const extension = await this.loadExtensionFromFolder(absPath, { isBundled: true });
|
||||
|
||||
if (extension) {
|
||||
@@ -433,7 +456,7 @@ export class ExtensionDiscovery {
|
||||
|
||||
async loadFromFolder(folderPath: string, bundledExtensions: string[]): Promise<InstalledExtension[]> {
|
||||
const extensions: InstalledExtension[] = [];
|
||||
const paths = await fse.readdir(folderPath);
|
||||
const paths = await this.dependencies.readDirectory(folderPath);
|
||||
|
||||
for (const fileName of paths) {
|
||||
// do not allow to override bundled extensions
|
||||
@@ -441,17 +464,21 @@ export class ExtensionDiscovery {
|
||||
continue;
|
||||
}
|
||||
|
||||
const absPath = path.resolve(folderPath, fileName);
|
||||
const absPath = this.dependencies.joinPaths(folderPath, fileName);
|
||||
|
||||
if (!fse.existsSync(absPath)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
const lstat = await this.dependencies.lstat(absPath);
|
||||
|
||||
const lstat = await fse.lstat(absPath);
|
||||
// skip non-directories
|
||||
if (!isDirectoryLike(lstat)) {
|
||||
continue;
|
||||
}
|
||||
} catch (error) {
|
||||
if (isErrnoException(error) && error.code === "ENOENT") {
|
||||
continue;
|
||||
}
|
||||
|
||||
// skip non-directories
|
||||
if (!isDirectoryLike(lstat)) {
|
||||
continue;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const extension = await this.loadExtensionFromFolder(absPath);
|
||||
@@ -471,7 +498,7 @@ export class ExtensionDiscovery {
|
||||
* @param folderPath Folder path to extension
|
||||
*/
|
||||
async loadExtensionFromFolder(folderPath: string, { isBundled = false }: LoadFromFolderOptions = {}): Promise<InstalledExtension | null> {
|
||||
const manifestPath = path.resolve(folderPath, manifestFilename);
|
||||
const manifestPath = this.dependencies.joinPaths(folderPath, manifestFilename);
|
||||
|
||||
return this.getByManifest(manifestPath, { isBundled });
|
||||
}
|
||||
|
||||
@@ -9,6 +9,9 @@ import { createExtensionInstanceInjectionToken } from "./create-extension-instan
|
||||
import extensionInstancesInjectable from "./extension-instances.injectable";
|
||||
import type { LensExtension } from "../lens-extension";
|
||||
import extensionInjectable from "./extension/extension.injectable";
|
||||
import loggerInjectable from "../../common/logger.injectable";
|
||||
import joinPathsInjectable from "../../common/path/join-paths.injectable";
|
||||
import getDirnameOfPathInjectable from "../../common/path/get-dirname.injectable";
|
||||
|
||||
const extensionLoaderInjectable = getInjectable({
|
||||
id: "extension-loader",
|
||||
@@ -18,6 +21,9 @@ const extensionLoaderInjectable = getInjectable({
|
||||
createExtensionInstance: di.inject(createExtensionInstanceInjectionToken),
|
||||
extensionInstances: di.inject(extensionInstancesInjectable),
|
||||
getExtension: (instance: LensExtension) => di.inject(extensionInjectable, instance),
|
||||
logger: di.inject(loggerInjectable),
|
||||
joinPaths: di.inject(joinPathsInjectable),
|
||||
getDirnameOfPath: di.inject(getDirnameOfPathInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -7,11 +7,9 @@ import { ipcMain, ipcRenderer } from "electron";
|
||||
import { isEqual } from "lodash";
|
||||
import type { ObservableMap } from "mobx";
|
||||
import { action, computed, makeObservable, observable, observe, reaction, when } from "mobx";
|
||||
import path from "path";
|
||||
import { broadcastMessage, ipcMainOn, ipcRendererOn, ipcMainHandle } from "../../common/ipc";
|
||||
import type { Disposer } from "../../common/utils";
|
||||
import { isDefined, toJS } from "../../common/utils";
|
||||
import logger from "../../main/logger";
|
||||
import type { InstalledExtension } from "../extension-discovery/extension-discovery";
|
||||
import type { LensExtension, LensExtensionConstructor, LensExtensionId } from "../lens-extension";
|
||||
import type { LensRendererExtension } from "../lens-renderer-extension";
|
||||
@@ -23,14 +21,20 @@ import assert from "assert";
|
||||
import { EventEmitter } from "../../common/event-emitter";
|
||||
import type { CreateExtensionInstance } from "./create-extension-instance.token";
|
||||
import type { Extension } from "./extension/extension.injectable";
|
||||
import type { Logger } from "../../common/logger";
|
||||
import type { JoinPaths } from "../../common/path/join-paths.injectable";
|
||||
import type { GetDirnameOfPath } from "../../common/path/get-dirname.injectable";
|
||||
|
||||
const logModule = "[EXTENSIONS-LOADER]";
|
||||
|
||||
interface Dependencies {
|
||||
readonly extensionInstances: ObservableMap<LensExtensionId, LensExtension>;
|
||||
readonly logger: Logger;
|
||||
updateExtensionsState: (extensionsState: Record<LensExtensionId, LensExtensionState>) => void;
|
||||
createExtensionInstance: CreateExtensionInstance;
|
||||
readonly extensionInstances: ObservableMap<LensExtensionId, LensExtension>;
|
||||
getExtension: (instance: LensExtension) => Extension;
|
||||
joinPaths: JoinPaths;
|
||||
getDirnameOfPath: GetDirnameOfPath;
|
||||
}
|
||||
|
||||
export interface ExtensionLoading {
|
||||
@@ -159,7 +163,7 @@ export class ExtensionLoader {
|
||||
|
||||
@action
|
||||
removeInstance(lensExtensionId: LensExtensionId) {
|
||||
logger.info(`${logModule} deleting extension instance ${lensExtensionId}`);
|
||||
this.dependencies.logger.info(`${logModule} deleting extension instance ${lensExtensionId}`);
|
||||
const instance = this.dependencies.extensionInstances.get(lensExtensionId);
|
||||
|
||||
if (!instance) {
|
||||
@@ -177,7 +181,7 @@ export class ExtensionLoader {
|
||||
this.dependencies.extensionInstances.delete(lensExtensionId);
|
||||
this.nonInstancesByName.delete(instance.name);
|
||||
} catch (error) {
|
||||
logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error });
|
||||
this.dependencies.logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,7 +256,7 @@ export class ExtensionLoader {
|
||||
}
|
||||
|
||||
loadOnClusterManagerRenderer = () => {
|
||||
logger.debug(`${logModule}: load on main renderer (cluster manager)`);
|
||||
this.dependencies.logger.debug(`${logModule}: load on main renderer (cluster manager)`);
|
||||
|
||||
return this.autoInitExtensions(async (ext) => {
|
||||
const extension = ext as LensRendererExtension;
|
||||
@@ -274,7 +278,7 @@ export class ExtensionLoader {
|
||||
};
|
||||
|
||||
loadOnClusterRenderer = () => {
|
||||
logger.debug(`${logModule}: load on cluster renderer (dashboard)`);
|
||||
this.dependencies.logger.debug(`${logModule}: load on cluster renderer (dashboard)`);
|
||||
|
||||
this.autoInitExtensions(async () => []);
|
||||
};
|
||||
@@ -313,7 +317,7 @@ export class ExtensionLoader {
|
||||
activated: instance.activate(),
|
||||
};
|
||||
} catch (err) {
|
||||
logger.error(`${logModule}: error loading extension`, { ext: extension, err });
|
||||
this.dependencies.logger.error(`${logModule}: error loading extension`, { ext: extension, err });
|
||||
}
|
||||
} else if (!extension.isEnabled && alreadyInit) {
|
||||
this.removeInstance(extId);
|
||||
@@ -330,7 +334,7 @@ export class ExtensionLoader {
|
||||
extensions.map(extension =>
|
||||
// If extension activation fails, log error
|
||||
extension.activated.catch((error) => {
|
||||
logger.error(`${logModule}: activation extension error`, { ext: extension.installedExtension, error });
|
||||
this.dependencies.logger.error(`${logModule}: activation extension error`, { ext: extension.installedExtension, error });
|
||||
}),
|
||||
),
|
||||
);
|
||||
@@ -344,7 +348,7 @@ export class ExtensionLoader {
|
||||
// Return ExtensionLoading[]
|
||||
return extensions.map(extension => {
|
||||
const loaded = extension.instance.enable(register).catch((err) => {
|
||||
logger.error(`${logModule}: failed to enable`, { ext: extension, err });
|
||||
this.dependencies.logger.error(`${logModule}: failed to enable`, { ext: extension, err });
|
||||
});
|
||||
|
||||
return {
|
||||
@@ -370,14 +374,14 @@ export class ExtensionLoader {
|
||||
return null;
|
||||
}
|
||||
|
||||
const extAbsolutePath = path.resolve(path.join(path.dirname(extension.manifestPath), extRelativePath));
|
||||
const extAbsolutePath = this.dependencies.joinPaths(this.dependencies.getDirnameOfPath(extension.manifestPath), extRelativePath);
|
||||
|
||||
try {
|
||||
return __non_webpack_require__(extAbsolutePath).default;
|
||||
} catch (error) {
|
||||
const message = (error instanceof Error ? error.stack : undefined) || error;
|
||||
|
||||
logger.error(`${logModule}: can't load ${entryPointName} for "${extension.manifest.name}": ${message}`, { extension });
|
||||
this.dependencies.logger.error(`${logModule}: can't load ${entryPointName} for "${extension.manifest.name}": ${message}`, { extension });
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
+3
-3
@@ -4,16 +4,16 @@
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import getAbsolutePathInjectable from "../../../common/path/get-absolute-path.injectable";
|
||||
import joinPathsInjectable from "../../../common/path/join-paths.injectable";
|
||||
|
||||
const directoryForExtensionDataInjectable = getInjectable({
|
||||
id: "directory-for-extension-data",
|
||||
|
||||
instantiate: (di) => {
|
||||
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
|
||||
const joinPaths = di.inject(joinPathsInjectable);
|
||||
const directoryForUserData = di.inject(directoryForUserDataInjectable);
|
||||
|
||||
return getAbsolutePath(directoryForUserData, "extension_data");
|
||||
return joinPaths(directoryForUserData, "extension_data");
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user