mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Restructure to monorepo (#6907)
* wip: restructure to monorepo Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * refactor create-release-pr to a package Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * build fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * github workflow fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix typo Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * add webpack-env types to core Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix github workflows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * refactor/fix integration tests Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * lint fix Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * yarn run dev Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * eslint settings for vscode Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * move templates to right package Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * open-lens build fixes Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * integration test fix Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix nx task dependencies Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * use bash shell for unit tests in test workflow Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix test:unit for windows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix win-ca webpack error in open-lens Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix win-ca webpack error in open-lens Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * fix build:app on windows Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * remove ELECTRON_BUILDER_EXTRA_ARGS Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * sync src/ from master Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> * remove Makefile from core Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com> Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
1 parent
0066ca4ea2
commit
2657df2293
3321 files changed
+28699
-11447
No files matched your search
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type Url from "url-parse";
|
||||
|
||||
export enum RoutingErrorType {
|
||||
INVALID_PROTOCOL = "invalid-protocol",
|
||||
INVALID_HOST = "invalid-host",
|
||||
INVALID_PATHNAME = "invalid-pathname",
|
||||
NO_HANDLER = "no-handler",
|
||||
NO_EXTENSION_ID = "no-ext-id",
|
||||
MISSING_EXTENSION = "missing-ext",
|
||||
}
|
||||
|
||||
export class RoutingError<Query> extends Error {
|
||||
/**
|
||||
* Will be set if the routing error originated in an extension route table
|
||||
*/
|
||||
public extensionName?: string;
|
||||
|
||||
constructor(public type: RoutingErrorType, public url: Url<Query>) {
|
||||
super("routing error");
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
switch (this.type) {
|
||||
case RoutingErrorType.INVALID_HOST:
|
||||
return "invalid host";
|
||||
case RoutingErrorType.INVALID_PROTOCOL:
|
||||
return "invalid protocol";
|
||||
case RoutingErrorType.INVALID_PATHNAME:
|
||||
return "invalid pathname";
|
||||
case RoutingErrorType.NO_EXTENSION_ID:
|
||||
return "no extension ID";
|
||||
case RoutingErrorType.MISSING_EXTENSION:
|
||||
return "extension not found";
|
||||
default:
|
||||
return `unknown error: ${this.type}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
export * from "./error";
|
||||
export * from "./router";
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
/**
|
||||
* ProtocolHandlerRegistration is the data required for an extension to register
|
||||
* a handler to a specific path or dynamic path.
|
||||
*/
|
||||
export interface ProtocolHandlerRegistration {
|
||||
pathSchema: string;
|
||||
handler: RouteHandler;
|
||||
}
|
||||
|
||||
/**
|
||||
* The collection of the dynamic parts of a URI which initiated a `lens://`
|
||||
* protocol request
|
||||
*/
|
||||
export interface RouteParams {
|
||||
/**
|
||||
* the parts of the URI query string
|
||||
*/
|
||||
search: Record<string, string | undefined>;
|
||||
|
||||
/**
|
||||
* the matching parts of the path. The dynamic parts of the URI path.
|
||||
*/
|
||||
pathname: Record<string, string | undefined>;
|
||||
|
||||
/**
|
||||
* if the most specific path schema that is matched does not cover the whole
|
||||
* of the URI's path. Then this field will be set to the remaining path
|
||||
* segments.
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* If the path schema `/landing/:type` is the matched schema for the URI
|
||||
* `/landing/soft/easy` then this field will be set to `"/easy"`.
|
||||
*/
|
||||
tail?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* RouteHandler represents the function signature of the handler function for
|
||||
* `lens://` protocol routing.
|
||||
*/
|
||||
export interface RouteHandler {
|
||||
(params: RouteParams): void;
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
/**
|
||||
* Copyright (c) OpenLens Authors. All rights reserved.
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import type { match } from "react-router";
|
||||
import { matchPath } from "react-router";
|
||||
import { countBy } from "lodash";
|
||||
import { isDefined, iter } from "../utils";
|
||||
import { pathToRegexp } from "path-to-regexp";
|
||||
import type Url from "url-parse";
|
||||
import { RoutingError, RoutingErrorType } from "./error";
|
||||
import type { ExtensionsStore } from "../../extensions/extensions-store/extensions-store";
|
||||
import type { ExtensionLoader } from "../../extensions/extension-loader";
|
||||
import type { LensExtension } from "../../extensions/lens-extension";
|
||||
import type { RouteHandler, RouteParams } from "./registration";
|
||||
import { when } from "mobx";
|
||||
import { ipcRenderer } from "electron";
|
||||
import type { Logger } from "../logger";
|
||||
|
||||
// IPC channel for protocol actions. Main broadcasts the open-url events to this channel.
|
||||
export const ProtocolHandlerIpcPrefix = "protocol-handler";
|
||||
|
||||
export const ProtocolHandlerInternal = `${ProtocolHandlerIpcPrefix}:internal`;
|
||||
export const ProtocolHandlerExtension = `${ProtocolHandlerIpcPrefix}:extension`;
|
||||
export const ProtocolHandlerInvalid = `${ProtocolHandlerIpcPrefix}:invalid`;
|
||||
|
||||
/**
|
||||
* These two names are long and cumbersome by design so as to decrease the chances
|
||||
* of an extension using the same names.
|
||||
*
|
||||
* Though under the current (2021/01/18) implementation, these are never matched
|
||||
* against in the final matching so their names are less of a concern.
|
||||
*/
|
||||
export const EXTENSION_PUBLISHER_MATCH = "LENS_INTERNAL_EXTENSION_PUBLISHER_MATCH";
|
||||
export const EXTENSION_NAME_MATCH = "LENS_INTERNAL_EXTENSION_NAME_MATCH";
|
||||
|
||||
/**
|
||||
* Returned from routing attempts
|
||||
*/
|
||||
export enum RouteAttempt {
|
||||
/**
|
||||
* A handler was found in the set of registered routes
|
||||
*/
|
||||
MATCHED = "matched",
|
||||
/**
|
||||
* A handler was not found within the set of registered routes
|
||||
*/
|
||||
MISSING = "missing",
|
||||
/**
|
||||
* The extension that was matched in the route was not activated
|
||||
*/
|
||||
MISSING_EXTENSION = "no-extension",
|
||||
}
|
||||
|
||||
export function foldAttemptResults(mainAttempt: RouteAttempt, rendererAttempt: RouteAttempt): RouteAttempt {
|
||||
switch (mainAttempt) {
|
||||
case RouteAttempt.MATCHED:
|
||||
return RouteAttempt.MATCHED;
|
||||
case RouteAttempt.MISSING:
|
||||
case RouteAttempt.MISSING_EXTENSION:
|
||||
return rendererAttempt;
|
||||
}
|
||||
}
|
||||
|
||||
export interface LensProtocolRouterDependencies {
|
||||
readonly extensionLoader: ExtensionLoader;
|
||||
readonly extensionsStore: ExtensionsStore;
|
||||
readonly logger: Logger;
|
||||
}
|
||||
|
||||
export abstract class LensProtocolRouter {
|
||||
// Map between path schemas and the handlers
|
||||
protected internalRoutes = new Map<string, RouteHandler>();
|
||||
|
||||
public static readonly LoggingPrefix = "[PROTOCOL ROUTER]";
|
||||
|
||||
static readonly ExtensionUrlSchema = `/:${EXTENSION_PUBLISHER_MATCH}(@[A-Za-z0-9_]+)?/:${EXTENSION_NAME_MATCH}`;
|
||||
|
||||
constructor(protected readonly dependencies: LensProtocolRouterDependencies) {}
|
||||
|
||||
/**
|
||||
* Attempts to route the given URL to all internal routes that have been registered
|
||||
* @param url the parsed URL that initiated the `lens://` protocol
|
||||
* @returns true if a route has been found
|
||||
*/
|
||||
protected _routeToInternal(url: Url<Record<string, string | undefined>>): RouteAttempt {
|
||||
return this._route(this.internalRoutes.entries(), url);
|
||||
}
|
||||
|
||||
/**
|
||||
* match against all matched URIs, returning either the first exact match or
|
||||
* the most specific match if none are exact.
|
||||
* @param routes the array of path schemas, handler pairs to match against
|
||||
* @param url the url (in its current state)
|
||||
*/
|
||||
protected _findMatchingRoute(routes: Iterable<[string, RouteHandler]>, url: Url<Record<string, string | undefined>>): null | [match<Record<string, string>>, RouteHandler] {
|
||||
const matches: [match<Record<string, string>>, RouteHandler][] = [];
|
||||
|
||||
for (const [schema, handler] of routes) {
|
||||
const match = matchPath(url.pathname, { path: schema });
|
||||
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// prefer an exact match
|
||||
if (match.isExact) {
|
||||
return [match, handler];
|
||||
}
|
||||
|
||||
matches.push([match, handler]);
|
||||
}
|
||||
|
||||
// if no exact match pick the one that is the most specific
|
||||
return matches.sort(([a], [b]) => {
|
||||
if (a.path === "/") {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (b.path === "/") {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return countBy(b.path)["/"] - countBy(a.path)["/"];
|
||||
})[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* find the most specific matching handler and call it
|
||||
* @param routes the array of (path schemas, handler) pairs to match against
|
||||
* @param url the url (in its current state)
|
||||
*/
|
||||
protected _route(routes: Iterable<[string, RouteHandler]>, url: Url<Record<string, string | undefined>>, extensionName?: string): RouteAttempt {
|
||||
const route = this._findMatchingRoute(routes, url);
|
||||
|
||||
if (!route) {
|
||||
const data: Record<string, string> = { url: url.toString() };
|
||||
|
||||
if (extensionName) {
|
||||
data.extensionName = extensionName;
|
||||
}
|
||||
|
||||
this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: No handler found`, data);
|
||||
|
||||
return RouteAttempt.MISSING;
|
||||
}
|
||||
|
||||
const [match, handler] = route;
|
||||
|
||||
const params: RouteParams = {
|
||||
pathname: match.params,
|
||||
search: url.query,
|
||||
};
|
||||
|
||||
if (!match.isExact) {
|
||||
params.tail = url.pathname.slice(match.url.length);
|
||||
}
|
||||
|
||||
handler(params);
|
||||
|
||||
return RouteAttempt.MATCHED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find the matching LensExtension instance
|
||||
*
|
||||
* Note: this needs to be async so that `main`'s overloaded version can also be async
|
||||
* @param url the protocol request URI that was "open"-ed
|
||||
* @returns either the found name or the instance of `LensExtension`
|
||||
*/
|
||||
protected async _findMatchingExtensionByName(url: Url<Record<string, string | undefined>>): Promise<LensExtension | string> {
|
||||
interface ExtensionUrlMatch {
|
||||
[EXTENSION_PUBLISHER_MATCH]: string;
|
||||
[EXTENSION_NAME_MATCH]: string;
|
||||
}
|
||||
|
||||
const match = matchPath<ExtensionUrlMatch>(url.pathname, LensProtocolRouter.ExtensionUrlSchema);
|
||||
|
||||
if (!match) {
|
||||
throw new RoutingError(RoutingErrorType.NO_EXTENSION_ID, url);
|
||||
}
|
||||
|
||||
const { [EXTENSION_PUBLISHER_MATCH]: publisher, [EXTENSION_NAME_MATCH]: partialName } = match.params;
|
||||
const name = [publisher, partialName].filter(isDefined).join("/");
|
||||
|
||||
const extensionLoader = this.dependencies.extensionLoader;
|
||||
|
||||
try {
|
||||
/**
|
||||
* Note, if `getInstanceByName` returns `null` that means we won't be getting an instance
|
||||
*/
|
||||
await when(() => extensionLoader.getInstanceByName(name) !== void 0, {
|
||||
timeout: 5_000,
|
||||
});
|
||||
} catch (error) {
|
||||
this.dependencies.logger.info(
|
||||
`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not installed (${error})`,
|
||||
);
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
const extension = extensionLoader.getInstanceByName(name);
|
||||
|
||||
if (!extension) {
|
||||
this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but does not have a class for ${ipcRenderer ? "renderer" : "main"}`);
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
if (!this.dependencies.extensionsStore.isEnabled(extension)) {
|
||||
this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched, but not enabled`);
|
||||
|
||||
return name;
|
||||
}
|
||||
|
||||
this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: Extension ${name} matched`);
|
||||
|
||||
return extension;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find a matching extension by the first one or two path segments of `url` and then try to `_route`
|
||||
* its correspondingly registered handlers.
|
||||
*
|
||||
* If no handlers are found or the extension is not enabled then `_missingHandlers` is called before
|
||||
* checking if more handlers have been added.
|
||||
*
|
||||
* Note: this function modifies its argument, do not reuse
|
||||
* @param url the protocol request URI that was "open"-ed
|
||||
*/
|
||||
protected async _routeToExtension(url: Url<Record<string, string | undefined>>): Promise<RouteAttempt> {
|
||||
const extension = await this._findMatchingExtensionByName(url);
|
||||
|
||||
if (typeof extension === "string") {
|
||||
// failed to find an extension, it returned its name
|
||||
return RouteAttempt.MISSING_EXTENSION;
|
||||
}
|
||||
|
||||
// remove the extension name from the path name so we don't need to match on it anymore
|
||||
url.set("pathname", url.pathname.slice(extension.name.length + 1));
|
||||
|
||||
try {
|
||||
const handlers = iter.map(extension.protocolHandlers, ({ pathSchema, handler }) => [pathSchema, handler] as [string, RouteHandler]);
|
||||
|
||||
return this._route(handlers, url, extension.name);
|
||||
} catch (error) {
|
||||
if (error instanceof RoutingError) {
|
||||
error.extensionName = extension.name;
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a handler under the `lens://app` tree of routing.
|
||||
* @param pathSchema the URI path schema to match against for this handler
|
||||
* @param handler a function that will be called if a protocol path matches
|
||||
*/
|
||||
public addInternalHandler(urlSchema: string, handler: RouteHandler): this {
|
||||
pathToRegexp(urlSchema); // verify now that the schema is valid
|
||||
this.dependencies.logger.info(`${LensProtocolRouter.LoggingPrefix}: internal registering ${urlSchema}`);
|
||||
this.internalRoutes.set(urlSchema, handler);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an internal protocol handler.
|
||||
* @param pathSchema the path schema that the handler was registered under
|
||||
*/
|
||||
public removeInternalHandler(urlSchema: string): void {
|
||||
this.internalRoutes.delete(urlSchema);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user