1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

add router

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2020-12-07 20:07:15 -05:00
parent 24b5352614
commit 62df81d955
7 changed files with 183 additions and 3 deletions

View File

@ -212,6 +212,7 @@
"@types/proper-lockfile": "^4.1.1",
"@types/react-beautiful-dnd": "^13.0.0",
"@types/tar": "^4.0.4",
"@types/url-parse": "^1.4.3",
"array-move": "^3.0.0",
"await-lock": "^2.1.0",
"chalk": "^4.1.0",
@ -250,6 +251,7 @@
"tar": "^6.0.5",
"tcp-port-used": "^1.0.1",
"tempy": "^0.5.0",
"url-parse": "^1.4.7",
"uuid": "^8.1.0",
"win-ca": "^3.2.0",
"winston": "^3.2.1",

View File

@ -2,6 +2,7 @@ import type { InstalledExtension } from "./extension-discovery";
import { action, observable, reaction } from "mobx";
import { filesystemProvisionerStore } from "../main/extension-filesystem";
import logger from "../main/logger";
import { LensProtocolRouter, RouteHandler } from "../main/protocol-handler";
export type LensExtensionId = string; // path to manifest (package.json)
export type LensExtensionConstructor = new (...args: ConstructorParameters<typeof LensExtension>) => LensExtension;
@ -49,6 +50,12 @@ export class LensExtension {
return filesystemProvisionerStore.requestDirectory(this.id);
}
onProtocolRequest(pathSchema: string, handler: RouteHandler): void {
const lpr = LensProtocolRouter.getInstance<LensProtocolRouter>();
lpr.extensionOn(this.id, pathSchema, handler);
}
get description() {
return this.manifest.description;
}

View File

@ -26,6 +26,8 @@ import { InstalledExtension, extensionDiscovery } from "../extensions/extension-
import type { LensExtensionId } from "../extensions/lens-extension";
import { installDeveloperTools } from "./developer-tools";
import { filesystemProvisionerStore } from "./extension-filesystem";
import { LensProtocolRouter, RoutingError } from "./protocol-handler";
import Url from "url-parse";
const workingDir = path.join(app.getPath("appData"), appName);
let proxyPort: number;
@ -125,10 +127,19 @@ app
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
})
.on("open-url", (event, url) => {
.on("open-url", (event, rawUrl) => {
// protocol handler for macOS
logger.info("open-url", { url });
event.preventDefault();
try {
LensProtocolRouter.getInstance<LensProtocolRouter>().route(Url(rawUrl, true));
} catch (error) {
if (error instanceof RoutingError) {
logger.error(`[PROTOCOL ROUTER]: ${error}`, { url: error.url });
} else {
logger.error(`[PROTOCOL ROUTER]: ${error}`, { rawUrl });
}
}
});
// Extensions-api runtime exports

View File

@ -0,0 +1,24 @@
import { LensProtocolRouter } from "../router";
import Url from "url-parse";
describe("protocol router tests", () => {
let lpr: LensProtocolRouter;
beforeEach(() => {
LensProtocolRouter.resetInstance();
lpr = LensProtocolRouter.getInstance<LensProtocolRouter>();
});
it("should throw on non-lens URLS", () => {
expect(() => lpr.route(Url("https://google.ca"))).toThrowError();
});
it("should throw when host not internal or extension", () => {
expect(() => lpr.route(Url("lens://foobar"))).toThrowError();
});
it("should not throw when has valid host", () => {
expect(() => lpr.route(Url("lens://internal"))).not.toThrowError();
expect(() => lpr.route(Url("lens://extension"))).not.toThrowError();
});
});

View File

@ -0,0 +1 @@
export * from "./router";

View File

@ -0,0 +1,130 @@
import { Singleton } from "../../common/utils";
import Url from "url-parse";
import { match, matchPath } from "react-router";
export enum RoutingErrorType {
INVALID_PROTOCOL = "invalid-protocol",
INVALID_HOST = "invalid-host",
NO_HANDLER = "no-handler",
NO_EXTENSION_ID = "no-ext-id",
MISSING_EXTENSION = "missing-ext",
}
export class RoutingError extends Error {
constructor(public type: RoutingErrorType, public url: Url) {
super();
}
toString() {
switch (this.type) {
case RoutingErrorType.INVALID_HOST:
return "invalid host";
case RoutingErrorType.INVALID_PROTOCOL:
return "invalid protocol";
case RoutingErrorType.NO_HANDLER:
return "no handler";
case RoutingErrorType.NO_EXTENSION_ID:
return "no extension ID";
case RoutingErrorType.MISSING_EXTENSION:
return "extension not found";
}
}
}
export interface RouteParams {
search: Record<string, string>;
pathname: Record<string, string>;
}
export type RouteHandler = (params: RouteParams) => void;
export type ExtensionId = string;
const EXT_ID_MATCH = "LENS_INTERNAL_EXTENSION_ID_MATCH";
interface ExtensionIdMatch {
[EXT_ID_MATCH]: string;
}
export class LensProtocolRouter extends Singleton {
private exentionRoutes = new Map<ExtensionId, Map<string, RouteHandler>>();
private internalRoutes = new Map<string, RouteHandler>();
private static ExtensionIDSchema = `/:${EXT_ID_MATCH}/`;
/**
* route
*/
public route(url: Url): void {
if (url.protocol.toLowerCase() !== "lens:") {
throw new RoutingError(RoutingErrorType.INVALID_PROTOCOL, url);
}
switch (url.host) {
case "internal":
return this._route(this.internalRoutes, url);
case "extension":
return this._routeToExtension(url);
default:
throw new RoutingError(RoutingErrorType.INVALID_HOST, url);
}
}
private _routeToExtension(url: Url): void {
const match = matchPath<ExtensionIdMatch>(url.pathname, { path: LensProtocolRouter.ExtensionIDSchema });
if (!match) {
throw new RoutingError(RoutingErrorType.NO_EXTENSION_ID, url);
}
const { [EXT_ID_MATCH]: id } = match.params;
const routes = this.exentionRoutes.get(id);
if (!routes) {
throw new RoutingError(RoutingErrorType.MISSING_EXTENSION, url);
}
this._route(routes, url, true);
}
private _route(routes: Map<string, RouteHandler>, url: Url, matchExtension = false): void {
const matches = Array.from(routes.entries())
.map(([schema, handler]): [match<Record<string, string>>, RouteHandler] => {
const path = `${matchExtension ? LensProtocolRouter.ExtensionIDSchema : ""}${schema}`;
return [matchPath(url.pathname, { path }), handler];
})
.filter(([match]) => match);
// prefer an exact match, but if not pick the first route registered
const route = matches.find(([match]) => match.isExact) ?? matches[0];
if (!route) {
throw new RoutingError(RoutingErrorType.NO_HANDLER, url);
}
const [match, handler] = route;
delete match.params[EXT_ID_MATCH];
handler({
pathname: match.params,
search: url.query,
});
}
public on(urlSchema: string, handler: RouteHandler): void {
this.internalRoutes.set(urlSchema, handler);
}
public extensionOn(id: ExtensionId, urlSchema: string, handler: RouteHandler): void {
if (!this.exentionRoutes.has(id)) {
this.exentionRoutes.set(id, new Map());
}
if (urlSchema.includes(`:${EXT_ID_MATCH}`)) {
throw new TypeError("Invalid url path schema");
}
this.exentionRoutes.get(id).set(urlSchema, handler);
}
}

View File

@ -2483,6 +2483,11 @@
resolved "https://registry.yarnpkg.com/@types/universal-analytics/-/universal-analytics-0.4.4.tgz#496a52b92b599a0112bec7c12414062de6ea8449"
integrity sha512-9g3F0SGxVr4UDd6y07bWtFnkpSSX1Ake7U7AGHgSFrwM6pF53/fV85bfxT2JLWS/3sjLCcyzoYzQlCxpkVo7wA==
"@types/url-parse@^1.4.3":
version "1.4.3"
resolved "https://registry.yarnpkg.com/@types/url-parse/-/url-parse-1.4.3.tgz#fba49d90f834951cb000a674efee3d6f20968329"
integrity sha512-4kHAkbV/OfW2kb5BLVUuUMoumB3CP8rHqlw48aHvFy5tf9ER0AfOonBlX29l/DD68G70DmyhRlSYfQPSYpC5Vw==
"@types/uuid@^8.0.0":
version "8.0.0"
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.0.0.tgz#165aae4819ad2174a17476dbe66feebd549556c0"
@ -14772,7 +14777,7 @@ url-parse-lax@^3.0.0:
dependencies:
prepend-http "^2.0.0"
url-parse@^1.4.3:
url-parse@^1.4.3, url-parse@^1.4.7:
version "1.4.7"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278"
integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg==