Getting the client to run (#12)
* Clean up workbench and integrate initialization data * Uncomment Electron fill * Run server & client together * Clean up Electron fill & patch * Bind fs methods This makes them usable with the promise form: `promisify(access)(...)`. * Add space between tag and title to browser logger * Add typescript dep to server and default __dirname for path * Serve web files from server * Adjust some dev options * Rework workbench a bit to use a class and catch unexpected errors * No mkdirs for now, fix util fill, use bash with exec * More fills, make general client abstract * More fills * Fix cp.exec * Fix require calls in fs fill being aliased * Create data and storage dir * Implement fs.watch Using exec for now. * Implement storage database fill * Fix os export and homedir * Add comment to use navigator.sendBeacon * Fix fs callbacks (some args are optional) * Make sure data directory exists when passing it back * Update patch * Target es5 * More fills * Add APIs required for bootstrap-fork to function (#15) * Add bootstrap-fork execution * Add createConnection * Bundle bootstrap-fork into cli * Remove .node directory created from spdlog * Fix npm start * Remove unnecessary comment * Add webpack-hot-middleware if CLI env is not set * Add restarting to shared process * Fix starting with yarn
This commit is contained in:
@ -1,11 +1,26 @@
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { field, logger, time, Time } from "@coder/logger";
|
||||
import { escapePath } from "@coder/protocol";
|
||||
import { retry } from "./retry";
|
||||
import { InitData } from "@coder/protocol";
|
||||
import { retry, Retry } from "./retry";
|
||||
import { client } from "./fill/client";
|
||||
import { Clipboard, clipboard } from "./fill/clipboard";
|
||||
|
||||
export interface IURI {
|
||||
|
||||
readonly path: string;
|
||||
readonly fsPath: string;
|
||||
readonly scheme: string;
|
||||
|
||||
}
|
||||
|
||||
export interface IURIFactory {
|
||||
|
||||
/**
|
||||
* Convert the object to an instance of a real URI.
|
||||
*/
|
||||
create<T extends IURI>(uri: IURI): T;
|
||||
file(path: string): IURI;
|
||||
parse(raw: string): IURI;
|
||||
|
||||
export interface IClientOptions {
|
||||
mkDirs?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -14,33 +29,81 @@ export interface IClientOptions {
|
||||
* Everything the client provides is asynchronous so you can wait on what
|
||||
* you need from it without blocking anything else.
|
||||
*
|
||||
* It also provides task management to help asynchronously load and time
|
||||
* external code.
|
||||
* It also provides task management to help asynchronously load and time code.
|
||||
*/
|
||||
export class Client {
|
||||
export abstract class Client {
|
||||
|
||||
public readonly mkDirs: Promise<void>;
|
||||
public readonly retry: Retry = retry;
|
||||
public readonly clipboard: Clipboard = clipboard;
|
||||
public readonly uriFactory: IURIFactory;
|
||||
private start: Time | undefined;
|
||||
private readonly progressElement: HTMLElement | undefined;
|
||||
private tasks: string[];
|
||||
private finishedTaskCount: number;
|
||||
private tasks: string[] = [];
|
||||
private finishedTaskCount = 0;
|
||||
private readonly loadTime: Time;
|
||||
|
||||
public constructor() {
|
||||
logger.info("Loading IDE");
|
||||
|
||||
this.loadTime = time(2500);
|
||||
|
||||
const overlay = document.getElementById("overlay");
|
||||
const logo = document.getElementById("logo");
|
||||
const msgElement = overlay
|
||||
? overlay.querySelector(".message") as HTMLElement
|
||||
: undefined;
|
||||
|
||||
if (overlay && logo) {
|
||||
overlay.addEventListener("mousemove", (event) => {
|
||||
const xPos = ((event.clientX - logo.offsetLeft) / 24).toFixed(2);
|
||||
const yPos = ((logo.offsetTop - event.clientY) / 24).toFixed(2);
|
||||
|
||||
logo.style.transform = `perspective(200px) rotateX(${yPos}deg) rotateY(${xPos}deg)`;
|
||||
});
|
||||
}
|
||||
|
||||
public constructor(options: IClientOptions) {
|
||||
this.tasks = [];
|
||||
this.finishedTaskCount = 0;
|
||||
this.progressElement = typeof document !== "undefined"
|
||||
? document.querySelector("#fill") as HTMLElement
|
||||
: undefined;
|
||||
|
||||
this.mkDirs = this.wrapTask("Creating directories", 100, async () => {
|
||||
if (options.mkDirs && options.mkDirs.length > 0) {
|
||||
await promisify(exec)(`mkdir -p ${options.mkDirs.map(escapePath).join(" ")}`);
|
||||
}
|
||||
require("path").posix = require("path");
|
||||
|
||||
window.addEventListener("contextmenu", (event) => {
|
||||
event.preventDefault();
|
||||
});
|
||||
|
||||
// Prevent Firefox from trying to reconnect when the page unloads.
|
||||
window.addEventListener("unload", () => {
|
||||
retry.block();
|
||||
this.retry.block();
|
||||
logger.info("Unloaded");
|
||||
});
|
||||
|
||||
this.uriFactory = this.createUriFactory();
|
||||
|
||||
this.initialize().then(() => {
|
||||
if (overlay) {
|
||||
overlay.style.opacity = "0";
|
||||
overlay.addEventListener("transitionend", () => {
|
||||
overlay.remove();
|
||||
});
|
||||
}
|
||||
logger.info("Load completed", field("duration", this.loadTime));
|
||||
}).catch((error) => {
|
||||
logger.error(error.message);
|
||||
if (overlay) {
|
||||
overlay.classList.add("error");
|
||||
}
|
||||
if (msgElement) {
|
||||
const button = document.createElement("div");
|
||||
button.className = "reload-button";
|
||||
button.innerText = "Reload";
|
||||
button.addEventListener("click", () => {
|
||||
location.reload();
|
||||
});
|
||||
msgElement.innerText = `Failed to load: ${error.message}.`;
|
||||
msgElement.parentElement!.appendChild(button);
|
||||
}
|
||||
logger.warn("Load completed with errors", field("duration", this.loadTime));
|
||||
});
|
||||
}
|
||||
|
||||
@ -48,14 +111,14 @@ export class Client {
|
||||
* Wrap a task in some logging, timing, and progress updates. Can optionally
|
||||
* wait on other tasks which won't count towards this task's time.
|
||||
*/
|
||||
public async wrapTask<T>(description: string, duration: number, task: () => Promise<T>): Promise<T>;
|
||||
public async wrapTask<T, V>(description: string, duration: number, task: (v: V) => Promise<T>, t: Promise<V>): Promise<T>;
|
||||
public async wrapTask<T, V1, V2>(description: string, duration: number, task: (v1: V1, v2: V2) => Promise<T>, t1: Promise<V1>, t2: Promise<V2>): Promise<T>;
|
||||
public async wrapTask<T, V1, V2, V3>(description: string, duration: number, task: (v1: V1, v2: V2, v3: V3) => Promise<T>, t1: Promise<V1>, t2: Promise<V2>, t3: Promise<V3>): Promise<T>;
|
||||
public async wrapTask<T, V1, V2, V3, V4>(description: string, duration: number, task: (v1: V1, v2: V2, v3: V3, v4: V4) => Promise<T>, t1: Promise<V1>, t2: Promise<V2>, t3: Promise<V3>, t4: Promise<V4>): Promise<T>;
|
||||
public async wrapTask<T, V1, V2, V3, V4, V5>(description: string, duration: number, task: (v1: V1, v2: V2, v3: V3, v4: V4, v5: V5) => Promise<T>, t1: Promise<V1>, t2: Promise<V2>, t3: Promise<V3>, t4: Promise<V4>, t5: Promise<V5>): Promise<T>;
|
||||
public async wrapTask<T, V1, V2, V3, V4, V5, V6>(description: string, duration: number, task: (v1: V1, v2: V2, v3: V3, v4: V4, v5: V5, v6: V6) => Promise<T>, t1: Promise<V1>, t2: Promise<V2>, t3: Promise<V3>, t4: Promise<V4>, t5: Promise<V5>, t6: Promise<V6>): Promise<T>;
|
||||
public async wrapTask<T>(
|
||||
public async task<T>(description: string, duration: number, task: () => Promise<T>): Promise<T>;
|
||||
public async task<T, V>(description: string, duration: number, task: (v: V) => Promise<T>, t: Promise<V>): Promise<T>;
|
||||
public async task<T, V1, V2>(description: string, duration: number, task: (v1: V1, v2: V2) => Promise<T>, t1: Promise<V1>, t2: Promise<V2>): Promise<T>;
|
||||
public async task<T, V1, V2, V3>(description: string, duration: number, task: (v1: V1, v2: V2, v3: V3) => Promise<T>, t1: Promise<V1>, t2: Promise<V2>, t3: Promise<V3>): Promise<T>;
|
||||
public async task<T, V1, V2, V3, V4>(description: string, duration: number, task: (v1: V1, v2: V2, v3: V3, v4: V4) => Promise<T>, t1: Promise<V1>, t2: Promise<V2>, t3: Promise<V3>, t4: Promise<V4>): Promise<T>;
|
||||
public async task<T, V1, V2, V3, V4, V5>(description: string, duration: number, task: (v1: V1, v2: V2, v3: V3, v4: V4, v5: V5) => Promise<T>, t1: Promise<V1>, t2: Promise<V2>, t3: Promise<V3>, t4: Promise<V4>, t5: Promise<V5>): Promise<T>;
|
||||
public async task<T, V1, V2, V3, V4, V5, V6>(description: string, duration: number, task: (v1: V1, v2: V2, v3: V3, v4: V4, v5: V5, v6: V6) => Promise<T>, t1: Promise<V1>, t2: Promise<V2>, t3: Promise<V3>, t4: Promise<V4>, t5: Promise<V5>, t6: Promise<V6>): Promise<T>;
|
||||
public async task<T>(
|
||||
description: string, duration: number = 100, task: (...args: any[]) => Promise<T>, ...after: Array<Promise<any>> // tslint:disable-line no-any
|
||||
): Promise<T> {
|
||||
this.tasks.push(description);
|
||||
@ -97,4 +160,21 @@ export class Client {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A promise that resolves with initialization data.
|
||||
*/
|
||||
public get initData(): Promise<InitData> {
|
||||
return client.initData;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the IDE.
|
||||
*/
|
||||
protected abstract initialize(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Create URI factory.
|
||||
*/
|
||||
protected abstract createUriFactory(): IURIFactory;
|
||||
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Emitter } from "@coder/events";
|
||||
import { logger, field } from "@coder/logger";
|
||||
import { field, logger } from "@coder/logger";
|
||||
import { Client, ReadWriteConnection } from "@coder/protocol";
|
||||
import { retry } from "../retry";
|
||||
|
||||
@ -10,27 +10,20 @@ import { retry } from "../retry";
|
||||
class Connection implements ReadWriteConnection {
|
||||
|
||||
private activeSocket: WebSocket | undefined;
|
||||
private readonly messageEmitter: Emitter<Uint8Array>;
|
||||
private readonly closeEmitter: Emitter<void>;
|
||||
private readonly upEmitter: Emitter<void>;
|
||||
private readonly downEmitter: Emitter<void>;
|
||||
private readonly messageBuffer: Uint8Array[];
|
||||
private readonly messageEmitter: Emitter<Uint8Array> = new Emitter();
|
||||
private readonly closeEmitter: Emitter<void> = new Emitter();
|
||||
private readonly upEmitter: Emitter<void> = new Emitter();
|
||||
private readonly downEmitter: Emitter<void> = new Emitter();
|
||||
private readonly messageBuffer: Uint8Array[] = [];
|
||||
private socketTimeoutDelay = 60 * 1000;
|
||||
private retryName = "Web socket";
|
||||
private isUp: boolean | undefined;
|
||||
private closed: boolean | undefined;
|
||||
private isUp: boolean = false;
|
||||
private closed: boolean = false;
|
||||
|
||||
public constructor() {
|
||||
this.messageEmitter = new Emitter();
|
||||
this.closeEmitter = new Emitter();
|
||||
this.upEmitter = new Emitter();
|
||||
this.downEmitter = new Emitter();
|
||||
this.messageBuffer = [];
|
||||
retry.register(this.retryName, () => this.connect());
|
||||
this.connect().catch(() => {
|
||||
retry.block(this.retryName);
|
||||
retry.run(this.retryName);
|
||||
});
|
||||
retry.block(this.retryName);
|
||||
retry.run(this.retryName);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -72,7 +65,7 @@ class Connection implements ReadWriteConnection {
|
||||
this.closeEmitter.emit();
|
||||
}
|
||||
|
||||
/**
|
||||
/**
|
||||
* Connect to the server.
|
||||
*/
|
||||
private async connect(): Promise<void> {
|
||||
@ -116,7 +109,7 @@ class Connection implements ReadWriteConnection {
|
||||
private async openSocket(): Promise<WebSocket> {
|
||||
this.dispose();
|
||||
const socket = new WebSocket(
|
||||
`${location.protocol === "https" ? "wss" : "ws"}://${location.host}/websocket`,
|
||||
`${location.protocol === "https" ? "wss" : "ws"}://${location.host}`,
|
||||
);
|
||||
socket.binaryType = "arraybuffer";
|
||||
this.activeSocket = socket;
|
||||
@ -153,7 +146,5 @@ class Connection implements ReadWriteConnection {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* A client for proxying Node APIs based on web sockets.
|
||||
*/
|
||||
// Global instance so all fills can use the same client.
|
||||
export const client = new Client(new Connection());
|
||||
|
132
packages/ide/src/fill/clipboard.ts
Normal file
132
packages/ide/src/fill/clipboard.ts
Normal file
@ -0,0 +1,132 @@
|
||||
import { IDisposable } from "@coder/disposable";
|
||||
import { Emitter } from "@coder/events";
|
||||
|
||||
/**
|
||||
* Native clipboard.
|
||||
*/
|
||||
export class Clipboard {
|
||||
|
||||
private readonly enableEmitter: Emitter<boolean> = new Emitter();
|
||||
private _isEnabled: boolean = false;
|
||||
|
||||
/**
|
||||
* Ask for permission to use the clipboard.
|
||||
*/
|
||||
public initialize(): void {
|
||||
// tslint:disable no-any
|
||||
const navigatorClip = (navigator as any).clipboard;
|
||||
const navigatorPerms = (navigator as any).permissions;
|
||||
// tslint:enable no-any
|
||||
if (navigatorClip && navigatorPerms) {
|
||||
navigatorPerms.query({
|
||||
name: "clipboard-read",
|
||||
}).then((permissionStatus: {
|
||||
onchange: () => void,
|
||||
state: "denied" | "granted" | "prompt",
|
||||
}) => {
|
||||
const updateStatus = (): void => {
|
||||
this._isEnabled = permissionStatus.state !== "denied";
|
||||
this.enableEmitter.emit(this.isEnabled);
|
||||
};
|
||||
updateStatus();
|
||||
permissionStatus.onchange = (): void => {
|
||||
updateStatus();
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the native clipboard is supported.
|
||||
*/
|
||||
public get isSupported(): boolean {
|
||||
// tslint:disable no-any
|
||||
return typeof navigator !== "undefined"
|
||||
&& typeof (navigator as any).clipboard !== "undefined"
|
||||
&& typeof (navigator as any).clipboard.readText !== "undefined";
|
||||
// tslint:enable no-any
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a function to be called when the native clipboard is
|
||||
* enabled/disabled.
|
||||
*/
|
||||
public onPermissionChange(cb: (enabled: boolean) => void): IDisposable {
|
||||
return this.enableEmitter.event(cb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read text from the clipboard.
|
||||
*/
|
||||
public readText(): Promise<string> {
|
||||
return this.instance ? this.instance.readText() : Promise.resolve("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Write text to the clipboard.
|
||||
*/
|
||||
public writeText(value: string): Promise<void> {
|
||||
return this.instance
|
||||
? this.instance.writeText(value)
|
||||
: this.writeTextFallback(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the clipboard is currently enabled.
|
||||
*/
|
||||
public get isEnabled(): boolean {
|
||||
return !!this._isEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return clipboard instance if there is one.
|
||||
*/
|
||||
private get instance(): ({
|
||||
readText(): Promise<string>;
|
||||
writeText(value: string): Promise<void>;
|
||||
}) | undefined {
|
||||
// tslint:disable-next-line no-any
|
||||
return this.isSupported ? (navigator as any).clipboard : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback for writing text to the clipboard.
|
||||
* Taken from https://hackernoon.com/copying-text-to-clipboard-with-javascript-df4d4988697f
|
||||
*/
|
||||
private writeTextFallback(value: string): Promise<void> {
|
||||
// Note the current focus and selection.
|
||||
const active = document.activeElement as HTMLElement;
|
||||
const selection = document.getSelection();
|
||||
const selected = selection && selection.rangeCount > 0
|
||||
? selection.getRangeAt(0)
|
||||
: false;
|
||||
|
||||
// Insert a hidden textarea to put the text to copy in.
|
||||
const el = document.createElement("textarea");
|
||||
el.value = value;
|
||||
el.setAttribute("readonly", "");
|
||||
el.style.position = "absolute";
|
||||
el.style.left = "-9999px";
|
||||
document.body.appendChild(el);
|
||||
|
||||
// Select the textarea and execute a copy (this will only work as part of a
|
||||
// user interaction).
|
||||
el.select();
|
||||
document.execCommand("copy");
|
||||
|
||||
// Remove the textarea and put focus and selection back to where it was
|
||||
// previously.
|
||||
document.body.removeChild(el);
|
||||
active.focus();
|
||||
if (selected && selection) {
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(selected);
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Global clipboard instance since it's used in the Electron fill.
|
||||
export const clipboard = new Clipboard();
|
@ -131,7 +131,7 @@ export class Dialog {
|
||||
/**
|
||||
* Display or remove an error.
|
||||
*/
|
||||
public set error(error: string) {
|
||||
public set error(error: string | undefined) {
|
||||
while (this.errors.lastChild) {
|
||||
this.errors.removeChild(this.errors.lastChild);
|
||||
}
|
||||
|
@ -1,352 +1,388 @@
|
||||
// import * as electron from "electron";
|
||||
// import { EventEmitter } from "events";
|
||||
// import * as fs from "fs";
|
||||
// import { getFetchUrl } from "../src/coder/api";
|
||||
// import { escapePath } from "../src/coder/common";
|
||||
// import { wush } from "../src/coder/server";
|
||||
// import { IKey, Dialog } from "./dialog";
|
||||
/// <reference path="../../../../lib/vscode/src/typings/electron.d.ts" />
|
||||
import { exec } from "child_process";
|
||||
import { EventEmitter } from "events";
|
||||
import * as fs from "fs";
|
||||
import { promisify } from "util";
|
||||
import { logger, field } from "@coder/logger";
|
||||
import { escapePath } from "@coder/protocol";
|
||||
import { IKey, Dialog as DialogBox } from "./dialog";
|
||||
import { clipboard } from "./clipboard";
|
||||
|
||||
// (global as any).getOpenUrls = () => {
|
||||
// return [];
|
||||
// };
|
||||
// tslint:disable-next-line no-any
|
||||
(global as any).getOpenUrls = (): string[] => {
|
||||
return [];
|
||||
};
|
||||
|
||||
// const oldCreateElement = document.createElement;
|
||||
if (typeof document === "undefined") {
|
||||
(<any>global).document = {} as any;
|
||||
}
|
||||
|
||||
// document.createElement = (tagName: string) => {
|
||||
// const createElement = (tagName: string) => {
|
||||
// return oldCreateElement.call(document, tagName);
|
||||
// };
|
||||
const oldCreateElement: <K extends keyof HTMLElementTagNameMap>(
|
||||
tagName: K, options?: ElementCreationOptions,
|
||||
) => HTMLElementTagNameMap[K] = document.createElement;
|
||||
|
||||
// if (tagName === "webview") {
|
||||
// const view = createElement("iframe") as HTMLIFrameElement;
|
||||
// view.style.border = "0px";
|
||||
// const frameID = Math.random().toString();
|
||||
// view.addEventListener("error", (event) => {
|
||||
// console.log("Got iframe error", event.error, event.message);
|
||||
// });
|
||||
// window.addEventListener("message", (event) => {
|
||||
// if (!event.data || !event.data.id) {
|
||||
// return;
|
||||
// }
|
||||
// if (event.data.id !== frameID) {
|
||||
// return;
|
||||
// }
|
||||
// const e = new CustomEvent("ipc-message");
|
||||
// (e as any).channel = event.data.channel;
|
||||
// (e as any).args = event.data.data;
|
||||
// view.dispatchEvent(e);
|
||||
// });
|
||||
// view.sandbox.add("allow-same-origin", "allow-scripts", "allow-popups", "allow-forms");
|
||||
// Object.defineProperty(view, "preload", {
|
||||
// set: (url: string) => {
|
||||
// view.onload = () => {
|
||||
// view.contentDocument.body.id = frameID;
|
||||
// view.contentDocument.body.parentElement.style.overflow = "hidden";
|
||||
// const script = document.createElement("script");
|
||||
// script.src = url;
|
||||
// view.contentDocument.head.appendChild(script);
|
||||
// };
|
||||
// },
|
||||
// });
|
||||
// (view as any).getWebContents = () => undefined;
|
||||
// (view as any).send = (channel: string, ...args) => {
|
||||
// if (args[0] && typeof args[0] === "object" && args[0].contents) {
|
||||
// args[0].contents = (args[0].contents as string).replace(/"(file:\/\/[^"]*)"/g, (m) => `"${getFetchUrl(m)}"`);
|
||||
// args[0].contents = (args[0].contents as string).replace(/"vscode-resource:([^"]*)"/g, (m) => `"${getFetchUrl(m)}"`);
|
||||
// }
|
||||
// view.contentWindow.postMessage({
|
||||
// channel,
|
||||
// data: args,
|
||||
// id: frameID,
|
||||
// }, "*");
|
||||
// };
|
||||
// return view;
|
||||
// }
|
||||
const newCreateElement = <K extends keyof HTMLElementTagNameMap>(tagName: K): HTMLElementTagNameMap[K] => {
|
||||
const createElement = <K extends keyof HTMLElementTagNameMap>(tagName: K): HTMLElementTagNameMap[K] => {
|
||||
return oldCreateElement.call(document, tagName);
|
||||
};
|
||||
|
||||
// return createElement(tagName);
|
||||
// };
|
||||
if (tagName === "webview") {
|
||||
const view = createElement("iframe") as HTMLIFrameElement;
|
||||
view.style.border = "0px";
|
||||
const frameID = Math.random().toString();
|
||||
view.addEventListener("error", (event) => {
|
||||
logger.error("iframe error", field("event", event));
|
||||
});
|
||||
window.addEventListener("message", (event) => {
|
||||
if (!event.data || !event.data.id) {
|
||||
return;
|
||||
}
|
||||
if (event.data.id !== frameID) {
|
||||
return;
|
||||
}
|
||||
const e = new CustomEvent("ipc-message");
|
||||
(e as any).channel = event.data.channel; // tslint:disable-line no-any
|
||||
(e as any).args = event.data.data; // tslint:disable-line no-any
|
||||
view.dispatchEvent(e);
|
||||
});
|
||||
view.sandbox.add("allow-same-origin", "allow-scripts", "allow-popups", "allow-forms");
|
||||
Object.defineProperty(view, "preload", {
|
||||
set: (url: string): void => {
|
||||
view.onload = (): void => {
|
||||
if (view.contentDocument) {
|
||||
view.contentDocument.body.id = frameID;
|
||||
view.contentDocument.body.parentElement!.style.overflow = "hidden";
|
||||
const script = document.createElement("script");
|
||||
script.src = url;
|
||||
view.contentDocument.head.appendChild(script);
|
||||
}
|
||||
};
|
||||
},
|
||||
});
|
||||
(view as any).getWebContents = (): void => undefined; // tslint:disable-line no-any
|
||||
(view as any).send = (channel: string, ...args: any[]): void => { // tslint:disable-line no-any
|
||||
if (args[0] && typeof args[0] === "object" && args[0].contents) {
|
||||
// TODO
|
||||
// args[0].contents = (args[0].contents as string).replace(/"(file:\/\/[^"]*)"/g, (m) => `"${getFetchUrl(m)}"`);
|
||||
// args[0].contents = (args[0].contents as string).replace(/"vscode-resource:([^"]*)"/g, (m) => `"${getFetchUrl(m)}"`);
|
||||
}
|
||||
if (view.contentWindow) {
|
||||
view.contentWindow.postMessage({
|
||||
channel,
|
||||
data: args,
|
||||
id: frameID,
|
||||
}, "*");
|
||||
}
|
||||
};
|
||||
|
||||
// const rendererToMainEmitter = new EventEmitter();
|
||||
// const mainToRendererEmitter = new EventEmitter();
|
||||
return view;
|
||||
}
|
||||
|
||||
// module.exports = {
|
||||
// clipboard: {
|
||||
// has: () => {
|
||||
// return false;
|
||||
// },
|
||||
// writeText: (value: string) => {
|
||||
// // Taken from https://hackernoon.com/copying-text-to-clipboard-with-javascript-df4d4988697f
|
||||
// const active = document.activeElement as HTMLElement;
|
||||
// const el = document.createElement('textarea'); // Create a <textarea> element
|
||||
// el.value = value; // Set its value to the string that you want copied
|
||||
// el.setAttribute('readonly', ''); // Make it readonly to be tamper-proof
|
||||
// el.style.position = 'absolute';
|
||||
// el.style.left = '-9999px'; // Move outside the screen to make it invisible
|
||||
// document.body.appendChild(el); // Append the <textarea> element to the HTML document
|
||||
// const selected =
|
||||
// document.getSelection().rangeCount > 0 // Check if there is any content selected previously
|
||||
// ? document.getSelection().getRangeAt(0) // Store selection if found
|
||||
// : false; // Mark as false to know no selection existed before
|
||||
// el.select(); // Select the <textarea> content
|
||||
// document.execCommand('copy'); // Copy - only works as a result of a user action (e.g. click events)
|
||||
// document.body.removeChild(el); // Remove the <textarea> element
|
||||
// if (selected) { // If a selection existed before copying
|
||||
// document.getSelection().removeAllRanges(); // Unselect everything on the HTML document
|
||||
// document.getSelection().addRange(selected); // Restore the original selection
|
||||
// }
|
||||
// active.focus();
|
||||
// },
|
||||
// },
|
||||
// dialog: {
|
||||
// showSaveDialog: (_: void, options: Electron.SaveDialogOptions, callback: (filename: string) => void): void => {
|
||||
// const defaultPath = options.defaultPath || "/untitled";
|
||||
// const fileIndex = defaultPath.lastIndexOf("/");
|
||||
// const extensionIndex = defaultPath.lastIndexOf(".");
|
||||
// const saveDialogOptions = {
|
||||
// buttons: ["Cancel", "Save"],
|
||||
// detail: "Enter a path for this file",
|
||||
// input: {
|
||||
// value: defaultPath,
|
||||
// selection: {
|
||||
// start: fileIndex === -1 ? 0 : fileIndex + 1,
|
||||
// end: extensionIndex === -1 ? defaultPath.length : extensionIndex,
|
||||
// },
|
||||
// },
|
||||
// message: "Save file",
|
||||
// };
|
||||
return createElement(tagName);
|
||||
};
|
||||
|
||||
// const dialog = new Dialog(saveDialogOptions);
|
||||
// dialog.onAction((action) => {
|
||||
// if (action.key !== IKey.Enter && action.buttonIndex !== 1) {
|
||||
// dialog.hide();
|
||||
// return callback(undefined);
|
||||
// }
|
||||
document.createElement = newCreateElement;
|
||||
|
||||
// const filePath = dialog.inputValue.replace(/\/+$/, "");
|
||||
// const split = filePath.split("/");
|
||||
// const fileName = split.pop();
|
||||
// const parentName = split.pop() || "/";
|
||||
// if (fileName === "") {
|
||||
// dialog.error = "You must enter a file name.";
|
||||
// return;
|
||||
// }
|
||||
class Clipboard {
|
||||
|
||||
// fs.stat(filePath, (error, stats) => {
|
||||
// if (error && error.code === "ENOENT") {
|
||||
// dialog.hide();
|
||||
// callback(filePath);
|
||||
// } else if (error) {
|
||||
// dialog.error = error.message;
|
||||
// } else if (stats.isDirectory()) {
|
||||
// dialog.error = `A directory named "${fileName}" already exists.`;
|
||||
// } else {
|
||||
// dialog.error = undefined;
|
||||
public has(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// const confirmDialog = new Dialog({
|
||||
// message: `A file named "${fileName}" already exists. Do you want to replace it?`,
|
||||
// detail: `The file already exists in "${parentName}". Replacing it will overwrite its contents.`,
|
||||
// buttons: ["Cancel", "Replace"],
|
||||
// });
|
||||
public writeText(value: string): Promise<void> {
|
||||
return clipboard.writeText(value);
|
||||
}
|
||||
|
||||
// confirmDialog.onAction((action) => {
|
||||
// if (action.buttonIndex === 1) {
|
||||
// confirmDialog.hide();
|
||||
// return callback(filePath);
|
||||
// }
|
||||
}
|
||||
|
||||
// confirmDialog.hide();
|
||||
// dialog.show();
|
||||
// });
|
||||
class Shell {
|
||||
|
||||
// dialog.hide();
|
||||
// confirmDialog.show();
|
||||
// }
|
||||
// });
|
||||
// });
|
||||
// dialog.show();
|
||||
// },
|
||||
// showOpenDialog: () => {
|
||||
// console.log("Trying to show the open dialog");
|
||||
// },
|
||||
// showMessageBox: (_: void, options: Electron.MessageBoxOptions, callback: (button: number, checked: boolean) => void): void => {
|
||||
// const dialog = new Dialog(options);
|
||||
// dialog.onAction((action) => {
|
||||
// dialog.hide();
|
||||
// callback(action.buttonIndex, false);
|
||||
// });
|
||||
// dialog.show();
|
||||
// },
|
||||
// },
|
||||
// remote: {
|
||||
// dialog: {
|
||||
// showOpenDialog: () => {
|
||||
// console.log("Trying to remotely open");
|
||||
// },
|
||||
// },
|
||||
// },
|
||||
// webFrame: {
|
||||
// getZoomFactor: () => {
|
||||
// return 1;
|
||||
// },
|
||||
// getZoomLevel: () => {
|
||||
// return 1;
|
||||
// },
|
||||
// setZoomLevel: () => {
|
||||
// return;
|
||||
// },
|
||||
// },
|
||||
// screen: {
|
||||
// getAllDisplays: () => {
|
||||
// return [{
|
||||
// bounds: {
|
||||
// x: 1000,
|
||||
// y: 1000,
|
||||
// },
|
||||
// }];
|
||||
// },
|
||||
// },
|
||||
// app: {
|
||||
// isAccessibilitySupportEnabled: () => {
|
||||
// return false;
|
||||
// },
|
||||
// setAsDefaultProtocolClient: () => {
|
||||
public async moveItemToTrash(path: string): Promise<void> {
|
||||
await promisify(exec)(
|
||||
`trash-put --trash-dir ${escapePath("~/.Trash")} ${escapePath(path)}`,
|
||||
);
|
||||
}
|
||||
|
||||
// },
|
||||
// send: (str) => {
|
||||
// console.log("APP Trying to send", str);
|
||||
// //
|
||||
// },
|
||||
// on: () => {
|
||||
// //
|
||||
// },
|
||||
// once: () => {
|
||||
// //
|
||||
// },
|
||||
// },
|
||||
// // ipcRenderer communicates with ipcMain
|
||||
// ipcRenderer: {
|
||||
// send: (str, ...args) => {
|
||||
// rendererToMainEmitter.emit(str, {
|
||||
// sender: module.exports.ipcMain,
|
||||
// }, ...args);
|
||||
// },
|
||||
// on: (str, listener) => {
|
||||
// mainToRendererEmitter.on(str, listener);
|
||||
// },
|
||||
// once: (str, listener) => {
|
||||
// mainToRendererEmitter.once(str, listener);
|
||||
// },
|
||||
// removeListener: (str, listener) => {
|
||||
// mainToRendererEmitter.removeListener(str, listener);
|
||||
// },
|
||||
// },
|
||||
// ipcMain: {
|
||||
// send: (str, ...args) => {
|
||||
// mainToRendererEmitter.emit(str, {
|
||||
// sender: module.exports.ipcRenderer,
|
||||
// }, ...args);
|
||||
// },
|
||||
// on: (str, listener) => {
|
||||
// rendererToMainEmitter.on(str, listener);
|
||||
// },
|
||||
// once: (str, listener) => {
|
||||
// rendererToMainEmitter.once(str, listener);
|
||||
// },
|
||||
// },
|
||||
// shell: {
|
||||
// moveItemToTrash: async (path) => {
|
||||
// const response = await wush.execute({
|
||||
// command: `trash-put --trash-dir ${escapePath("~/.Trash")} ${escapePath(path)}`,
|
||||
// }).done();
|
||||
// return response.wasSuccessful();
|
||||
// },
|
||||
// },
|
||||
// BrowserWindow: class {
|
||||
}
|
||||
|
||||
// public webContents = {
|
||||
// on: () => {
|
||||
class App extends EventEmitter {
|
||||
|
||||
// },
|
||||
// session: {
|
||||
// webRequest: {
|
||||
// onBeforeRequest: () => {
|
||||
public isAccessibilitySupportEnabled(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
// },
|
||||
public setAsDefaultProtocolClient(): void {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
// onBeforeSendHeaders: () => {
|
||||
}
|
||||
|
||||
// },
|
||||
class Dialog {
|
||||
|
||||
// onHeadersReceived: () => {
|
||||
public showSaveDialog(_: void, options: Electron.SaveDialogOptions, callback: (filename: string | undefined) => void): void {
|
||||
const defaultPath = options.defaultPath || "/untitled";
|
||||
const fileIndex = defaultPath.lastIndexOf("/");
|
||||
const extensionIndex = defaultPath.lastIndexOf(".");
|
||||
const saveDialogOptions = {
|
||||
buttons: ["Cancel", "Save"],
|
||||
detail: "Enter a path for this file",
|
||||
input: {
|
||||
value: defaultPath,
|
||||
selection: {
|
||||
start: fileIndex === -1 ? 0 : fileIndex + 1,
|
||||
end: extensionIndex === -1 ? defaultPath.length : extensionIndex,
|
||||
},
|
||||
},
|
||||
message: "Save file",
|
||||
};
|
||||
|
||||
// },
|
||||
// }
|
||||
// },
|
||||
// removeAllListeners: () => {
|
||||
const dialog = new DialogBox(saveDialogOptions);
|
||||
dialog.onAction((action) => {
|
||||
if (action.key !== IKey.Enter && action.buttonIndex !== 1) {
|
||||
dialog.hide();
|
||||
|
||||
// },
|
||||
// }
|
||||
return callback(undefined);
|
||||
}
|
||||
|
||||
// public static getFocusedWindow() {
|
||||
// return undefined;
|
||||
// }
|
||||
const inputValue = dialog.inputValue || "";
|
||||
const filePath = inputValue.replace(/\/+$/, "");
|
||||
const split = filePath.split("/");
|
||||
const fileName = split.pop();
|
||||
const parentName = split.pop() || "/";
|
||||
if (fileName === "") {
|
||||
dialog.error = "You must enter a file name.";
|
||||
|
||||
// public isMaximized() {
|
||||
// return false;
|
||||
// }
|
||||
return;
|
||||
}
|
||||
|
||||
// public isFullScreen() {
|
||||
// return false;
|
||||
// }
|
||||
fs.stat(filePath, (error, stats) => {
|
||||
if (error && error.code === "ENOENT") {
|
||||
dialog.hide();
|
||||
callback(filePath);
|
||||
} else if (error) {
|
||||
dialog.error = error.message;
|
||||
} else if (stats.isDirectory()) {
|
||||
dialog.error = `A directory named "${fileName}" already exists.`;
|
||||
} else {
|
||||
dialog.error = undefined;
|
||||
|
||||
// public setMenuBarVisibility(visibility) {
|
||||
// console.log("We are setting the menu bar to ", visibility);
|
||||
// }
|
||||
const confirmDialog = new DialogBox({
|
||||
message: `A file named "${fileName}" already exists. Do you want to replace it?`,
|
||||
detail: `The file already exists in "${parentName}". Replacing it will overwrite its contents.`,
|
||||
buttons: ["Cancel", "Replace"],
|
||||
});
|
||||
|
||||
// public setAutoHideMenuBar() {
|
||||
confirmDialog.onAction((action) => {
|
||||
if (action.buttonIndex === 1) {
|
||||
confirmDialog.hide();
|
||||
|
||||
// }
|
||||
return callback(filePath);
|
||||
}
|
||||
|
||||
// public on() {
|
||||
confirmDialog.hide();
|
||||
dialog.show();
|
||||
});
|
||||
|
||||
// }
|
||||
dialog.hide();
|
||||
confirmDialog.show();
|
||||
}
|
||||
});
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
// public setTitle(value: string): void {
|
||||
// document.title = value;
|
||||
// }
|
||||
// },
|
||||
// toggleFullScreen: () => {
|
||||
// const doc = document as any;
|
||||
// const isInFullScreen = doc.fullscreenElement
|
||||
// || doc.webkitFullscreenElement
|
||||
// || doc.mozFullScreenElement
|
||||
// || doc.msFullscreenElement;
|
||||
public showOpenDialog(): void {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
// const body = doc.body;
|
||||
// if (!isInFullScreen) {
|
||||
// if (body.requestFullscreen) {
|
||||
// body.requestFullscreen();
|
||||
// } else if (body.mozRequestFullScreen) {
|
||||
// body.mozRequestFullScreen();
|
||||
// } else if (body.webkitRequestFullScreen) {
|
||||
// body.webkitRequestFullScreen();
|
||||
// } else if (body.msRequestFullscreen) {
|
||||
// body.msRequestFullscreen();
|
||||
// }
|
||||
// } else {
|
||||
// if (doc.exitFullscreen) {
|
||||
// doc.exitFullscreen();
|
||||
// } else if (doc.webkitExitFullscreen) {
|
||||
// doc.webkitExitFullscreen();
|
||||
// } else if (doc.mozCancelFullScreen) {
|
||||
// doc.mozCancelFullScreen();
|
||||
// } else if (doc.msExitFullscreen) {
|
||||
// doc.msExitFullscreen();
|
||||
// }
|
||||
// }
|
||||
// },
|
||||
// focusWindow: () => {
|
||||
// console.log("focusing window");
|
||||
// window.focus();
|
||||
// },
|
||||
// };
|
||||
public showMessageBox(_: void, options: Electron.MessageBoxOptions, callback: (button: number | undefined, checked: boolean) => void): void {
|
||||
const dialog = new DialogBox(options);
|
||||
dialog.onAction((action) => {
|
||||
dialog.hide();
|
||||
callback(action.buttonIndex, false);
|
||||
});
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class WebFrame {
|
||||
|
||||
public getZoomFactor(): number {
|
||||
return 1;
|
||||
}
|
||||
|
||||
public getZoomLevel(): number {
|
||||
return 1;
|
||||
}
|
||||
|
||||
public setZoomLevel(): void {
|
||||
// Nothing.
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Screen {
|
||||
|
||||
public getAllDisplays(): [] {
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class WebRequest extends EventEmitter {
|
||||
|
||||
public onBeforeRequest(): void {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public onBeforeSendHeaders(): void {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public onHeadersReceived(): void {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class Session extends EventEmitter {
|
||||
|
||||
public webRequest = new WebRequest();
|
||||
|
||||
public resolveProxy(url: string, callback: (proxy: string) => void): void {
|
||||
// TODO: not sure what this actually does.
|
||||
callback(url);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class WebContents extends EventEmitter {
|
||||
|
||||
public session = new Session();
|
||||
|
||||
}
|
||||
|
||||
class BrowserWindow extends EventEmitter {
|
||||
|
||||
public webContents = new WebContents();
|
||||
private representedFilename: string = "";
|
||||
|
||||
public static getFocusedWindow(): undefined {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
public focus(): void {
|
||||
window.focus();
|
||||
}
|
||||
|
||||
public show(): void {
|
||||
window.focus();
|
||||
}
|
||||
|
||||
public reload(): void {
|
||||
location.reload();
|
||||
}
|
||||
|
||||
public isMaximized(): boolean {
|
||||
return false;
|
||||
}
|
||||
|
||||
public setFullScreen(fullscreen: boolean): void {
|
||||
if (fullscreen) {
|
||||
document.documentElement.requestFullscreen();
|
||||
} else {
|
||||
document.exitFullscreen();
|
||||
}
|
||||
}
|
||||
|
||||
public isFullScreen(): boolean {
|
||||
return document.fullscreenEnabled;
|
||||
}
|
||||
|
||||
public isFocused(): boolean {
|
||||
return document.hasFocus();
|
||||
}
|
||||
|
||||
public setMenuBarVisibility(): void {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public setAutoHideMenuBar(): void {
|
||||
throw new Error("not implemented");
|
||||
}
|
||||
|
||||
public setRepresentedFilename(filename: string): void {
|
||||
this.representedFilename = filename;
|
||||
}
|
||||
|
||||
public getRepresentedFilename(): string {
|
||||
return this.representedFilename;
|
||||
}
|
||||
|
||||
public setTitle(value: string): void {
|
||||
document.title = value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* We won't be able to do a 1 to 1 fill because things like moveItemToTrash for
|
||||
* example returns a boolean while we need a promise.
|
||||
*/
|
||||
class ElectronFill {
|
||||
|
||||
public readonly shell = new Shell();
|
||||
public readonly clipboard = new Clipboard();
|
||||
public readonly app = new App();
|
||||
public readonly dialog = new Dialog();
|
||||
public readonly webFrame = new WebFrame();
|
||||
public readonly screen = new Screen();
|
||||
|
||||
private readonly rendererToMainEmitter = new EventEmitter();
|
||||
private readonly mainToRendererEmitter = new EventEmitter();
|
||||
|
||||
public get BrowserWindow(): typeof BrowserWindow {
|
||||
return BrowserWindow;
|
||||
}
|
||||
|
||||
// tslint:disable no-any
|
||||
public get ipcRenderer(): object {
|
||||
return {
|
||||
send: (str: string, ...args: any[]): void => {
|
||||
this.rendererToMainEmitter.emit(str, {
|
||||
sender: module.exports.ipcMain,
|
||||
}, ...args);
|
||||
},
|
||||
on: (str: string, listener: (...args: any[]) => void): void => {
|
||||
this.mainToRendererEmitter.on(str, listener);
|
||||
},
|
||||
once: (str: string, listener: (...args: any[]) => void): void => {
|
||||
this.mainToRendererEmitter.once(str, listener);
|
||||
},
|
||||
removeListener: (str: string, listener: (...args: any[]) => void): void => {
|
||||
this.mainToRendererEmitter.removeListener(str, listener);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public get ipcMain(): object {
|
||||
return {
|
||||
send: (str: string, ...args: any[]): void => {
|
||||
this.mainToRendererEmitter.emit(str, {
|
||||
sender: module.exports.ipcRenderer,
|
||||
}, ...args);
|
||||
},
|
||||
on: (str: string, listener: (...args: any[]) => void): void => {
|
||||
this.rendererToMainEmitter.on(str, listener);
|
||||
},
|
||||
once: (str: string, listener: (...args: any[]) => void): void => {
|
||||
this.rendererToMainEmitter.once(str, listener);
|
||||
},
|
||||
};
|
||||
}
|
||||
// tslint:enable no-any
|
||||
|
||||
}
|
||||
|
||||
module.exports = new ElectronFill();
|
||||
|
@ -1 +1 @@
|
||||
module.exports = {};
|
||||
export = {};
|
||||
|
38
packages/ide/src/fill/os.ts
Normal file
38
packages/ide/src/fill/os.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import { InitData } from "@coder/protocol";
|
||||
import { client } from "./client";
|
||||
|
||||
class OS {
|
||||
|
||||
private _homedir: string | undefined;
|
||||
private _tmpdir: string | undefined;
|
||||
|
||||
public constructor() {
|
||||
client.initData.then((data) => {
|
||||
this.initialize(data);
|
||||
});
|
||||
}
|
||||
|
||||
public homedir(): string {
|
||||
if (typeof this._homedir === "undefined") {
|
||||
throw new Error("not initialized");
|
||||
}
|
||||
|
||||
return this._homedir;
|
||||
}
|
||||
|
||||
public tmpdir(): string {
|
||||
if (typeof this._tmpdir === "undefined") {
|
||||
throw new Error("not initialized");
|
||||
}
|
||||
|
||||
return this._tmpdir;
|
||||
}
|
||||
|
||||
public initialize(data: InitData): void {
|
||||
this._homedir = data.homeDirectory;
|
||||
this._tmpdir = data.tmpDirectory;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export = new OS();
|
@ -1,5 +1,4 @@
|
||||
import { implementation as promisify } from "util.promisify";
|
||||
export * from "../../../../node_modules/util";
|
||||
import { implementation } from "util.promisify";
|
||||
|
||||
export {
|
||||
promisify,
|
||||
}
|
||||
export const promisify = implementation;
|
||||
|
@ -1,4 +1,2 @@
|
||||
export * from "./client";
|
||||
export * from "./retry";
|
||||
export * from "./upload";
|
||||
export * from "./uri";
|
||||
|
@ -209,7 +209,7 @@ export class Retry {
|
||||
|
||||
const item = this.items.get(name)!;
|
||||
if (typeof item.timeout === "undefined" && !item.running && typeof item.count !== "undefined") {
|
||||
logger.info(`Recovered connection to ${name.toLowerCase()}`);
|
||||
logger.info(`Connected to ${name.toLowerCase()}`);
|
||||
item.delay = undefined;
|
||||
item.count = undefined;
|
||||
}
|
||||
@ -228,7 +228,7 @@ export class Retry {
|
||||
const retryCountText = item.count <= this.maxImmediateRetries
|
||||
? `[${item.count}/${this.maxImmediateRetries}]`
|
||||
: `[${item.count}]`;
|
||||
logger.info(`Retrying ${name.toLowerCase()} ${retryCountText}...`);
|
||||
logger.info(`Trying ${name.toLowerCase()} ${retryCountText}...`);
|
||||
|
||||
const endItem = (): void => {
|
||||
this.stopItem(item);
|
||||
@ -341,4 +341,6 @@ export class Retry {
|
||||
|
||||
}
|
||||
|
||||
// Global instance so we can block other retries when retrying the main
|
||||
// connection.
|
||||
export const retry = new Retry();
|
||||
|
@ -1,45 +0,0 @@
|
||||
export interface IURI {
|
||||
|
||||
readonly path: string;
|
||||
readonly fsPath: string;
|
||||
readonly scheme: string;
|
||||
|
||||
}
|
||||
|
||||
export interface IURIFactory {
|
||||
|
||||
/**
|
||||
* Convert the object to an instance of a real URI.
|
||||
*/
|
||||
create<T extends IURI>(uri: IURI): T;
|
||||
file(path: string): IURI;
|
||||
parse(raw: string): IURI;
|
||||
|
||||
}
|
||||
|
||||
let activeUriFactory: IURIFactory;
|
||||
|
||||
/**
|
||||
* Get the active URI factory
|
||||
*/
|
||||
export const getFactory = (): IURIFactory => {
|
||||
if (!activeUriFactory) {
|
||||
throw new Error("default uri factory not set");
|
||||
}
|
||||
|
||||
return activeUriFactory;
|
||||
};
|
||||
|
||||
/**
|
||||
* Update the active URI factory.
|
||||
*/
|
||||
export const setUriFactory = (factory: IURIFactory): void => {
|
||||
activeUriFactory = factory;
|
||||
};
|
||||
|
||||
export interface IUriSwitcher {
|
||||
|
||||
strip(uri: IURI): IURI;
|
||||
prepend(uri: IURI): IURI;
|
||||
|
||||
}
|
Reference in New Issue
Block a user