Electron support
From https://github.com/vector-im/vector-web/pull/2511 but with just the actual electron changes
This commit is contained in:
parent
8c3fed7559
commit
caa3cb7d89
9 changed files with 481 additions and 3 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -13,3 +13,4 @@
|
||||||
/vector/olm.*
|
/vector/olm.*
|
||||||
.DS_Store
|
.DS_Store
|
||||||
npm-debug.log
|
npm-debug.log
|
||||||
|
electron/dist
|
||||||
|
|
BIN
electron/build/icon.icns
Normal file
BIN
electron/build/icon.icns
Normal file
Binary file not shown.
BIN
electron/build/icon.ico
Normal file
BIN
electron/build/icon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
157
electron/src/electron-main.js
Normal file
157
electron/src/electron-main.js
Normal file
|
@ -0,0 +1,157 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2016 Aviral Dasgupta and OpenMarket Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const electron = require('electron');
|
||||||
|
const url = require('url');
|
||||||
|
|
||||||
|
const VectorMenu = require('./vectormenu');
|
||||||
|
|
||||||
|
const PERMITTED_URL_SCHEMES = [
|
||||||
|
'http:',
|
||||||
|
'https:',
|
||||||
|
'mailto:',
|
||||||
|
];
|
||||||
|
|
||||||
|
const UPDATE_POLL_INTERVAL_MS = 60 * 60 * 1000;
|
||||||
|
|
||||||
|
let mainWindow = null;
|
||||||
|
let appQuitting = false;
|
||||||
|
|
||||||
|
function safeOpenURL(target) {
|
||||||
|
// openExternal passes the target to open/start/xdg-open,
|
||||||
|
// so put fairly stringent limits on what can be opened
|
||||||
|
// (for instance, open /bin/sh does indeed open a terminal
|
||||||
|
// with a shell, albeit with no arguments)
|
||||||
|
const parsed_url = url.parse(target);
|
||||||
|
if (PERMITTED_URL_SCHEMES.indexOf(parsed_url.protocol) > -1) {
|
||||||
|
// explicitly use the URL re-assembled by the url library,
|
||||||
|
// so we know the url parser has understood all the parts
|
||||||
|
// of the input string
|
||||||
|
const new_target = url.format(parsed_url);
|
||||||
|
electron.shell.openExternal(new_target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onWindowOrNavigate(ev, target) {
|
||||||
|
// always prevent the default: if something goes wrong,
|
||||||
|
// we don't want to end up opening it in the electron
|
||||||
|
// app, as we could end up opening any sort of random
|
||||||
|
// url in a window that has node scripting access.
|
||||||
|
ev.preventDefault();
|
||||||
|
safeOpenURL(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onLinkContextMenu(ev, params) {
|
||||||
|
const popup_menu = new electron.Menu();
|
||||||
|
popup_menu.append(new electron.MenuItem({
|
||||||
|
label: params.linkURL,
|
||||||
|
click() {
|
||||||
|
safeOpenURL(params.linkURL);
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
popup_menu.popup();
|
||||||
|
ev.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
function installUpdate() {
|
||||||
|
// for some reason, quitAndInstall does not fire the
|
||||||
|
// before-quit event, so we need to set the flag here.
|
||||||
|
appQuitting = true;
|
||||||
|
electron.autoUpdater.quitAndInstall();
|
||||||
|
}
|
||||||
|
|
||||||
|
function pollForUpdates() {
|
||||||
|
try {
|
||||||
|
electron.autoUpdater.checkForUpdates();
|
||||||
|
} catch (e) {
|
||||||
|
console.log("Couldn't check for update", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
electron.ipcMain.on('install_update', installUpdate);
|
||||||
|
|
||||||
|
electron.app.on('ready', () => {
|
||||||
|
try {
|
||||||
|
// For reasons best known to Squirrel, the way it checks for updates
|
||||||
|
// is completely different between macOS and windows. On macOS, it
|
||||||
|
// hits a URL that either gives it a 200 with some json or
|
||||||
|
// 204 No Content. On windows it takes a base path and looks for
|
||||||
|
// files under that path.
|
||||||
|
if (process.platform == 'darwin') {
|
||||||
|
electron.autoUpdater.setFeedURL("https://riot.im/autoupdate/desktop/");
|
||||||
|
} else if (process.platform == 'win32') {
|
||||||
|
electron.autoUpdater.setFeedURL("https://riot.im/download/desktop/win32/");
|
||||||
|
} else {
|
||||||
|
// Squirrel / electron only supports auto-update on these two platforms.
|
||||||
|
// I'm not even going to try to guess which feed style they'd use if they
|
||||||
|
// implemented it on Linux, or if it would be different again.
|
||||||
|
console.log("Auto update not supported on this platform");
|
||||||
|
}
|
||||||
|
// We check for updates ourselves rather than using 'updater' because we need to
|
||||||
|
// do it in the main process (and we don't really need to check every 10 minutes:
|
||||||
|
// every hour should be just fine for a desktop app)
|
||||||
|
// However, we still let the main window listen for the update events.
|
||||||
|
pollForUpdates();
|
||||||
|
setInterval(pollForUpdates, UPDATE_POLL_INTERVAL_MS);
|
||||||
|
} catch (err) {
|
||||||
|
// will fail if running in debug mode
|
||||||
|
console.log("Couldn't enable update checking", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
mainWindow = new electron.BrowserWindow({
|
||||||
|
icon: `${__dirname}/../../vector/img/logo.png`,
|
||||||
|
width: 1024, height: 768,
|
||||||
|
});
|
||||||
|
mainWindow.loadURL(`file://${__dirname}/../../vector/index.html`);
|
||||||
|
electron.Menu.setApplicationMenu(VectorMenu);
|
||||||
|
|
||||||
|
mainWindow.on('closed', () => {
|
||||||
|
mainWindow = null;
|
||||||
|
});
|
||||||
|
mainWindow.on('close', (e) => {
|
||||||
|
if (process.platform == 'darwin' && !appQuitting) {
|
||||||
|
// On Mac, closing the window just hides it
|
||||||
|
// (this is generally how single-window Mac apps
|
||||||
|
// behave, eg. Mail.app)
|
||||||
|
e.preventDefault();
|
||||||
|
mainWindow.hide();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
mainWindow.webContents.on('new-window', onWindowOrNavigate);
|
||||||
|
mainWindow.webContents.on('will-navigate', onWindowOrNavigate);
|
||||||
|
|
||||||
|
mainWindow.webContents.on('context-menu', function(ev, params) {
|
||||||
|
if (params.linkURL) {
|
||||||
|
onLinkContextMenu(ev, params);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
electron.app.on('window-all-closed', () => {
|
||||||
|
electron.app.quit();
|
||||||
|
});
|
||||||
|
|
||||||
|
electron.app.on('activate', () => {
|
||||||
|
mainWindow.show();
|
||||||
|
});
|
||||||
|
|
||||||
|
electron.app.on('before-quit', () => {
|
||||||
|
appQuitting = true;
|
||||||
|
});
|
184
electron/src/vectormenu.js
Normal file
184
electron/src/vectormenu.js
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
/*
|
||||||
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const electron = require('electron');
|
||||||
|
|
||||||
|
// Menu template from http://electron.atom.io/docs/api/menu/, edited
|
||||||
|
const template = [
|
||||||
|
{
|
||||||
|
label: 'Edit',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
role: 'undo'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'redo'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'cut'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'copy'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'paste'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'pasteandmatchstyle'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'delete'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'selectall'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'View',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'resetzoom'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'zoomin'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'zoomout'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'togglefullscreen'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Toggle Developer Tools',
|
||||||
|
accelerator: process.platform == 'darwin' ? 'Alt+Command+I' : 'Ctrl+Shift+I',
|
||||||
|
click: function(item, focusedWindow) {
|
||||||
|
if (focusedWindow) focusedWindow.toggleDevTools();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'window',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
role: 'minimize'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'close'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'help',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
label: 'riot.im',
|
||||||
|
click () { electron.shell.openExternal('https://riot.im/') }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
const name = electron.app.getName()
|
||||||
|
template.unshift({
|
||||||
|
label: name,
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
role: 'about'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'services',
|
||||||
|
submenu: []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'hide'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'hideothers'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'unhide'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'quit'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
// Edit menu.
|
||||||
|
template[1].submenu.push(
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Speech',
|
||||||
|
submenu: [
|
||||||
|
{
|
||||||
|
role: 'startspeaking'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'stopspeaking'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// Window menu.
|
||||||
|
template[3].submenu = [
|
||||||
|
{
|
||||||
|
label: 'Close',
|
||||||
|
accelerator: 'CmdOrCtrl+W',
|
||||||
|
role: 'close'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Minimize',
|
||||||
|
accelerator: 'CmdOrCtrl+M',
|
||||||
|
role: 'minimize'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Zoom',
|
||||||
|
role: 'zoom'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'separator'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Bring All to Front',
|
||||||
|
role: 'front'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = electron.Menu.buildFromTemplate(template)
|
||||||
|
|
27
package.json
27
package.json
|
@ -1,8 +1,10 @@
|
||||||
{
|
{
|
||||||
"name": "vector-web",
|
"name": "vector-web",
|
||||||
|
"productName": "Riot",
|
||||||
|
"main": "electron/src/electron-main.js",
|
||||||
"version": "0.8.4-rc.2",
|
"version": "0.8.4-rc.2",
|
||||||
"description": "Vector webapp",
|
"description": "A feature-rich client for Matrix.org",
|
||||||
"author": "matrix.org",
|
"author": "Vector Creations Ltd.",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://github.com/vector-im/vector-web"
|
"url": "https://github.com/vector-im/vector-web"
|
||||||
|
@ -31,6 +33,7 @@
|
||||||
"build:compile": "babel --source-maps -d lib src",
|
"build:compile": "babel --source-maps -d lib src",
|
||||||
"build:bundle": "NODE_ENV=production webpack -p --progress",
|
"build:bundle": "NODE_ENV=production webpack -p --progress",
|
||||||
"build:bundle:dev": "webpack --optimize-occurence-order --progress",
|
"build:bundle:dev": "webpack --optimize-occurence-order --progress",
|
||||||
|
"build:electron": "build -lwm",
|
||||||
"build": "node scripts/babelcheck.js && npm run build:emojione && npm run build:css && npm run build:bundle",
|
"build": "node scripts/babelcheck.js && npm run build:emojione && npm run build:css && npm run build:bundle",
|
||||||
"build:dev": "npm run build:emojione && npm run build:css && npm run build:bundle:dev",
|
"build:dev": "npm run build:emojione && npm run build:css && npm run build:bundle:dev",
|
||||||
"dist": "scripts/package.sh",
|
"dist": "scripts/package.sh",
|
||||||
|
@ -91,6 +94,7 @@
|
||||||
"catw": "^1.0.1",
|
"catw": "^1.0.1",
|
||||||
"cpx": "^1.3.2",
|
"cpx": "^1.3.2",
|
||||||
"css-raw-loader": "^0.1.1",
|
"css-raw-loader": "^0.1.1",
|
||||||
|
"electron-builder": "^7.10.2",
|
||||||
"emojione": "^2.2.3",
|
"emojione": "^2.2.3",
|
||||||
"expect": "^1.16.0",
|
"expect": "^1.16.0",
|
||||||
"fs-extra": "^0.30.0",
|
"fs-extra": "^0.30.0",
|
||||||
|
@ -117,5 +121,24 @@
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"olm": "https://matrix.org/packages/npm/olm/olm-2.0.0.tgz"
|
"olm": "https://matrix.org/packages/npm/olm/olm-2.0.0.tgz"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"appId": "im.riot.app",
|
||||||
|
"category": "Network",
|
||||||
|
"electronVersion": "1.4.2",
|
||||||
|
"//asar=false": "https://github.com/electron-userland/electron-builder/issues/675",
|
||||||
|
"asar": false,
|
||||||
|
"dereference": true,
|
||||||
|
"//files": "We bundle everything, so we only need to include vector/",
|
||||||
|
"files": [
|
||||||
|
"!**/*",
|
||||||
|
"electron/src/**",
|
||||||
|
"vector/**",
|
||||||
|
"package.json"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"directories": {
|
||||||
|
"buildResources": "electron/build",
|
||||||
|
"output": "electron/dist"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
102
src/vector/platform/ElectronPlatform.js
Normal file
102
src/vector/platform/ElectronPlatform.js
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
// @flow
|
||||||
|
|
||||||
|
/*
|
||||||
|
Copyright 2016 Aviral Dasgupta
|
||||||
|
Copyright 2016 OpenMarket Ltd
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import VectorBasePlatform from './VectorBasePlatform';
|
||||||
|
import dis from 'matrix-react-sdk/lib/dispatcher';
|
||||||
|
|
||||||
|
function onUpdateDownloaded(ev, releaseNotes, ver, date, updateURL) {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'new_version',
|
||||||
|
currentVersion: electron.remote.app.getVersion(),
|
||||||
|
newVersion: ver,
|
||||||
|
releaseNotes: releaseNotes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// index.js imports us unconditionally, so we need this check here as well
|
||||||
|
let electron = null, remote = null;
|
||||||
|
if (window && window.process && window.process && window.process.type === 'renderer') {
|
||||||
|
electron = require('electron');
|
||||||
|
electron.remote.autoUpdater.on('update-downloaded', onUpdateDownloaded);
|
||||||
|
remote = electron.remote;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class ElectronPlatform extends VectorBasePlatform {
|
||||||
|
setNotificationCount(count: number) {
|
||||||
|
super.setNotificationCount(count);
|
||||||
|
// this sometimes throws because electron is made of fail:
|
||||||
|
// https://github.com/electron/electron/issues/7351
|
||||||
|
// For now, let's catch the error, but I suspect it may
|
||||||
|
// continue to fail and we might just have to accept that
|
||||||
|
// electron's remote RPC is a non-starter for now and use IPC
|
||||||
|
try {
|
||||||
|
remote.app.setBadgeCount(count);
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Failed to set notification count", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
supportsNotifications() : boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
maySendNotifications() : boolean {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
displayNotification(title: string, msg: string, avatarUrl: string): Notification {
|
||||||
|
// Notifications in Electron use the HTML5 notification API
|
||||||
|
const notification = new global.Notification(
|
||||||
|
title,
|
||||||
|
{
|
||||||
|
body: msg,
|
||||||
|
icon: avatarUrl,
|
||||||
|
tag: "vector",
|
||||||
|
silent: true, // we play our own sounds
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
notification.onclick = function() {
|
||||||
|
dis.dispatch({
|
||||||
|
action: 'view_room',
|
||||||
|
room_id: room.roomId
|
||||||
|
});
|
||||||
|
global.focus();
|
||||||
|
};
|
||||||
|
|
||||||
|
return notification;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearNotification(notif: Notification) {
|
||||||
|
notif.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
pollForUpdate() {
|
||||||
|
// In electron we control the update process ourselves, since
|
||||||
|
// it needs to run in the main process, so we just run the timer
|
||||||
|
// loop in the main electron process instead.
|
||||||
|
}
|
||||||
|
|
||||||
|
installUpdate() {
|
||||||
|
// IPC to the main process to install the update, since quitAndInstall
|
||||||
|
// doesn't fire the before-quit event so the main process needs to know
|
||||||
|
// it should exit.
|
||||||
|
electron.ipcRenderer.send('install_update');
|
||||||
|
}
|
||||||
|
}
|
|
@ -16,8 +16,16 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import ElectronPlatform from './ElectronPlatform';
|
||||||
import WebPlatform from './WebPlatform';
|
import WebPlatform from './WebPlatform';
|
||||||
|
|
||||||
let Platform = WebPlatform;
|
let Platform = null;
|
||||||
|
|
||||||
|
if (window && window.process && window.process && window.process.type === 'renderer') {
|
||||||
|
// we're running inside electron
|
||||||
|
Platform = ElectronPlatform;
|
||||||
|
} else {
|
||||||
|
Platform = WebPlatform;
|
||||||
|
}
|
||||||
|
|
||||||
export default Platform;
|
export default Platform;
|
||||||
|
|
|
@ -75,6 +75,9 @@ module.exports = {
|
||||||
},
|
},
|
||||||
externals: {
|
externals: {
|
||||||
"olm": "Olm",
|
"olm": "Olm",
|
||||||
|
// Don't try to bundle electron: leave it as a commonjs dependency
|
||||||
|
// (the 'commonjs' here means it will output a 'require')
|
||||||
|
"electron": "commonjs electron",
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
new webpack.DefinePlugin({
|
new webpack.DefinePlugin({
|
||||||
|
|
Loading…
Reference in a new issue