1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00
lens/src/extensions/extension-manager.ts
Roman 94ac081588
Extensions api fixes (#1233)
* fix: create extension instance only when enabled

Signed-off-by: Roman <ixrock@gmail.com>

* mark extension.isEnabled with private modifier

Signed-off-by: Roman <ixrock@gmail.com>

* try-catch errors for extension.disable()

Signed-off-by: Roman <ixrock@gmail.com>

* fixes & refactoring

Signed-off-by: Roman <ixrock@gmail.com>

* make ext.isBundled non optional

Signed-off-by: Roman <ixrock@gmail.com>
2020-11-07 18:56:26 +02:00

173 lines
5.8 KiB
TypeScript

import type { LensExtensionId, LensExtensionManifest } from "./lens-extension"
import path from "path"
import os from "os"
import fs from "fs-extra"
import child_process from "child_process";
import logger from "../main/logger"
import { extensionPackagesRoot } from "./extension-loader"
import { getBundledExtensions } from "../common/utils/app-version"
export interface InstalledExtension {
readonly manifest: LensExtensionManifest;
readonly manifestPath: string;
readonly isBundled: boolean; // defined in project root's package.json
isEnabled: boolean;
}
type Dependencies = {
[name: string]: string;
}
type PackageJson = {
dependencies: Dependencies;
}
export class ExtensionManager {
protected bundledFolderPath: string
protected packagesJson: PackageJson = {
dependencies: {}
}
get extensionPackagesRoot() {
return extensionPackagesRoot()
}
get inTreeTargetPath() {
return path.join(this.extensionPackagesRoot, "extensions")
}
get inTreeFolderPath(): string {
return path.resolve(__static, "../extensions");
}
get nodeModulesPath(): string {
return path.join(this.extensionPackagesRoot, "node_modules")
}
get localFolderPath(): string {
return path.join(os.homedir(), ".k8slens", "extensions");
}
get npmPath() {
return __non_webpack_require__.resolve('npm/bin/npm-cli')
}
get packageJsonPath() {
return path.join(this.extensionPackagesRoot, "package.json")
}
async load(): Promise<Map<LensExtensionId, InstalledExtension>> {
logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot)
if (fs.existsSync(path.join(this.extensionPackagesRoot, "package-lock.json"))) {
await fs.remove(path.join(this.extensionPackagesRoot, "package-lock.json"))
}
try {
await fs.access(this.inTreeFolderPath, fs.constants.W_OK)
this.bundledFolderPath = this.inTreeFolderPath
} catch {
// we need to copy in-tree extensions so that we can symlink them properly on "npm install"
await fs.remove(this.inTreeTargetPath)
await fs.ensureDir(this.inTreeTargetPath)
await fs.copy(this.inTreeFolderPath, this.inTreeTargetPath)
this.bundledFolderPath = this.inTreeTargetPath
}
await fs.ensureDir(this.nodeModulesPath)
await fs.ensureDir(this.localFolderPath)
return await this.loadExtensions();
}
protected async getByManifest(manifestPath: string, { isBundled = false } = {}): Promise<InstalledExtension> {
let manifestJson: LensExtensionManifest;
try {
fs.accessSync(manifestPath, fs.constants.F_OK); // check manifest file for existence
manifestJson = __non_webpack_require__(manifestPath)
this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath)
logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name)
return {
manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"),
manifest: manifestJson,
isBundled: isBundled,
isEnabled: isBundled,
}
} catch (err) {
logger.error(`[EXTENSION-MANAGER]: can't install extension at ${manifestPath}: ${err}`, { manifestJson });
}
}
protected installPackages(): Promise<void> {
return new Promise((resolve, reject) => {
const child = child_process.fork(this.npmPath, ["install", "--silent", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], {
cwd: extensionPackagesRoot(),
silent: true
})
child.on("close", () => {
resolve()
})
child.on("error", (err) => {
reject(err)
})
})
}
async loadExtensions() {
const bundledExtensions = await this.loadBundledExtensions()
const localExtensions = await this.loadFromFolder(this.localFolderPath)
await fs.writeFile(path.join(this.packageJsonPath), JSON.stringify(this.packagesJson, null, 2), { mode: 0o600 })
await this.installPackages()
const extensions = bundledExtensions.concat(localExtensions)
return new Map(extensions.map(ext => [ext.manifestPath, ext]));
}
async loadBundledExtensions() {
const extensions: InstalledExtension[] = []
const folderPath = this.bundledFolderPath
const bundledExtensions = getBundledExtensions()
const paths = await fs.readdir(folderPath);
for (const fileName of paths) {
if (!bundledExtensions.includes(fileName)) {
continue
}
const absPath = path.resolve(folderPath, fileName);
const manifestPath = path.resolve(absPath, "package.json");
const ext = await this.getByManifest(manifestPath, { isBundled: true }).catch(() => null)
if (ext) {
extensions.push(ext)
}
}
logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
return extensions
}
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> {
const bundledExtensions = getBundledExtensions()
const extensions: InstalledExtension[] = []
const paths = await fs.readdir(folderPath);
for (const fileName of paths) {
if (bundledExtensions.includes(fileName)) { // do no allow to override bundled extensions
continue
}
const absPath = path.resolve(folderPath, fileName);
if (!fs.existsSync(absPath)) {
continue
}
const lstat = await fs.lstat(absPath)
if (!lstat.isDirectory() && !lstat.isSymbolicLink()) { // skip non-directories
continue
}
const manifestPath = path.resolve(absPath, "package.json");
const ext = await this.getByManifest(manifestPath).catch(() => null)
if (ext) {
extensions.push(ext)
}
}
logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
return extensions;
}
}
export const extensionManager = new ExtensionManager()