mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Initial support for validating extension engines.lens version (#2884)
This commit is contained in:
parent
d6e72ddc1c
commit
3847a91758
@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
// App's common configuration for any process (main, renderer, build pipeline, etc.)
|
// App's common configuration for any process (main, renderer, build pipeline, etc.)
|
||||||
import path from "path";
|
import path from "path";
|
||||||
|
import { SemVer } from "semver";
|
||||||
import packageInfo from "../../package.json";
|
import packageInfo from "../../package.json";
|
||||||
import { defineGlobal } from "./utils/defineGlobal";
|
import { defineGlobal } from "./utils/defineGlobal";
|
||||||
|
|
||||||
@ -66,4 +67,6 @@ export const apiKubePrefix = "/api-kube" as string; // k8s cluster apis
|
|||||||
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues" as string;
|
export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues" as string;
|
||||||
export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI" as string;
|
export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/enQtOTc5NjAyNjYyOTk4LWU1NDQ0ZGFkOWJkNTRhYTc2YjVmZDdkM2FkNGM5MjhiYTRhMDU2NDQ1MzIyMDA4ZGZlNmExOTc0N2JmY2M3ZGI" as string;
|
||||||
export const supportUrl = "https://docs.k8slens.dev/latest/support/" as string;
|
export const supportUrl = "https://docs.k8slens.dev/latest/support/" as string;
|
||||||
|
|
||||||
|
export const appSemVer = new SemVer(packageInfo.version);
|
||||||
export const docsUrl = `https://docs.k8slens.dev/main/` as string;
|
export const docsUrl = `https://docs.k8slens.dev/main/` as string;
|
||||||
|
|||||||
@ -93,6 +93,7 @@ describe("ExtensionDiscovery", () => {
|
|||||||
id: path.normalize("node_modules/my-extension/package.json"),
|
id: path.normalize("node_modules/my-extension/package.json"),
|
||||||
isBundled: false,
|
isBundled: false,
|
||||||
isEnabled: false,
|
isEnabled: false,
|
||||||
|
isCompatible: false,
|
||||||
manifest: {
|
manifest: {
|
||||||
name: "my-extension",
|
name: "my-extension",
|
||||||
},
|
},
|
||||||
|
|||||||
@ -38,7 +38,8 @@ describe("lens extension", () => {
|
|||||||
absolutePath: "/absolute/fake/",
|
absolutePath: "/absolute/fake/",
|
||||||
manifestPath: "/this/is/fake/package.json",
|
manifestPath: "/this/is/fake/package.json",
|
||||||
isBundled: false,
|
isBundled: false,
|
||||||
isEnabled: true
|
isEnabled: true,
|
||||||
|
isCompatible: true
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -30,10 +30,13 @@ import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } fr
|
|||||||
import { Singleton, toJS } from "../common/utils";
|
import { Singleton, toJS } from "../common/utils";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import { ExtensionInstallationStateStore } from "../renderer/components/+extensions/extension-install.store";
|
import { ExtensionInstallationStateStore } from "../renderer/components/+extensions/extension-install.store";
|
||||||
import { extensionInstaller, PackageJson } from "./extension-installer";
|
import { extensionInstaller } from "./extension-installer";
|
||||||
import { ExtensionsStore } from "./extensions-store";
|
import { ExtensionsStore } from "./extensions-store";
|
||||||
import { ExtensionLoader } from "./extension-loader";
|
import { ExtensionLoader } from "./extension-loader";
|
||||||
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
import type { LensExtensionId, LensExtensionManifest } from "./lens-extension";
|
||||||
|
import type { PackageJson } from "type-fest";
|
||||||
|
import semver from "semver";
|
||||||
|
import { appSemVer } from "../common/vars";
|
||||||
import { isProduction } from "../common/vars";
|
import { isProduction } from "../common/vars";
|
||||||
|
|
||||||
export interface InstalledExtension {
|
export interface InstalledExtension {
|
||||||
@ -48,6 +51,7 @@ export interface InstalledExtension {
|
|||||||
// Absolute to the symlinked package.json file
|
// Absolute to the symlinked package.json file
|
||||||
readonly manifestPath: string;
|
readonly manifestPath: string;
|
||||||
readonly isBundled: boolean; // defined in project root's package.json
|
readonly isBundled: boolean; // defined in project root's package.json
|
||||||
|
readonly isCompatible: boolean;
|
||||||
isEnabled: boolean;
|
isEnabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -349,12 +353,17 @@ export class ExtensionDiscovery extends Singleton {
|
|||||||
*/
|
*/
|
||||||
protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise<InstalledExtension | null> {
|
protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise<InstalledExtension | null> {
|
||||||
try {
|
try {
|
||||||
const manifest = await fse.readJson(manifestPath);
|
const manifest = await fse.readJson(manifestPath) as LensExtensionManifest;
|
||||||
const installedManifestPath = this.getInstalledManifestPath(manifest.name);
|
const installedManifestPath = this.getInstalledManifestPath(manifest.name);
|
||||||
const isEnabled = isBundled || ExtensionsStore.getInstance().isEnabled(installedManifestPath);
|
const isEnabled = isBundled || ExtensionsStore.getInstance().isEnabled(installedManifestPath);
|
||||||
const extensionDir = path.dirname(manifestPath);
|
const extensionDir = path.dirname(manifestPath);
|
||||||
const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
const npmPackage = path.join(extensionDir, `${manifest.name}-${manifest.version}.tgz`);
|
||||||
const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir;
|
const absolutePath = (isProduction && await fse.pathExists(npmPackage)) ? npmPackage : extensionDir;
|
||||||
|
let isCompatible = isBundled;
|
||||||
|
|
||||||
|
if (manifest.engines?.lens) {
|
||||||
|
isCompatible = semver.satisfies(appSemVer, manifest.engines.lens);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: installedManifestPath,
|
id: installedManifestPath,
|
||||||
@ -362,7 +371,8 @@ export class ExtensionDiscovery extends Singleton {
|
|||||||
manifestPath: installedManifestPath,
|
manifestPath: installedManifestPath,
|
||||||
manifest,
|
manifest,
|
||||||
isBundled,
|
isBundled,
|
||||||
isEnabled
|
isEnabled,
|
||||||
|
isCompatible
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.code === "ENOTDIR") {
|
if (error.code === "ENOTDIR") {
|
||||||
|
|||||||
@ -25,17 +25,10 @@ import fs from "fs-extra";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import { extensionPackagesRoot } from "./extension-loader";
|
import { extensionPackagesRoot } from "./extension-loader";
|
||||||
|
import type { PackageJson } from "type-fest";
|
||||||
|
|
||||||
const logModule = "[EXTENSION-INSTALLER]";
|
const logModule = "[EXTENSION-INSTALLER]";
|
||||||
|
|
||||||
type Dependencies = {
|
|
||||||
[name: string]: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Type for the package.json file that is written by ExtensionInstaller
|
|
||||||
export type PackageJson = {
|
|
||||||
dependencies: Dependencies;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Installs dependencies for extensions
|
* Installs dependencies for extensions
|
||||||
|
|||||||
@ -301,7 +301,7 @@ export class ExtensionLoader extends Singleton {
|
|||||||
for (const [extId, extension] of installedExtensions) {
|
for (const [extId, extension] of installedExtensions) {
|
||||||
const alreadyInit = this.instances.has(extId);
|
const alreadyInit = this.instances.has(extId);
|
||||||
|
|
||||||
if (extension.isEnabled && !alreadyInit) {
|
if (extension.isCompatible && extension.isEnabled && !alreadyInit) {
|
||||||
try {
|
try {
|
||||||
const LensExtensionClass = this.requireExtension(extension);
|
const LensExtensionClass = this.requireExtension(extension);
|
||||||
|
|
||||||
|
|||||||
@ -24,18 +24,17 @@ import { action, observable, makeObservable } from "mobx";
|
|||||||
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
|
import { FilesystemProvisionerStore } from "../main/extension-filesystem";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import type { ProtocolHandlerRegistration } from "./registries";
|
import type { ProtocolHandlerRegistration } from "./registries";
|
||||||
|
import type { PackageJson } from "type-fest";
|
||||||
import { Disposer, disposer } from "../common/utils";
|
import { Disposer, disposer } from "../common/utils";
|
||||||
|
|
||||||
export type LensExtensionId = string; // path to manifest (package.json)
|
export type LensExtensionId = string; // path to manifest (package.json)
|
||||||
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
|
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
|
||||||
|
|
||||||
export interface LensExtensionManifest {
|
export interface LensExtensionManifest extends PackageJson {
|
||||||
name: string;
|
name: string;
|
||||||
version: string;
|
version: string;
|
||||||
description?: string;
|
|
||||||
main?: string; // path to %ext/dist/main.js
|
main?: string; // path to %ext/dist/main.js
|
||||||
renderer?: string; // path to %ext/dist/renderer.js
|
renderer?: string; // path to %ext/dist/renderer.js
|
||||||
lens?: object; // fixme: add more required fields for validation
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Disposers = Symbol();
|
export const Disposers = Symbol();
|
||||||
|
|||||||
@ -40,7 +40,8 @@ describe("getPageUrl", () => {
|
|||||||
absolutePath: "/absolute/fake/",
|
absolutePath: "/absolute/fake/",
|
||||||
manifestPath: "/this/is/fake/package.json",
|
manifestPath: "/this/is/fake/package.json",
|
||||||
isBundled: false,
|
isBundled: false,
|
||||||
isEnabled: true
|
isEnabled: true,
|
||||||
|
isCompatible: true
|
||||||
});
|
});
|
||||||
globalPageRegistry.add({
|
globalPageRegistry.add({
|
||||||
id: "page-with-params",
|
id: "page-with-params",
|
||||||
@ -107,7 +108,8 @@ describe("globalPageRegistry", () => {
|
|||||||
absolutePath: "/absolute/fake/",
|
absolutePath: "/absolute/fake/",
|
||||||
manifestPath: "/this/is/fake/package.json",
|
manifestPath: "/this/is/fake/package.json",
|
||||||
isBundled: false,
|
isBundled: false,
|
||||||
isEnabled: true
|
isEnabled: true,
|
||||||
|
isCompatible: true
|
||||||
});
|
});
|
||||||
globalPageRegistry.add([
|
globalPageRegistry.add([
|
||||||
{
|
{
|
||||||
|
|||||||
@ -87,6 +87,7 @@ describe("protocol router tests", () => {
|
|||||||
},
|
},
|
||||||
isBundled: false,
|
isBundled: false,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
|
isCompatible: true,
|
||||||
absolutePath: "/foo/bar",
|
absolutePath: "/foo/bar",
|
||||||
});
|
});
|
||||||
const lpr = LensProtocolRouterMain.getInstance();
|
const lpr = LensProtocolRouterMain.getInstance();
|
||||||
@ -165,6 +166,7 @@ describe("protocol router tests", () => {
|
|||||||
},
|
},
|
||||||
isBundled: false,
|
isBundled: false,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
|
isCompatible: true,
|
||||||
absolutePath: "/foo/bar",
|
absolutePath: "/foo/bar",
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -206,6 +208,7 @@ describe("protocol router tests", () => {
|
|||||||
},
|
},
|
||||||
isBundled: false,
|
isBundled: false,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
|
isCompatible: true,
|
||||||
absolutePath: "/foo/bar",
|
absolutePath: "/foo/bar",
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -230,6 +233,7 @@ describe("protocol router tests", () => {
|
|||||||
},
|
},
|
||||||
isBundled: false,
|
isBundled: false,
|
||||||
isEnabled: true,
|
isEnabled: true,
|
||||||
|
isCompatible: true,
|
||||||
absolutePath: "/foo/bar",
|
absolutePath: "/foo/bar",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -76,7 +76,8 @@ describe("Extensions", () => {
|
|||||||
absolutePath: "/absolute/path",
|
absolutePath: "/absolute/path",
|
||||||
manifestPath: "/symlinked/path/package.json",
|
manifestPath: "/symlinked/path/package.json",
|
||||||
isBundled: false,
|
isBundled: false,
|
||||||
isEnabled: true
|
isEnabled: true,
|
||||||
|
isCompatible: true
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,10 @@
|
|||||||
color: var(--colorOk);
|
color: var(--colorOk);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.invalid {
|
||||||
|
color: var(--colorWarning);
|
||||||
|
}
|
||||||
|
|
||||||
.title {
|
.title {
|
||||||
margin-bottom: 0!important;
|
margin-bottom: 0!important;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -39,14 +39,18 @@ interface Props {
|
|||||||
uninstall: (extension: InstalledExtension) => void;
|
uninstall: (extension: InstalledExtension) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatus(isEnabled: boolean) {
|
function getStatus(extension: InstalledExtension) {
|
||||||
return isEnabled ? "Enabled" : "Disabled";
|
if (!extension.isCompatible) {
|
||||||
|
return "Incompatible";
|
||||||
|
}
|
||||||
|
|
||||||
|
return extension.isEnabled ? "Enabled" : "Disabled";
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InstalledExtensions = observer(({ extensions, uninstall, enable, disable }: Props) => {
|
export const InstalledExtensions = observer(({ extensions, uninstall, enable, disable }: Props) => {
|
||||||
const filters = [
|
const filters = [
|
||||||
(extension: InstalledExtension) => extension.manifest.name,
|
(extension: InstalledExtension) => extension.manifest.name,
|
||||||
(extension: InstalledExtension) => getStatus(extension.isEnabled),
|
(extension: InstalledExtension) => getStatus(extension),
|
||||||
(extension: InstalledExtension) => extension.manifest.version,
|
(extension: InstalledExtension) => extension.manifest.version,
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -87,7 +91,7 @@ export const InstalledExtensions = observer(({ extensions, uninstall, enable, di
|
|||||||
const data = useMemo(
|
const data = useMemo(
|
||||||
() => {
|
() => {
|
||||||
return extensions.map(extension => {
|
return extensions.map(extension => {
|
||||||
const { id, isEnabled, manifest } = extension;
|
const { id, isEnabled, isCompatible, manifest } = extension;
|
||||||
const { name, description, version } = manifest;
|
const { name, description, version } = manifest;
|
||||||
const isUninstalling = ExtensionInstallationStateStore.isExtensionUninstalling(id);
|
const isUninstalling = ExtensionInstallationStateStore.isExtensionUninstalling(id);
|
||||||
|
|
||||||
@ -102,12 +106,14 @@ export const InstalledExtensions = observer(({ extensions, uninstall, enable, di
|
|||||||
),
|
),
|
||||||
version,
|
version,
|
||||||
status: (
|
status: (
|
||||||
<div className={cssNames({[styles.enabled]: getStatus(isEnabled) == "Enabled"})}>
|
<div className={cssNames({[styles.enabled]: isEnabled, [styles.invalid]: !isCompatible})}>
|
||||||
{getStatus(isEnabled)}
|
{getStatus(extension)}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
actions: (
|
actions: (
|
||||||
<MenuActions usePortal toolbar={false}>
|
<MenuActions usePortal toolbar={false}>
|
||||||
|
{ isCompatible && (
|
||||||
|
<>
|
||||||
{isEnabled ? (
|
{isEnabled ? (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={isUninstalling}
|
disabled={isUninstalling}
|
||||||
@ -125,6 +131,9 @@ export const InstalledExtensions = observer(({ extensions, uninstall, enable, di
|
|||||||
<span className="title" aria-disabled={isUninstalling}>Enable</span>
|
<span className="title" aria-disabled={isUninstalling}>Enable</span>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<MenuItem
|
<MenuItem
|
||||||
disabled={isUninstalling}
|
disabled={isUninstalling}
|
||||||
onClick={() => uninstall(extension)}
|
onClick={() => uninstall(extension)}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user