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:
Jari Kolehmainen authored and GitHub committed 2023-01-24 10:46:26 -08:00
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);
}
}