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:
parent
24b5352614
commit
62df81d955
@ -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",
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
24
src/main/protocol-handler/__test__/router.test.ts
Normal file
24
src/main/protocol-handler/__test__/router.test.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
1
src/main/protocol-handler/index.ts
Normal file
1
src/main/protocol-handler/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./router";
|
||||
130
src/main/protocol-handler/router.ts
Normal file
130
src/main/protocol-handler/router.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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==
|
||||
|
||||
Loading…
Reference in New Issue
Block a user