add router

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton committed 2020-12-09 11:00:07 -05:00
1 parent 24b5352614
commit 62df81d955
7 files changed
+183 -3

No files matched your search

+7
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;
}
+13 -2
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
@@ -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();
});
});
+1
View File
@@ -0,0 +1 @@
export * from "./router";
+130
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);
}
}