From 72bf4547d42cb434f956ebda0e0564eb7040439c Mon Sep 17 00:00:00 2001 From: Asher Date: Fri, 18 Jan 2019 15:46:40 -0600 Subject: [PATCH] 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 --- .gitignore | 3 +- package.json | 8 +- packages/ide/package.json | 2 +- packages/ide/src/client.ts | 136 +- packages/ide/src/fill/client.ts | 35 +- packages/ide/src/fill/clipboard.ts | 132 + packages/ide/src/fill/dialog.ts | 2 +- packages/ide/src/fill/electron.ts | 674 +- packages/ide/src/fill/empty.ts | 2 +- packages/ide/src/fill/os.ts | 38 + packages/ide/src/fill/util.ts | 7 +- packages/ide/src/index.ts | 2 - packages/ide/src/retry.ts | 6 +- packages/ide/src/uri.ts | 45 - packages/logger/src/logger.ts | 2 + packages/package.json | 1 + packages/protocol/src/browser/client.ts | 80 +- packages/protocol/src/browser/command.ts | 136 +- .../src/browser/modules/child_process.ts | 20 +- packages/protocol/src/browser/modules/fs.ts | 590 +- packages/protocol/src/browser/modules/net.ts | 10 +- packages/protocol/src/browser/modules/util.ts | 8 - packages/protocol/src/common/util.ts | 20 + packages/protocol/src/node/command.ts | 65 +- packages/protocol/src/node/evaluate.ts | 13 +- packages/protocol/src/node/server.ts | 70 +- packages/protocol/src/proto/client.proto | 24 +- packages/protocol/src/proto/client_pb.d.ts | 91 +- packages/protocol/src/proto/client_pb.js | 509 +- packages/protocol/src/proto/command.proto | 29 + packages/protocol/src/proto/command_pb.d.ts | 118 + packages/protocol/src/proto/command_pb.js | 932 ++ packages/protocol/src/proto/index.ts | 1 + packages/protocol/src/proto/vscode.proto | 9 + packages/protocol/src/proto/vscode_pb.d.ts | 33 + packages/protocol/src/proto/vscode_pb.js | 226 + packages/protocol/test/command.test.ts | 60 +- packages/protocol/test/server.test.ts | 4 +- packages/server/.gitignore | 1 + packages/server/package.json | 12 +- packages/server/scripts/nexe.js | 12 +- packages/server/src/cli.ts | 80 +- packages/server/src/ipc.ts | 70 + packages/server/src/server.ts | 51 +- packages/server/src/vscode/bootstrapFork.ts | 29 + packages/server/src/vscode/sharedProcess.ts | 106 + packages/server/webpack.config.js | 8 +- packages/server/yarn.lock | 33 +- packages/vscode/.gitignore | 1 + packages/vscode/package.json | 9 +- packages/vscode/src/client.ts | 133 + packages/vscode/src/fill/amd.ts | 11 + packages/vscode/src/fill/graceful-fs.ts | 1 + packages/vscode/src/fill/native-keymap.ts | 17 +- packages/vscode/src/fill/node-pty.ts | 2 +- packages/vscode/src/fill/package.ts | 2 + packages/vscode/src/fill/paths.ts | 7 + packages/vscode/src/fill/product.ts | 24 + packages/vscode/src/fill/require.ts | 3 + packages/vscode/src/fill/spdlog.ts | 186 + packages/vscode/src/fill/stdioElectron.ts | 22 + packages/vscode/src/fill/storageDatabase.ts | 78 + packages/vscode/src/fill/windowsService.ts | 274 + packages/vscode/src/index.ts | 39 +- packages/vscode/src/protocol.ts | 129 + packages/vscode/src/startup.ts | 151 + packages/vscode/src/storageService.ts | 59 - packages/vscode/src/workbench.ts | 224 - packages/vscode/webpack.config.bootstrap.js | 123 + packages/vscode/yarn.lock | 119 + packages/web/src/index.ts | 47 +- packages/web/tsconfig.json | 8 + packages/web/webpack.common.config.js | 103 + packages/web/webpack.dev.config.js | 13 + scripts/vscode.css.patch | 178 - scripts/vscode.patch | 8259 +---------------- scripts/webpack.general.config.js | 14 +- tsconfig.json | 1 - webpack.config.app.js | 66 - yarn.lock | 32 +- 80 files changed, 5183 insertions(+), 9697 deletions(-) create mode 100644 packages/ide/src/fill/clipboard.ts create mode 100644 packages/ide/src/fill/os.ts delete mode 100644 packages/ide/src/uri.ts delete mode 100644 packages/protocol/src/browser/modules/util.ts create mode 100644 packages/protocol/src/proto/vscode.proto create mode 100644 packages/protocol/src/proto/vscode_pb.d.ts create mode 100644 packages/protocol/src/proto/vscode_pb.js create mode 100644 packages/server/src/ipc.ts create mode 100644 packages/server/src/vscode/bootstrapFork.ts create mode 100644 packages/server/src/vscode/sharedProcess.ts create mode 100644 packages/vscode/.gitignore create mode 100644 packages/vscode/src/client.ts create mode 100644 packages/vscode/src/fill/amd.ts create mode 100644 packages/vscode/src/fill/graceful-fs.ts create mode 100644 packages/vscode/src/fill/package.ts create mode 100644 packages/vscode/src/fill/paths.ts create mode 100644 packages/vscode/src/fill/product.ts create mode 100644 packages/vscode/src/fill/require.ts create mode 100644 packages/vscode/src/fill/spdlog.ts create mode 100644 packages/vscode/src/fill/stdioElectron.ts create mode 100644 packages/vscode/src/fill/storageDatabase.ts create mode 100644 packages/vscode/src/fill/windowsService.ts create mode 100644 packages/vscode/src/protocol.ts create mode 100644 packages/vscode/src/startup.ts delete mode 100644 packages/vscode/src/storageService.ts delete mode 100644 packages/vscode/src/workbench.ts create mode 100644 packages/vscode/webpack.config.bootstrap.js create mode 100644 packages/web/tsconfig.json create mode 100644 packages/web/webpack.common.config.js create mode 100644 packages/web/webpack.dev.config.js delete mode 100644 scripts/vscode.css.patch delete mode 100644 webpack.config.app.js diff --git a/.gitignore b/.gitignore index 3fc94becb..f47eea075 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -lib/vscode +lib/vscode* +lib/VSCode* node_modules dist diff --git a/package.json b/package.json index 6b913ce7e..bb97907e1 100644 --- a/package.json +++ b/package.json @@ -9,9 +9,9 @@ "vscode:clone": "mkdir -p ./lib && test -d ./lib/vscode || git clone https://github.com/Microsoft/vscode/ ./lib/vscode", "vscode:install": "cd ./lib/vscode && git checkout tags/1.30.1 && yarn", "vscode": "npm-run-all vscode:*", - "packages:install": "cd ./packages && yarn && ts-node ../scripts/install-packages.ts", + "packages:install": "cd ./packages && yarn", "postinstall": "npm-run-all --parallel vscode packages:install build:rules", - "start": "webpack-dev-server --hot --config ./webpack.config.app.js", + "start": "cd ./packages/server && yarn start", "test": "cd ./packages && yarn test" }, "devDependencies": { @@ -26,9 +26,9 @@ "mini-css-extract-plugin": "^0.5.0", "node-sass": "^4.11.0", "npm-run-all": "^4.1.5", - "os-browserify": "^0.3.0", "preload-webpack-plugin": "^3.0.0-beta.2", "sass-loader": "^7.1.0", + "string-replace-loader": "^2.1.1", "style-loader": "^0.23.1", "ts-loader": "^5.3.3", "ts-node": "^7.0.1", @@ -39,7 +39,9 @@ "webpack": "^4.28.4", "webpack-bundle-analyzer": "^3.0.3", "webpack-cli": "^3.2.1", + "webpack-dev-middleware": "^3.5.0", "webpack-dev-server": "^3.1.14", + "webpack-hot-middleware": "^2.24.3", "write-file-webpack-plugin": "^4.5.0" }, "dependencies": { diff --git a/packages/ide/package.json b/packages/ide/package.json index 1d36978c9..ac53ecb0a 100644 --- a/packages/ide/package.json +++ b/packages/ide/package.json @@ -1,5 +1,5 @@ { "name": "@coder/ide", "description": "Browser-based IDE client abstraction.", - "main": "src/index.ts" + "main": "src/index.ts" } diff --git a/packages/ide/src/client.ts b/packages/ide/src/client.ts index 81928f0ce..ac48317ef 100644 --- a/packages/ide/src/client.ts +++ b/packages/ide/src/client.ts @@ -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(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; + 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(description: string, duration: number, task: () => Promise): Promise; - public async wrapTask(description: string, duration: number, task: (v: V) => Promise, t: Promise): Promise; - public async wrapTask(description: string, duration: number, task: (v1: V1, v2: V2) => Promise, t1: Promise, t2: Promise): Promise; - public async wrapTask(description: string, duration: number, task: (v1: V1, v2: V2, v3: V3) => Promise, t1: Promise, t2: Promise, t3: Promise): Promise; - public async wrapTask(description: string, duration: number, task: (v1: V1, v2: V2, v3: V3, v4: V4) => Promise, t1: Promise, t2: Promise, t3: Promise, t4: Promise): Promise; - public async wrapTask(description: string, duration: number, task: (v1: V1, v2: V2, v3: V3, v4: V4, v5: V5) => Promise, t1: Promise, t2: Promise, t3: Promise, t4: Promise, t5: Promise): Promise; - public async wrapTask(description: string, duration: number, task: (v1: V1, v2: V2, v3: V3, v4: V4, v5: V5, v6: V6) => Promise, t1: Promise, t2: Promise, t3: Promise, t4: Promise, t5: Promise, t6: Promise): Promise; - public async wrapTask( + public async task(description: string, duration: number, task: () => Promise): Promise; + public async task(description: string, duration: number, task: (v: V) => Promise, t: Promise): Promise; + public async task(description: string, duration: number, task: (v1: V1, v2: V2) => Promise, t1: Promise, t2: Promise): Promise; + public async task(description: string, duration: number, task: (v1: V1, v2: V2, v3: V3) => Promise, t1: Promise, t2: Promise, t3: Promise): Promise; + public async task(description: string, duration: number, task: (v1: V1, v2: V2, v3: V3, v4: V4) => Promise, t1: Promise, t2: Promise, t3: Promise, t4: Promise): Promise; + public async task(description: string, duration: number, task: (v1: V1, v2: V2, v3: V3, v4: V4, v5: V5) => Promise, t1: Promise, t2: Promise, t3: Promise, t4: Promise, t5: Promise): Promise; + public async task(description: string, duration: number, task: (v1: V1, v2: V2, v3: V3, v4: V4, v5: V5, v6: V6) => Promise, t1: Promise, t2: Promise, t3: Promise, t4: Promise, t5: Promise, t6: Promise): Promise; + public async task( description: string, duration: number = 100, task: (...args: any[]) => Promise, ...after: Array> // tslint:disable-line no-any ): Promise { this.tasks.push(description); @@ -97,4 +160,21 @@ export class Client { } } + /** + * A promise that resolves with initialization data. + */ + public get initData(): Promise { + return client.initData; + } + + /** + * Initialize the IDE. + */ + protected abstract initialize(): Promise; + + /** + * Create URI factory. + */ + protected abstract createUriFactory(): IURIFactory; + } diff --git a/packages/ide/src/fill/client.ts b/packages/ide/src/fill/client.ts index a05f9f8ce..ceb16a322 100644 --- a/packages/ide/src/fill/client.ts +++ b/packages/ide/src/fill/client.ts @@ -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; - private readonly closeEmitter: Emitter; - private readonly upEmitter: Emitter; - private readonly downEmitter: Emitter; - private readonly messageBuffer: Uint8Array[]; + private readonly messageEmitter: Emitter = new Emitter(); + private readonly closeEmitter: Emitter = new Emitter(); + private readonly upEmitter: Emitter = new Emitter(); + private readonly downEmitter: Emitter = 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 { @@ -116,7 +109,7 @@ class Connection implements ReadWriteConnection { private async openSocket(): Promise { 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()); diff --git a/packages/ide/src/fill/clipboard.ts b/packages/ide/src/fill/clipboard.ts new file mode 100644 index 000000000..065e73f49 --- /dev/null +++ b/packages/ide/src/fill/clipboard.ts @@ -0,0 +1,132 @@ +import { IDisposable } from "@coder/disposable"; +import { Emitter } from "@coder/events"; + +/** + * Native clipboard. + */ +export class Clipboard { + + private readonly enableEmitter: Emitter = 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 { + return this.instance ? this.instance.readText() : Promise.resolve(""); + } + + /** + * Write text to the clipboard. + */ + public writeText(value: string): Promise { + 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; + writeText(value: string): Promise; + }) | 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 { + // 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(); diff --git a/packages/ide/src/fill/dialog.ts b/packages/ide/src/fill/dialog.ts index 73b428798..2ea6c48a9 100644 --- a/packages/ide/src/fill/dialog.ts +++ b/packages/ide/src/fill/dialog.ts @@ -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); } diff --git a/packages/ide/src/fill/electron.ts b/packages/ide/src/fill/electron.ts index ef2bcd574..5c236060f 100644 --- a/packages/ide/src/fill/electron.ts +++ b/packages/ide/src/fill/electron.ts @@ -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"; +/// +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") { + (global).document = {} as any; +} -// document.createElement = (tagName: string) => { -// const createElement = (tagName: string) => { -// return oldCreateElement.call(document, tagName); -// }; +const oldCreateElement: ( + 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 = (tagName: K): HTMLElementTagNameMap[K] => { + const createElement = (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