Browse Source

refactor: Port window-setup to TS (#16894)

* refactor: Port window-setup to TS

* refactor: Make the linter happy

* refactor: Sneaky little TS error

* refactor: Correctly import window-setup

* refactor: Implement feedback <3

* refactor: Allow decorators in TS

* refactor: Use named windowSetup in isolatedRenderer

* refactor: Help TS understand

* refactor: Welp, use createEvent again

* refactor: Use the correct target in the decorator
Felix Rieseberg 6 years ago
parent
commit
6cd75744ef
6 changed files with 282 additions and 243 deletions
  1. 1 1
      filenames.gni
  2. 2 1
      lib/isolated_renderer/init.js
  3. 2 1
      lib/renderer/init.js
  4. 0 239
      lib/renderer/window-setup.js
  5. 276 0
      lib/renderer/window-setup.ts
  6. 1 1
      tsconfig.json

+ 1 - 1
filenames.gni

@@ -72,7 +72,7 @@ filenames = {
     "lib/renderer/remote.js",
     "lib/renderer/security-warnings.js",
     "lib/renderer/web-frame-init.js",
-    "lib/renderer/window-setup.js",
+    "lib/renderer/window-setup.ts",
     "lib/renderer/web-view/guest-view-internal.js",
     "lib/renderer/web-view/web-view-attributes.js",
     "lib/renderer/web-view/web-view-constants.js",

+ 2 - 1
lib/isolated_renderer/init.js

@@ -18,5 +18,6 @@ const isolatedWorldArgs = v8Util.getHiddenValue(isolatedWorld, 'isolated-world-a
 
 if (isolatedWorldArgs) {
   const { ipcRenderer, guestInstanceId, isHiddenPage, openerId, usesNativeWindowOpen } = isolatedWorldArgs
-  require('@electron/internal/renderer/window-setup')(ipcRenderer, guestInstanceId, openerId, isHiddenPage, usesNativeWindowOpen)
+  const { windowSetup } = require('@electron/internal/renderer/window-setup')
+  windowSetup(ipcRenderer, guestInstanceId, openerId, isHiddenPage, usesNativeWindowOpen)
 }

+ 2 - 1
lib/renderer/init.js

@@ -73,7 +73,8 @@ switch (window.location.protocol) {
     break
   default: {
     // Override default web functions.
-    require('@electron/internal/renderer/window-setup')(ipcRenderer, guestInstanceId, openerId, isHiddenPage, usesNativeWindowOpen)
+    const { windowSetup } = require('@electron/internal/renderer/window-setup')
+    windowSetup(ipcRenderer, guestInstanceId, openerId, isHiddenPage, usesNativeWindowOpen)
 
     // Inject content scripts.
     if (process.isMainFrame) {

+ 0 - 239
lib/renderer/window-setup.js

@@ -1,239 +0,0 @@
-'use strict'
-
-// This file should have no requires since it is used by the isolated context
-// preload bundle. Instead arguments should be passed in for everything it
-// needs.
-
-// This file implements the following APIs:
-// - window.history.back()
-// - window.history.forward()
-// - window.history.go()
-// - window.history.length
-// - window.open()
-// - window.opener.blur()
-// - window.opener.close()
-// - window.opener.eval()
-// - window.opener.focus()
-// - window.opener.location
-// - window.opener.print()
-// - window.opener.postMessage()
-// - window.prompt()
-// - document.hidden
-// - document.visibilityState
-
-const { defineProperty, defineProperties } = Object
-
-// Helper function to resolve relative url.
-const a = window.document.createElement('a')
-const resolveURL = function (url) {
-  a.href = url
-  return a.href
-}
-
-// Use this method to ensure values expected as strings in the main process
-// are convertible to strings in the renderer process. This ensures exceptions
-// converting values to strings are thrown in this process.
-const toString = (value) => {
-  return value != null ? `${value}` : value
-}
-
-const windowProxies = {}
-
-const getOrCreateProxy = (ipcRenderer, guestId) => {
-  let proxy = windowProxies[guestId]
-  if (proxy == null) {
-    proxy = new BrowserWindowProxy(ipcRenderer, guestId)
-    windowProxies[guestId] = proxy
-  }
-  return proxy
-}
-
-const removeProxy = (guestId) => {
-  delete windowProxies[guestId]
-}
-
-function LocationProxy (ipcRenderer, guestId) {
-  const getGuestURL = function () {
-    const urlString = ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', guestId, 'getURL')
-    try {
-      return new URL(urlString)
-    } catch (e) {
-      console.error('LocationProxy: failed to parse string', urlString, e)
-    }
-
-    return null
-  }
-
-  const propertyProxyFor = function (property) {
-    return {
-      get: function () {
-        const guestURL = getGuestURL()
-        const value = guestURL ? guestURL[property] : ''
-        return value === undefined ? '' : value
-      },
-      set: function (newVal) {
-        const guestURL = getGuestURL()
-        if (guestURL) {
-          guestURL[property] = newVal
-          return ipcRenderer.sendSync(
-            'ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC',
-            guestId, 'loadURL', guestURL.toString())
-        }
-      }
-    }
-  }
-
-  defineProperties(this, {
-    hash: propertyProxyFor('hash'),
-    href: propertyProxyFor('href'),
-    host: propertyProxyFor('host'),
-    hostname: propertyProxyFor('hostname'),
-    origin: propertyProxyFor('origin'),
-    pathname: propertyProxyFor('pathname'),
-    port: propertyProxyFor('port'),
-    protocol: propertyProxyFor('protocol'),
-    search: propertyProxyFor('search')
-  })
-
-  this.toString = function () {
-    return this.href
-  }
-}
-
-function BrowserWindowProxy (ipcRenderer, guestId) {
-  this.closed = false
-
-  const location = new LocationProxy(ipcRenderer, guestId)
-  defineProperty(this, 'location', {
-    get: function () {
-      return location
-    },
-    set: function (url) {
-      url = resolveURL(url)
-      return ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', guestId, 'loadURL', url)
-    }
-  })
-
-  ipcRenderer.once(`ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_${guestId}`, () => {
-    removeProxy(guestId)
-    this.closed = true
-  })
-
-  this.close = () => {
-    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSE', guestId)
-  }
-
-  this.focus = () => {
-    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', guestId, 'focus')
-  }
-
-  this.blur = () => {
-    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', guestId, 'blur')
-  }
-
-  this.print = () => {
-    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', guestId, 'print')
-  }
-
-  this.postMessage = (message, targetOrigin) => {
-    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', guestId, message, toString(targetOrigin), window.location.origin)
-  }
-
-  this.eval = (...args) => {
-    ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', guestId, 'executeJavaScript', ...args)
-  }
-}
-
-module.exports = (ipcRenderer, guestInstanceId, openerId, hiddenPage, usesNativeWindowOpen) => {
-  if (guestInstanceId == null) {
-    // Override default window.close.
-    window.close = function () {
-      ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_CLOSE')
-    }
-  }
-
-  if (!usesNativeWindowOpen) {
-    // Make the browser window or guest view emit "new-window" event.
-    window.open = function (url, frameName, features) {
-      if (url != null && url !== '') {
-        url = resolveURL(url)
-      }
-      const guestId = ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', url, toString(frameName), toString(features))
-      if (guestId != null) {
-        return getOrCreateProxy(ipcRenderer, guestId)
-      } else {
-        return null
-      }
-    }
-
-    if (openerId != null) {
-      window.opener = getOrCreateProxy(ipcRenderer, openerId)
-    }
-  }
-
-  // But we do not support prompt().
-  window.prompt = function () {
-    throw new Error('prompt() is and will not be supported.')
-  }
-
-  ipcRenderer.on('ELECTRON_GUEST_WINDOW_POSTMESSAGE', function (event, sourceId, message, sourceOrigin) {
-    // Manually dispatch event instead of using postMessage because we also need to
-    // set event.source.
-    event = document.createEvent('Event')
-    event.initEvent('message', false, false)
-    event.data = message
-    event.origin = sourceOrigin
-    event.source = getOrCreateProxy(ipcRenderer, sourceId)
-    window.dispatchEvent(event)
-  })
-
-  window.history.back = function () {
-    ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER_GO_BACK')
-  }
-
-  window.history.forward = function () {
-    ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER_GO_FORWARD')
-  }
-
-  window.history.go = function (offset) {
-    ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER_GO_TO_OFFSET', +offset)
-  }
-
-  defineProperty(window.history, 'length', {
-    get: function () {
-      return ipcRenderer.sendSync('ELECTRON_NAVIGATION_CONTROLLER_LENGTH')
-    }
-  })
-
-  if (guestInstanceId != null) {
-    // Webview `document.visibilityState` tracks window visibility (and ignores
-    // the actual <webview> element visibility) for backwards compatibility.
-    // See discussion in #9178.
-    //
-    // Note that this results in duplicate visibilitychange events (since
-    // Chromium also fires them) and potentially incorrect visibility change.
-    // We should reconsider this decision for Electron 2.0.
-    let cachedVisibilityState = hiddenPage ? 'hidden' : 'visible'
-
-    // Subscribe to visibilityState changes.
-    ipcRenderer.on('ELECTRON_GUEST_INSTANCE_VISIBILITY_CHANGE', function (event, visibilityState) {
-      if (cachedVisibilityState !== visibilityState) {
-        cachedVisibilityState = visibilityState
-        document.dispatchEvent(new Event('visibilitychange'))
-      }
-    })
-
-    // Make document.hidden and document.visibilityState return the correct value.
-    defineProperty(document, 'hidden', {
-      get: function () {
-        return cachedVisibilityState !== 'visible'
-      }
-    })
-
-    defineProperty(document, 'visibilityState', {
-      get: function () {
-        return cachedVisibilityState
-      }
-    })
-  }
-}

+ 276 - 0
lib/renderer/window-setup.ts

@@ -0,0 +1,276 @@
+// This file should have no requires since it is used by the isolated context
+// preload bundle. Instead arguments should be passed in for everything it
+// needs.
+
+// This file implements the following APIs:
+// - window.history.back()
+// - window.history.forward()
+// - window.history.go()
+// - window.history.length
+// - window.open()
+// - window.opener.blur()
+// - window.opener.close()
+// - window.opener.eval()
+// - window.opener.focus()
+// - window.opener.location
+// - window.opener.print()
+// - window.opener.postMessage()
+// - window.prompt()
+// - document.hidden
+// - document.visibilityState
+
+const { defineProperty, defineProperties } = Object
+
+// Helper function to resolve relative url.
+const a = window.document.createElement('a')
+const resolveURL = function (url: string) {
+  a.href = url
+  return a.href
+}
+
+// Use this method to ensure values expected as strings in the main process
+// are convertible to strings in the renderer process. This ensures exceptions
+// converting values to strings are thrown in this process.
+const toString = (value: any) => {
+  return value != null ? `${value}` : value
+}
+
+const windowProxies: Record<number, BrowserWindowProxy> = {}
+
+const getOrCreateProxy = (ipcRenderer: Electron.IpcRenderer, guestId: number) => {
+  let proxy = windowProxies[guestId]
+  if (proxy == null) {
+    proxy = new BrowserWindowProxy(ipcRenderer, guestId)
+    windowProxies[guestId] = proxy
+  }
+  return proxy
+}
+
+const removeProxy = (guestId: number) => {
+  delete windowProxies[guestId]
+}
+
+type LocationProperties = 'hash' | 'href' | 'host' | 'hostname' | 'origin' | 'pathname' | 'port' | 'protocol' | 'search'
+
+class LocationProxy {
+  @LocationProxy.ProxyProperty public hash!: string;
+  @LocationProxy.ProxyProperty public href!: string;
+  @LocationProxy.ProxyProperty public host!: string;
+  @LocationProxy.ProxyProperty public hostname!: string;
+  @LocationProxy.ProxyProperty public origin!: string;
+  @LocationProxy.ProxyProperty public pathname!: string;
+  @LocationProxy.ProxyProperty public port!: string;
+  @LocationProxy.ProxyProperty public protocol!: string;
+  @LocationProxy.ProxyProperty public search!: URLSearchParams;
+
+  private ipcRenderer: Electron.IpcRenderer;
+  private guestId: number;
+
+  /**
+   * Beware: This decorator will have the _prototype_ as the `target`. It defines properties
+   * commonly found in URL on the LocationProxy.
+   */
+  private static ProxyProperty<T> (target: LocationProxy, propertyKey: LocationProperties) {
+    Object.defineProperty(target, propertyKey, {
+      get: function (): T | string {
+        const guestURL = this.getGuestURL()
+        const value = guestURL ? guestURL[propertyKey] : ''
+        return value === undefined ? '' : value
+      },
+      set: function (newVal: T) {
+        const guestURL = this.getGuestURL()
+        if (guestURL) {
+          // TypeScript doesn't want us to assign to read-only variables.
+          // It's right, that's bad, but we're doing it anway.
+          (guestURL as any)[propertyKey] = newVal
+
+          return this.ipcRenderer.sendSync(
+            'ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC',
+            this.guestId, 'loadURL', guestURL.toString())
+        }
+      }
+    })
+  }
+
+  constructor (ipcRenderer: Electron.IpcRenderer, guestId: number) {
+    // eslint will consider the constructor "useless"
+    // unless we assign them in the body. It's fine, that's what
+    // TS would do anyway.
+    this.ipcRenderer = ipcRenderer
+    this.guestId = guestId
+    this.getGuestURL = this.getGuestURL.bind(this)
+  }
+
+  public toString (): string {
+    return this.href
+  }
+
+  private getGuestURL (): URL | null {
+    const urlString = this.ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', this.guestId, 'getURL')
+    try {
+      return new URL(urlString)
+    } catch (e) {
+      console.error('LocationProxy: failed to parse string', urlString, e)
+    }
+
+    return null
+  }
+}
+
+class BrowserWindowProxy {
+  public closed: boolean = false
+
+  private _location: LocationProxy
+  private guestId: number
+  private ipcRenderer: Electron.IpcRenderer
+
+  // TypeScript doesn't allow getters/accessors with different types,
+  // so for now, we'll have to make do with an "any" in the mix.
+  // https://github.com/Microsoft/TypeScript/issues/2521
+  public get location (): LocationProxy | any {
+    return this._location
+  }
+  public set location (url: string | any) {
+    url = resolveURL(url)
+    this.ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD_SYNC', this.guestId, 'loadURL', url)
+  }
+
+  constructor (ipcRenderer: Electron.IpcRenderer, guestId: number) {
+    this.guestId = guestId
+    this.ipcRenderer = ipcRenderer
+    this._location = new LocationProxy(ipcRenderer, guestId)
+
+    ipcRenderer.once(`ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSED_${guestId}`, () => {
+      removeProxy(guestId)
+      this.closed = true
+    })
+  }
+
+  public close () {
+    this.ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_CLOSE', this.guestId)
+  }
+
+  public focus () {
+    this.ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', this.guestId, 'focus')
+  }
+
+  public blur () {
+    this.ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_METHOD', this.guestId, 'blur')
+  }
+
+  public print () {
+    this.ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', this.guestId, 'print')
+  }
+
+  public postMessage (message: any, targetOrigin: any) {
+    this.ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_POSTMESSAGE', this.guestId, message, toString(targetOrigin), window.location.origin)
+  }
+
+  public eval (...args: any[]) {
+    this.ipcRenderer.send('ELECTRON_GUEST_WINDOW_MANAGER_WEB_CONTENTS_METHOD', this.guestId, 'executeJavaScript', ...args)
+  }
+}
+
+export const windowSetup = (
+  ipcRenderer: Electron.IpcRenderer, guestInstanceId: number, openerId: number, isHiddenPage: boolean, usesNativeWindowOpen: boolean
+) => {
+  if (guestInstanceId == null) {
+    // Override default window.close.
+    window.close = function () {
+      ipcRenderer.sendSync('ELECTRON_BROWSER_WINDOW_CLOSE')
+    }
+  }
+
+  if (!usesNativeWindowOpen) {
+    // Make the browser window or guest view emit "new-window" event.
+    (window as any).open = function (url?: string, frameName?: string, features?: string) {
+      if (url != null && url !== '') {
+        url = resolveURL(url)
+      }
+      const guestId = ipcRenderer.sendSync('ELECTRON_GUEST_WINDOW_MANAGER_WINDOW_OPEN', url, toString(frameName), toString(features))
+      if (guestId != null) {
+        return getOrCreateProxy(ipcRenderer, guestId)
+      } else {
+        return null
+      }
+    }
+
+    if (openerId != null) {
+      window.opener = getOrCreateProxy(ipcRenderer, openerId)
+    }
+  }
+
+  // But we do not support prompt().
+  window.prompt = function () {
+    throw new Error('prompt() is and will not be supported.')
+  }
+
+  ipcRenderer.on('ELECTRON_GUEST_WINDOW_POSTMESSAGE', function (
+    _event: Electron.Event, sourceId: number, message: any, sourceOrigin: string
+  ) {
+    // Manually dispatch event instead of using postMessage because we also need to
+    // set event.source.
+    //
+    // Why any? We can't construct a MessageEvent and we can't
+    // use `as MessageEvent` because you're not supposed to override
+    // data, origin, and source
+    const event: any = document.createEvent('Event')
+    event.initEvent('message', false, false)
+
+    event.data = message
+    event.origin = sourceOrigin
+    event.source = getOrCreateProxy(ipcRenderer, sourceId)
+
+    window.dispatchEvent(event as MessageEvent)
+  })
+
+  window.history.back = function () {
+    ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER_GO_BACK')
+  }
+
+  window.history.forward = function () {
+    ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER_GO_FORWARD')
+  }
+
+  window.history.go = function (offset: number) {
+    ipcRenderer.send('ELECTRON_NAVIGATION_CONTROLLER_GO_TO_OFFSET', +offset)
+  }
+
+  defineProperty(window.history, 'length', {
+    get: function () {
+      return ipcRenderer.sendSync('ELECTRON_NAVIGATION_CONTROLLER_LENGTH')
+    }
+  })
+
+  if (guestInstanceId != null) {
+    // Webview `document.visibilityState` tracks window visibility (and ignores
+    // the actual <webview> element visibility) for backwards compatibility.
+    // See discussion in #9178.
+    //
+    // Note that this results in duplicate visibilitychange events (since
+    // Chromium also fires them) and potentially incorrect visibility change.
+    // We should reconsider this decision for Electron 2.0.
+    let cachedVisibilityState = isHiddenPage ? 'hidden' : 'visible'
+
+    // Subscribe to visibilityState changes.
+    ipcRenderer.on('ELECTRON_GUEST_INSTANCE_VISIBILITY_CHANGE', function (_event: Electron.Event, visibilityState: VisibilityState) {
+      if (cachedVisibilityState !== visibilityState) {
+        cachedVisibilityState = visibilityState
+        document.dispatchEvent(new Event('visibilitychange'))
+      }
+    })
+
+    // Make document.hidden and document.visibilityState return the correct value.
+    defineProperty(document, 'hidden', {
+      get: function () {
+        return cachedVisibilityState !== 'visible'
+      }
+    })
+
+    defineProperty(document, 'visibilityState', {
+      get: function () {
+        return cachedVisibilityState
+      }
+    })
+  }
+}

+ 1 - 1
tsconfig.json

@@ -8,7 +8,7 @@
       "dom.iterable"
     ],
     "sourceMap": true,
-    "experimentalDecorators": false,
+    "experimentalDecorators": true,
     "strict": true,
     "baseUrl": ".",
     "allowJs": true,