From 6c341a13d813fc63b76fbbffe7920eeaf116d3a8 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Sat, 2 May 2020 12:50:44 -0400 Subject: Refactor frontend API classes (#482) * Mark functions as private * Remove constructor side effects * Use safer handler invocation * Mark functions as private * Mark fields as private * Update BackendApiForwarder public API --- ext/fg/js/frontend-api-receiver.js | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) (limited to 'ext/fg/js/frontend-api-receiver.js') diff --git a/ext/fg/js/frontend-api-receiver.js b/ext/fg/js/frontend-api-receiver.js index 4abd4e81..c5bb58af 100644 --- a/ext/fg/js/frontend-api-receiver.js +++ b/ext/fg/js/frontend-api-receiver.js @@ -20,38 +20,42 @@ class FrontendApiReceiver { constructor(source='', handlers=new Map()) { this._source = source; this._handlers = handlers; + } - chrome.runtime.onConnect.addListener(this.onConnect.bind(this)); + prepare() { + chrome.runtime.onConnect.addListener(this._onConnect.bind(this)); } - onConnect(port) { + _onConnect(port) { if (port.name !== 'frontend-api-receiver') { return; } - port.onMessage.addListener(this.onMessage.bind(this, port)); + port.onMessage.addListener(this._onMessage.bind(this, port)); } - onMessage(port, {id, action, params, target, senderId}) { + _onMessage(port, {id, action, params, target, senderId}) { if (target !== this._source) { return; } const handler = this._handlers.get(action); if (typeof handler !== 'function') { return; } - this.sendAck(port, id, senderId); + this._sendAck(port, id, senderId); + this._invokeHandler(handler, params, port, id, senderId); + } - handler(params).then( - (result) => { - this.sendResult(port, id, senderId, {result}); - }, - (error) => { - this.sendResult(port, id, senderId, {error: errorToJson(error)}); - }); + async _invokeHandler(handler, params, port, id, senderId) { + try { + const result = await handler(params); + this._sendResult(port, id, senderId, {result}); + } catch (error) { + this._sendResult(port, id, senderId, {error: errorToJson(error)}); + } } - sendAck(port, id, senderId) { + _sendAck(port, id, senderId) { port.postMessage({type: 'ack', id, senderId}); } - sendResult(port, id, senderId, data) { + _sendResult(port, id, senderId, data) { port.postMessage({type: 'result', id, senderId, data}); } } -- cgit v1.2.3 From b936c3e4b1bc993e535b02dee91bf6afc15a3564 Mon Sep 17 00:00:00 2001 From: toasted-nutbread Date: Fri, 8 May 2020 19:04:53 -0400 Subject: Popup proxy host refactor (#516) * Rename PopupProxyHost to PopupFactory * Update FrontendApiReceiver to support non-async handlers * Make some functions non-async * Make setCustomCss non-async * Make setContentScale non-async * Remove static * Rename variables * Pass frameId into PopupFactory's constructor * Change FrontendApiReceiver source from popup-proxy-host to popup-factor * Rename _invokeHostApi to _invoke * Rename PopupProxy.getHostUrl to getUrl --- ext/bg/js/search-main.js | 2 +- ext/bg/js/settings/popup-preview-frame.js | 11 +- ext/bg/settings-popup-preview.html | 2 +- ext/fg/js/content-script-main.js | 28 +++-- ext/fg/js/frontend-api-receiver.js | 27 ++++- ext/fg/js/popup-factory.js | 181 +++++++++++++++++++++++++++++ ext/fg/js/popup-proxy-host.js | 186 ------------------------------ ext/fg/js/popup-proxy.js | 32 ++--- ext/fg/js/popup.js | 2 +- ext/manifest.json | 2 +- 10 files changed, 247 insertions(+), 226 deletions(-) create mode 100644 ext/fg/js/popup-factory.js delete mode 100644 ext/fg/js/popup-proxy-host.js (limited to 'ext/fg/js/frontend-api-receiver.js') diff --git a/ext/bg/js/search-main.js b/ext/bg/js/search-main.js index 1075d46e..6e092fbc 100644 --- a/ext/bg/js/search-main.js +++ b/ext/bg/js/search-main.js @@ -31,7 +31,7 @@ async function injectSearchFrontend() { '/fg/js/frontend-api-receiver.js', '/fg/js/frame-offset-forwarder.js', '/fg/js/popup.js', - '/fg/js/popup-proxy-host.js', + '/fg/js/popup-factory.js', '/fg/js/frontend.js', '/fg/js/content-script-main.js' ]); diff --git a/ext/bg/js/settings/popup-preview-frame.js b/ext/bg/js/settings/popup-preview-frame.js index c7b0c218..8901a0c4 100644 --- a/ext/bg/js/settings/popup-preview-frame.js +++ b/ext/bg/js/settings/popup-preview-frame.js @@ -18,8 +18,9 @@ /* global * Frontend * Popup - * PopupProxyHost + * PopupFactory * TextSourceRange + * apiFrameInformationGet * apiOptionsGet */ @@ -56,10 +57,12 @@ class SettingsPopupPreview { window.apiOptionsGet = this.apiOptionsGet.bind(this); // Overwrite frontend - const popupHost = new PopupProxyHost(); - await popupHost.prepare(); + const {frameId} = await apiFrameInformationGet(); - this.popup = popupHost.getOrCreatePopup(); + const popupFactory = new PopupFactory(frameId); + await popupFactory.prepare(); + + this.popup = popupFactory.getOrCreatePopup(); this.popup.setChildrenSupported(false); this.popupSetCustomOuterCssOld = this.popup.setCustomOuterCss; diff --git a/ext/bg/settings-popup-preview.html b/ext/bg/settings-popup-preview.html index a332fb22..3d7f6455 100644 --- a/ext/bg/settings-popup-preview.html +++ b/ext/bg/settings-popup-preview.html @@ -127,7 +127,7 @@ - + diff --git a/ext/fg/js/content-script-main.js b/ext/fg/js/content-script-main.js index 277e6567..3afc9648 100644 --- a/ext/fg/js/content-script-main.js +++ b/ext/fg/js/content-script-main.js @@ -19,10 +19,11 @@ * DOM * FrameOffsetForwarder * Frontend + * PopupFactory * PopupProxy - * PopupProxyHost * apiBroadcastTab * apiForwardLogsToBackend + * apiFrameInformationGet * apiOptionsGet */ @@ -47,10 +48,17 @@ async function createIframePopupProxy(frameOffsetForwarder, setDisabled) { } async function getOrCreatePopup(depth) { - const popupHost = new PopupProxyHost(); - await popupHost.prepare(); + const {frameId} = await apiFrameInformationGet(); + if (typeof frameId !== 'number') { + const error = new Error('Failed to get frameId'); + yomichan.logError(error); + throw error; + } - const popup = popupHost.getOrCreatePopup(null, null, depth); + const popupFactory = new PopupFactory(frameId); + await popupFactory.prepare(); + + const popup = popupFactory.getOrCreatePopup(null, null, depth); return popup; } @@ -89,20 +97,20 @@ async function createPopupProxy(depth, id, parentFrameId) { }; let urlUpdatedAt = 0; - let proxyHostUrlCached = url; - const getProxyHostUrl = async () => { + let popupProxyUrlCached = url; + const getPopupProxyUrl = async () => { const now = Date.now(); if (popups.proxy !== null && now - urlUpdatedAt > 500) { - proxyHostUrlCached = await popups.proxy.getHostUrl(); + popupProxyUrlCached = await popups.proxy.getUrl(); urlUpdatedAt = now; } - return proxyHostUrlCached; + return popupProxyUrlCached; }; const applyOptions = async () => { const optionsContext = { depth: isSearchPage ? 0 : depth, - url: proxy ? await getProxyHostUrl() : window.location.href + url: proxy ? await getPopupProxyUrl() : window.location.href }; const options = await apiOptionsGet(optionsContext); @@ -124,7 +132,7 @@ async function createPopupProxy(depth, id, parentFrameId) { } if (frontend === null) { - const getUrl = proxy ? getProxyHostUrl : null; + const getUrl = proxy ? getPopupProxyUrl : null; frontend = new Frontend(popup, getUrl); frontendPreparePromise = frontend.prepare(); await frontendPreparePromise; diff --git a/ext/fg/js/frontend-api-receiver.js b/ext/fg/js/frontend-api-receiver.js index c5bb58af..3fa9e8b6 100644 --- a/ext/fg/js/frontend-api-receiver.js +++ b/ext/fg/js/frontend-api-receiver.js @@ -17,9 +17,9 @@ class FrontendApiReceiver { - constructor(source='', handlers=new Map()) { + constructor(source, messageHandlers) { this._source = source; - this._handlers = handlers; + this._messageHandlers = messageHandlers; } prepare() { @@ -35,14 +35,29 @@ class FrontendApiReceiver { _onMessage(port, {id, action, params, target, senderId}) { if (target !== this._source) { return; } - const handler = this._handlers.get(action); - if (typeof handler !== 'function') { return; } + const messageHandler = this._messageHandlers.get(action); + if (typeof messageHandler === 'undefined') { return; } + + const {handler, async} = messageHandler; this._sendAck(port, id, senderId); - this._invokeHandler(handler, params, port, id, senderId); + if (async) { + this._invokeHandlerAsync(handler, params, port, id, senderId); + } else { + this._invokeHandler(handler, params, port, id, senderId); + } + } + + _invokeHandler(handler, params, port, id, senderId) { + try { + const result = handler(params); + this._sendResult(port, id, senderId, {result}); + } catch (error) { + this._sendResult(port, id, senderId, {error: errorToJson(error)}); + } } - async _invokeHandler(handler, params, port, id, senderId) { + async _invokeHandlerAsync(handler, params, port, id, senderId) { try { const result = await handler(params); this._sendResult(port, id, senderId, {result}); diff --git a/ext/fg/js/popup-factory.js b/ext/fg/js/popup-factory.js new file mode 100644 index 00000000..21e64dd0 --- /dev/null +++ b/ext/fg/js/popup-factory.js @@ -0,0 +1,181 @@ +/* + * Copyright (C) 2019-2020 Yomichan Authors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* global + * FrontendApiReceiver + * Popup + */ + +class PopupFactory { + constructor(frameId) { + this._popups = new Map(); + this._frameId = frameId; + } + + // Public functions + + async prepare() { + const apiReceiver = new FrontendApiReceiver(`popup-factory#${this._frameId}`, new Map([ + ['getOrCreatePopup', {async: false, handler: this._onApiGetOrCreatePopup.bind(this)}], + ['setOptionsContext', {async: true, handler: this._onApiSetOptionsContext.bind(this)}], + ['hide', {async: false, handler: this._onApiHide.bind(this)}], + ['isVisible', {async: true, handler: this._onApiIsVisibleAsync.bind(this)}], + ['setVisibleOverride', {async: true, handler: this._onApiSetVisibleOverride.bind(this)}], + ['containsPoint', {async: true, handler: this._onApiContainsPoint.bind(this)}], + ['showContent', {async: true, handler: this._onApiShowContent.bind(this)}], + ['setCustomCss', {async: false, handler: this._onApiSetCustomCss.bind(this)}], + ['clearAutoPlayTimer', {async: false, handler: this._onApiClearAutoPlayTimer.bind(this)}], + ['setContentScale', {async: false, handler: this._onApiSetContentScale.bind(this)}], + ['getUrl', {async: false, handler: this._onApiGetUrl.bind(this)}] + ])); + apiReceiver.prepare(); + } + + getOrCreatePopup(id=null, parentId=null, depth=null) { + // Find by existing id + if (id !== null) { + const popup = this._popups.get(id); + if (typeof popup !== 'undefined') { + return popup; + } + } + + // Find by existing parent id + let parent = null; + if (parentId !== null) { + parent = this._popups.get(parentId); + if (typeof parent !== 'undefined') { + const popup = parent.child; + if (popup !== null) { + return popup; + } + } else { + parent = null; + } + } + + // New unique id + if (id === null) { + id = yomichan.generateId(16); + } + + // Create new popup + if (parent !== null) { + if (depth !== null) { + throw new Error('Depth cannot be set when parent exists'); + } + depth = parent.depth + 1; + } else if (depth === null) { + depth = 0; + } + const popup = new Popup(id, depth, this._frameId); + if (parent !== null) { + popup.setParent(parent); + } + this._popups.set(id, popup); + return popup; + } + + // API message handlers + + _onApiGetOrCreatePopup({id, parentId}) { + const popup = this.getOrCreatePopup(id, parentId); + return { + id: popup.id + }; + } + + async _onApiSetOptionsContext({id, optionsContext, source}) { + const popup = this._getPopup(id); + return await popup.setOptionsContext(optionsContext, source); + } + + _onApiHide({id, changeFocus}) { + const popup = this._getPopup(id); + return popup.hide(changeFocus); + } + + async _onApiIsVisibleAsync({id}) { + const popup = this._getPopup(id); + return await popup.isVisible(); + } + + async _onApiSetVisibleOverride({id, visible}) { + const popup = this._getPopup(id); + return await popup.setVisibleOverride(visible); + } + + async _onApiContainsPoint({id, x, y}) { + const popup = this._getPopup(id); + [x, y] = this._convertPopupPointToRootPagePoint(popup, x, y); + return await popup.containsPoint(x, y); + } + + async _onApiShowContent({id, elementRect, writingMode, type, details, context}) { + const popup = this._getPopup(id); + elementRect = this._convertJsonRectToDOMRect(popup, elementRect); + if (!this._popupCanShow(popup)) { return; } + return await popup.showContent(elementRect, writingMode, type, details, context); + } + + _onApiSetCustomCss({id, css}) { + const popup = this._getPopup(id); + return popup.setCustomCss(css); + } + + _onApiClearAutoPlayTimer({id}) { + const popup = this._getPopup(id); + return popup.clearAutoPlayTimer(); + } + + _onApiSetContentScale({id, scale}) { + const popup = this._getPopup(id); + return popup.setContentScale(scale); + } + + _onApiGetUrl() { + return window.location.href; + } + + // Private functions + + _getPopup(id) { + const popup = this._popups.get(id); + if (typeof popup === 'undefined') { + throw new Error(`Invalid popup ID ${id}`); + } + return popup; + } + + _convertJsonRectToDOMRect(popup, jsonRect) { + const [x, y] = this._convertPopupPointToRootPagePoint(popup, jsonRect.x, jsonRect.y); + return new DOMRect(x, y, jsonRect.width, jsonRect.height); + } + + _convertPopupPointToRootPagePoint(popup, x, y) { + if (popup.parent !== null) { + const popupRect = popup.parent.getContainerRect(); + x += popupRect.x; + y += popupRect.y; + } + return [x, y]; + } + + _popupCanShow(popup) { + return popup.parent === null || popup.parent.isVisibleSync(); + } +} diff --git a/ext/fg/js/popup-proxy-host.js b/ext/fg/js/popup-proxy-host.js deleted file mode 100644 index 39150bc7..00000000 --- a/ext/fg/js/popup-proxy-host.js +++ /dev/null @@ -1,186 +0,0 @@ -/* - * Copyright (C) 2019-2020 Yomichan Authors - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -/* global - * FrontendApiReceiver - * Popup - * apiFrameInformationGet - */ - -class PopupProxyHost { - constructor() { - this._popups = new Map(); - this._frameId = null; - } - - // Public functions - - async prepare() { - const {frameId} = await apiFrameInformationGet(); - if (typeof frameId !== 'number') { return; } - this._frameId = frameId; - - const apiReceiver = new FrontendApiReceiver(`popup-proxy-host#${this._frameId}`, new Map([ - ['getOrCreatePopup', this._onApiGetOrCreatePopup.bind(this)], - ['setOptionsContext', this._onApiSetOptionsContext.bind(this)], - ['hide', this._onApiHide.bind(this)], - ['isVisible', this._onApiIsVisibleAsync.bind(this)], - ['setVisibleOverride', this._onApiSetVisibleOverride.bind(this)], - ['containsPoint', this._onApiContainsPoint.bind(this)], - ['showContent', this._onApiShowContent.bind(this)], - ['setCustomCss', this._onApiSetCustomCss.bind(this)], - ['clearAutoPlayTimer', this._onApiClearAutoPlayTimer.bind(this)], - ['setContentScale', this._onApiSetContentScale.bind(this)], - ['getHostUrl', this._onApiGetHostUrl.bind(this)] - ])); - apiReceiver.prepare(); - } - - getOrCreatePopup(id=null, parentId=null, depth=null) { - // Find by existing id - if (id !== null) { - const popup = this._popups.get(id); - if (typeof popup !== 'undefined') { - return popup; - } - } - - // Find by existing parent id - let parent = null; - if (parentId !== null) { - parent = this._popups.get(parentId); - if (typeof parent !== 'undefined') { - const popup = parent.child; - if (popup !== null) { - return popup; - } - } else { - parent = null; - } - } - - // New unique id - if (id === null) { - id = yomichan.generateId(16); - } - - // Create new popup - if (parent !== null) { - if (depth !== null) { - throw new Error('Depth cannot be set when parent exists'); - } - depth = parent.depth + 1; - } else if (depth === null) { - depth = 0; - } - const popup = new Popup(id, depth, this._frameId); - if (parent !== null) { - popup.setParent(parent); - } - this._popups.set(id, popup); - return popup; - } - - // API message handlers - - async _onApiGetOrCreatePopup({id, parentId}) { - const popup = this.getOrCreatePopup(id, parentId); - return { - id: popup.id - }; - } - - async _onApiSetOptionsContext({id, optionsContext, source}) { - const popup = this._getPopup(id); - return await popup.setOptionsContext(optionsContext, source); - } - - async _onApiHide({id, changeFocus}) { - const popup = this._getPopup(id); - return popup.hide(changeFocus); - } - - async _onApiIsVisibleAsync({id}) { - const popup = this._getPopup(id); - return await popup.isVisible(); - } - - async _onApiSetVisibleOverride({id, visible}) { - const popup = this._getPopup(id); - return await popup.setVisibleOverride(visible); - } - - async _onApiContainsPoint({id, x, y}) { - const popup = this._getPopup(id); - [x, y] = PopupProxyHost._convertPopupPointToRootPagePoint(popup, x, y); - return await popup.containsPoint(x, y); - } - - async _onApiShowContent({id, elementRect, writingMode, type, details, context}) { - const popup = this._getPopup(id); - elementRect = PopupProxyHost._convertJsonRectToDOMRect(popup, elementRect); - if (!PopupProxyHost._popupCanShow(popup)) { return; } - return await popup.showContent(elementRect, writingMode, type, details, context); - } - - async _onApiSetCustomCss({id, css}) { - const popup = this._getPopup(id); - return popup.setCustomCss(css); - } - - async _onApiClearAutoPlayTimer({id}) { - const popup = this._getPopup(id); - return popup.clearAutoPlayTimer(); - } - - async _onApiSetContentScale({id, scale}) { - const popup = this._getPopup(id); - return popup.setContentScale(scale); - } - - async _onApiGetHostUrl() { - return window.location.href; - } - - // Private functions - - _getPopup(id) { - const popup = this._popups.get(id); - if (typeof popup === 'undefined') { - throw new Error(`Invalid popup ID ${id}`); - } - return popup; - } - - static _convertJsonRectToDOMRect(popup, jsonRect) { - const [x, y] = PopupProxyHost._convertPopupPointToRootPagePoint(popup, jsonRect.x, jsonRect.y); - return new DOMRect(x, y, jsonRect.width, jsonRect.height); - } - - static _convertPopupPointToRootPagePoint(popup, x, y) { - if (popup.parent !== null) { - const popupRect = popup.parent.getContainerRect(); - x += popupRect.x; - y += popupRect.y; - } - return [x, y]; - } - - static _popupCanShow(popup) { - return popup.parent === null || popup.parent.isVisibleSync(); - } -} diff --git a/ext/fg/js/popup-proxy.js b/ext/fg/js/popup-proxy.js index 93418202..6a84e000 100644 --- a/ext/fg/js/popup-proxy.js +++ b/ext/fg/js/popup-proxy.js @@ -51,7 +51,7 @@ class PopupProxy { // Public functions async prepare() { - const {id} = await this._invokeHostApi('getOrCreatePopup', {id: this._id, parentId: this._parentId}); + const {id} = await this._invoke('getOrCreatePopup', {id: this._id, parentId: this._parentId}); this._id = id; } @@ -60,19 +60,19 @@ class PopupProxy { } async setOptionsContext(optionsContext, source) { - return await this._invokeHostApi('setOptionsContext', {id: this._id, optionsContext, source}); + return await this._invoke('setOptionsContext', {id: this._id, optionsContext, source}); } hide(changeFocus) { - this._invokeHostApi('hide', {id: this._id, changeFocus}); + this._invoke('hide', {id: this._id, changeFocus}); } async isVisible() { - return await this._invokeHostApi('isVisible', {id: this._id}); + return await this._invoke('isVisible', {id: this._id}); } setVisibleOverride(visible) { - this._invokeHostApi('setVisibleOverride', {id: this._id, visible}); + this._invoke('setVisibleOverride', {id: this._id, visible}); } async containsPoint(x, y) { @@ -80,7 +80,7 @@ class PopupProxy { await this._updateFrameOffset(); [x, y] = this._applyFrameOffset(x, y); } - return await this._invokeHostApi('containsPoint', {id: this._id, x, y}); + return await this._invoke('containsPoint', {id: this._id, x, y}); } async showContent(elementRect, writingMode, type, details, context) { @@ -90,32 +90,32 @@ class PopupProxy { [x, y] = this._applyFrameOffset(x, y); } elementRect = {x, y, width, height}; - return await this._invokeHostApi('showContent', {id: this._id, elementRect, writingMode, type, details, context}); + return await this._invoke('showContent', {id: this._id, elementRect, writingMode, type, details, context}); } - async setCustomCss(css) { - return await this._invokeHostApi('setCustomCss', {id: this._id, css}); + setCustomCss(css) { + this._invoke('setCustomCss', {id: this._id, css}); } clearAutoPlayTimer() { - this._invokeHostApi('clearAutoPlayTimer', {id: this._id}); + this._invoke('clearAutoPlayTimer', {id: this._id}); } - async setContentScale(scale) { - this._invokeHostApi('setContentScale', {id: this._id, scale}); + setContentScale(scale) { + this._invoke('setContentScale', {id: this._id, scale}); } - async getHostUrl() { - return await this._invokeHostApi('getHostUrl', {}); + async getUrl() { + return await this._invoke('getUrl', {}); } // Private - _invokeHostApi(action, params={}) { + _invoke(action, params={}) { if (typeof this._parentFrameId !== 'number') { return Promise.reject(new Error('Invalid frame')); } - return this._apiSender.invoke(action, params, `popup-proxy-host#${this._parentFrameId}`); + return this._apiSender.invoke(action, params, `popup-factory#${this._parentFrameId}`); } async _updateFrameOffset() { diff --git a/ext/fg/js/popup.js b/ext/fg/js/popup.js index 7db53f0d..79b37251 100644 --- a/ext/fg/js/popup.js +++ b/ext/fg/js/popup.js @@ -139,7 +139,7 @@ class Popup { this._invokeApi('setContent', {type, details}); } - async setCustomCss(css) { + setCustomCss(css) { this._invokeApi('setCustomCss', {css}); } diff --git a/ext/manifest.json b/ext/manifest.json index 3cb634f0..80823fc4 100644 --- a/ext/manifest.json +++ b/ext/manifest.json @@ -44,9 +44,9 @@ "fg/js/frontend-api-receiver.js", "fg/js/popup.js", "fg/js/source.js", + "fg/js/popup-factory.js", "fg/js/frame-offset-forwarder.js", "fg/js/popup-proxy.js", - "fg/js/popup-proxy-host.js", "fg/js/frontend.js", "fg/js/content-script-main.js" ], -- cgit v1.2.3