diff --git a/.eslintrc.js b/.eslintrc.js index 823c5bdfee..3fd52c2465 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -33,7 +33,6 @@ module.exports = { "indent": ["error", 2, { "SwitchCase": 1, }], - "no-invalid-this": "error", "no-unused-vars": "off", "unused-imports/no-unused-imports": "error", "unused-imports/no-unused-vars": [ @@ -96,7 +95,6 @@ module.exports = { "indent": ["error", 2, { "SwitchCase": 1, }], - "no-invalid-this": "error", "quotes": ["error", "double", { "avoidEscape": true, "allowTemplateLiterals": true, @@ -162,7 +160,6 @@ module.exports = { "avoidEscape": true, "allowTemplateLiterals": true, }], - "no-invalid-this": "error", "semi": "off", "@typescript-eslint/semi": ["error"], "object-shorthand": "error", diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index 50a27b7e13..5e4ce43a2a 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -84,6 +84,18 @@ export class ExtensionLoader { this.extensions.replace(extensions); } + isInstalled(name: string) { + for (const extensionEntry of this.extensions) { + const [, extension ] = extensionEntry; + + if (extension.manifest.name === name) { + return true; + } + } + + return false; + } + addExtension(extension: InstalledExtension) { this.extensions.set(extension.id, extension); } @@ -103,7 +115,6 @@ export class ExtensionLoader { } catch (error) { logger.error(`${logModule}: deactivation extension error`, { lensExtensionId, error }); } - } removeExtension(lensExtensionId: LensExtensionId) { diff --git a/src/main/protocol-handler/router.ts b/src/main/protocol-handler/router.ts index b7addf8fb7..dedcd2e1e1 100644 --- a/src/main/protocol-handler/router.ts +++ b/src/main/protocol-handler/router.ts @@ -46,20 +46,23 @@ export type RouteHandler = (params: RouteParams) => void; export type ExtensionId = string; -const EXT_ID_MATCH = "LENS_INTERNAL_EXTENSION_ID_MATCH"; +const EXTENSION_PUBLISHER_MATCH = "LENS_INTERNAL_EXTENSION_PUBLISHER_MATCH"; +const EXTENSION_NAME_MATCH = "LENS_INTERNAL_EXTENSION_NAME_MATCH"; // IPC channel for protocol actions. Main broadcasts the open-url events to this channel. export const lensProtocolChannel = "protocol-handler"; -interface ExtensionIdMatch { - [EXT_ID_MATCH]: string; +interface ExtensionUrlMatch { + [EXTENSION_PUBLISHER_MATCH]: string; + [EXTENSION_NAME_MATCH]: string; } export class LensProtocolRouter extends Singleton { private extentionRoutes = new Map>(); private internalRoutes = new Map(); + private missingExtensionHandler?: (name: string) => Promise; - private static ExtensionIDSchema = `/:${EXT_ID_MATCH}`; + private static ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}/:${EXTENSION_NAME_MATCH}`; public init() { subscribeToBroadcast(lensProtocolChannel, ((_event, { rawUrl }) => { @@ -87,25 +90,38 @@ export class LensProtocolRouter extends Singleton { case "internal": return this._route(this.internalRoutes, url); case "extension": - return this._routeToExtension(url); + // Possible rejected promise is ignored + this._routeToExtension(url); default: throw new RoutingError(RoutingErrorType.INVALID_HOST, url); } } - private _routeToExtension(url: Url): void { - const match = matchPath(url.pathname, LensProtocolRouter.ExtensionIDSchema); + private async _routeToExtension(url: Url) { + const match = matchPath(url.pathname, LensProtocolRouter.ExtensionUrlSchema); if (!match) { throw new RoutingError(RoutingErrorType.NO_EXTENSION_ID, url); } - const { [EXT_ID_MATCH]: id } = match.params; - const routes = this.extentionRoutes.get(id); + const { [EXTENSION_PUBLISHER_MATCH]: publisher, [EXTENSION_NAME_MATCH]: partialName } = match.params; + const name = `${publisher}/${partialName}`; + + logger.info(`[PROTOCOL ROUTER] Extension ${name} matched`); + + const routes = this.extentionRoutes.get(name); if (!routes) { - throw new RoutingError(RoutingErrorType.MISSING_EXTENSION, url); + if (this.missingExtensionHandler) { + await this.missingExtensionHandler(name); + + // TODO: After installation we can continue to route to the extension.. + // but this is difficult, since the promise resolves before extension installation is complete. + return; + } else { + throw new RoutingError(RoutingErrorType.MISSING_EXTENSION, url); + } } this._route(routes, url, true); @@ -117,7 +133,7 @@ export class LensProtocolRouter extends Singleton { if (matchExtension) { const joinChar = schema.startsWith("/") ? "" : "/"; - schema = `${LensProtocolRouter.ExtensionIDSchema}${joinChar}${schema}`; + schema = `${LensProtocolRouter.ExtensionUrlSchema}${joinChar}${schema}`; } return [matchPath(url.pathname, { path: schema }), handler]; @@ -132,7 +148,7 @@ export class LensProtocolRouter extends Singleton { const [match, handler] = route; - delete match.params[EXT_ID_MATCH]; + delete match.params[EXTENSION_NAME_MATCH]; handler({ pathname: match.params, search: url.query, @@ -151,10 +167,14 @@ export class LensProtocolRouter extends Singleton { this.extentionRoutes.set(id, new Map()); } - if (urlSchema.includes(`:${EXT_ID_MATCH}`)) { + if (urlSchema.includes(`:${EXTENSION_NAME_MATCH}`)) { throw new TypeError("Invalid url path schema"); } this.extentionRoutes.get(id).set(urlSchema, handler); } + + public onMissingExtension(handler: (name: string) => Promise) { + this.missingExtensionHandler = handler; + } } diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index f6cff633af..c04361f6d5 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -19,6 +19,8 @@ import { LensApp } from "./lens-app"; import { themeStore } from "./theme.store"; import protocolEndpoints from "./api/protocol-endpoints"; import { LensProtocolRouter } from "../main/protocol-handler"; +import logger from "../main/logger"; +import { installFromNpm } from "./components/+extensions"; type AppComponent = React.ComponentType & { init?(): Promise; @@ -38,7 +40,19 @@ export async function bootstrap(App: AppComponent) { extensionLoader.init(); extensionDiscovery.init(); - LensProtocolRouter.getInstance().init(); + const lensProtocolRouter = LensProtocolRouter.getInstance(); + + lensProtocolRouter.init(); + lensProtocolRouter.onMissingExtension(async name => { + if (!extensionLoader.isInstalled(name)) { + logger.info(`[PROTOCOL ROUTER] Extension ${name} not installed, installing..`); + + // TODO: This actually resolves before the extension installation is complete + return installFromNpm(name); + } else { + logger.info(`[PROTOCOL ROUTER] Extension already installed, but route is missing.`); + } + }); protocolEndpoints.registerHandlers(); // preload common stores