A little more UI refactor, cleanup, eslint more strict (#54)

* A little more UI refactor, cleanup, eslint more strict

* Split out imports for jQuery components and put them where needed.
* No longer do all of it in application module.
* Prepares better for code splitting.
* Split out video player dialog
* Simplifies jquery-ui dependencies for code splitting
* Simplifies code
* Configure to generate more, but smaller bundles.
* Setup some more strict eslint settings
* Fix css to import rather than require
* Change settings to correctly support tree shaking in production build

Signed-off-by: Dolf Starreveld <dolf@starreveld.com>

* Remove “old” code from TimeFormatter

* Accidentally left behind due to overlapping PRs

Signed-off-by: Dolf Starreveld <dolf@starreveld.com>
This commit is contained in:
Dolf Starreveld 2018-03-25 22:18:56 -07:00 committed by Scott Lamb
parent eaae640703
commit f5aa0080bb
21 changed files with 434 additions and 186 deletions

View File

@ -3,7 +3,20 @@
"ecmaVersion": 6, "ecmaVersion": 6,
"sourceType": "module" "sourceType": "module"
}, },
"env": {
"es6": true,
"browser": true,
"node": true
},
"extends": "google", "extends": "google",
"rules": { "rules": {
"init-declarations": ["error", "always"],
"no-catch-shadow": ["error"],
"no-delete-var": ["error"],
"no-shadow": ["error", { "builtinGlobals": false, "hoist": "functions", "allow": [] }],
"no-shadow-restricted-names": ["error"],
"no-undef": ["error", {"typeof": true}],
"no-unused-vars": ["error", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }],
"no-use-before-define": ["error", { "functions": true, "classes": true }]
} }
} }

View File

@ -21,6 +21,7 @@
"babel-core": "^6.26.0", "babel-core": "^6.26.0",
"babel-loader": "^7.1.4", "babel-loader": "^7.1.4",
"babel-minify-webpack-plugin": "^0.3.0", "babel-minify-webpack-plugin": "^0.3.0",
"babel-plugin-transform-imports": "^1.5.0",
"babel-preset-env": "^1.6.1", "babel-preset-env": "^1.6.1",
"clean-webpack-plugin": "^0.1.18", "clean-webpack-plugin": "^0.1.18",
"compression-webpack-plugin": "^1.1.10", "compression-webpack-plugin": "^1.1.10",

View File

@ -34,31 +34,32 @@
// TODO: add error bar on fetch failure. // TODO: add error bar on fetch failure.
// TODO: live updating. // TODO: live updating.
import 'jquery-ui/themes/base/button.css'; import $ from 'jquery';
// tooltip needs:
// css.structure: ../../themes/base/core.css
// css.structure: ../../themes/base/tooltip.css
// css.theme: ../../themes/base/theme.css
import 'jquery-ui/themes/base/core.css'; import 'jquery-ui/themes/base/core.css';
import 'jquery-ui/themes/base/datepicker.css';
import 'jquery-ui/themes/base/dialog.css';
import 'jquery-ui/themes/base/resizable.css';
import 'jquery-ui/themes/base/theme.css';
import 'jquery-ui/themes/base/tooltip.css'; import 'jquery-ui/themes/base/tooltip.css';
import 'jquery-ui/themes/base/theme.css';
// This causes our custom css to be loaded after the above! // This causes our custom css to be loaded after the above!
require('./assets/index.css'); import './assets/index.css';
import $ from 'jquery'; // Get ui widgets themselves
import 'jquery-ui/ui/widgets/datepicker';
import 'jquery-ui/ui/widgets/dialog';
import 'jquery-ui/ui/widgets/tooltip'; import 'jquery-ui/ui/widgets/tooltip';
import Camera from './lib/models/Camera'; import Camera from './lib/models/Camera';
import CameraView from './lib/views/CameraView'; import CameraView from './lib/views/CameraView';
import CalendarView from './lib/views/CalendarView'; import CalendarView from './lib/views/CalendarView';
import VideoDialogView from './lib/views/VideoDialogView';
import NVRSettingsView from './lib/views/NVRSettingsView'; import NVRSettingsView from './lib/views/NVRSettingsView';
import CheckboxGroupView from './lib/views/CheckboxGroupView'; import CheckboxGroupView from './lib/views/CheckboxGroupView';
import RecordingFormatter from './lib/support/RecordingFormatter'; import RecordingFormatter from './lib/support/RecordingFormatter';
import TimeFormatter, { import TimeFormatter from './lib/support/TimeFormatter';
TimeStamp90kFormatter, import TimeStamp90kFormatter from './lib/support/TimeStamp90kFormatter';
} from './lib/support/TimeFormatter';
import MoonfireAPI from './lib/MoonfireAPI'; import MoonfireAPI from './lib/MoonfireAPI';
const api = new MoonfireAPI(); const api = new MoonfireAPI();
@ -130,27 +131,18 @@ function onSelectVideo(nvrSettingsView, camera, range, recording) {
trimmedRange, trimmedRange,
nvrSettingsView.timeStampTrack nvrSettingsView.timeStampTrack
); );
const video = $('<video controls preload="auto" autoplay="true" />'); const [
const dialog = $('<div class="playdialog" />').append(video); formattedStart,
$('body').append(dialog); formattedEnd,
] = timeFormatter90k.formatSameDayShortened(
let [formattedStart, formattedEnd] = timeFormatter90k.formatSameDayShortened(
trimmedRange.startTime90k, trimmedRange.startTime90k,
trimmedRange.endTime90k trimmedRange.endTime90k
); );
dialog.dialog({ const videoTitle =
title: camera.shortName + ', ' + formattedStart + ' to ' + formattedEnd, camera.shortName + ', ' + formattedStart + ' to ' + formattedEnd;
width: recording.videoSampleEntryWidth / 4, new VideoDialogView()
close: () => { .attach($('body'))
const videoDOMElement = video[0]; .play(videoTitle, recording.videoSampleEntryWidth / 4, url);
videoDOMElement.pause();
videoDOMElement.src = ''; // Remove current source to stop loading
dialog.remove();
},
});
// Now that dialog is up, set the src so video starts
console.log('Video url: ' + url);
video.attr('src', url);
} }
/** /**
@ -193,16 +185,19 @@ function fetch(selectedRange, videoLength) {
cameraView.recordingsReq = null; cameraView.recordingsReq = null;
}); });
r r
.then(function(data, status, req) { .then(function(data /* , status, req */) {
// Sort recordings in descending order. // Sort recordings in descending order.
data.recordings.sort(function(a, b) { data.recordings.sort(function(a, b) {
return b.startId - a.startId; return b.startId - a.startId;
}); });
console.log('Fetched results > updating recordings'); console.log(
'Fetched results for "%s" > updating recordings',
cameraView.camera.shortName
);
cameraView.recordingsJSON = data.recordings; cameraView.recordingsJSON = data.recordings;
}) })
.catch(function(data, status, err) { .catch(function(data, status, err) {
console.log(url, ' load failed: ', status, ': ', err); console.error(url, ' load failed: ', status, ': ', err);
}); });
} }
} }
@ -295,11 +290,11 @@ export default class NVRApplication {
.request(api.nvrUrl(true)) .request(api.nvrUrl(true))
.done((data) => onReceivedCameras(data)) .done((data) => onReceivedCameras(data))
.fail((req, status, err) => { .fail((req, status, err) => {
console.log('NVR load error: ', status, err); console.error('NVR load error: ', status, err);
onReceivedCameras({cameras: []}); onReceivedCameras({cameras: []});
}) })
.catch((e) => { .catch((e) => {
console.log('NVR load exception: ', e); console.error('NVR load exception: ', e);
onReceivedCameras({cameras: []}); onReceivedCameras({cameras: []});
}); });
} }

View File

@ -4,7 +4,8 @@ body {
#nav { #nav {
float: left; float: left;
} }
.ui-datepicker {
#datetime .ui-datepicker {
width: 100%; width: 100%;
} }

View File

@ -33,6 +33,7 @@
import NVRApplication from './NVRApplication'; import NVRApplication from './NVRApplication';
import $ from 'jquery'; import $ from 'jquery';
import './favicon.ico';
// On document load, start application // On document load, start application
$(function() { $(function() {

View File

@ -59,7 +59,7 @@ export default class MoonfireAPI {
url.hostname = host; url.hostname = host;
url.port = port; url.port = port;
console.log('API: ' + url.origin + url.pathname); console.log('API: ' + url.origin + url.pathname);
this._builder = new URLBuilder(url.origin + url.pathname); this._builder = new URLBuilder(url.origin + url.pathname, relativeUrls);
} }
/** /**

View File

@ -31,7 +31,7 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import Time90kParser from '../support/Time90kParser'; import Time90kParser from '../support/Time90kParser';
import {TimeStamp90kFormatter} from '../support/TimeFormatter'; import TimeStamp90kFormatter from '../support/TimeStamp90kFormatter';
import Range90k from './Range90k'; import Range90k from './Range90k';
/** /**

View File

@ -45,7 +45,7 @@ export default class Range {
*/ */
constructor(low, high) { constructor(low, high) {
if (high < low) { if (high < low) {
console.log('Warning range swap: ' + low + ' - ' + high); console.warn('Warning range swap: ' + low + ' - ' + high);
[low, high] = [high, low]; [low, high] = [high, low];
} }
this.low = low; this.low = low;

View File

@ -40,9 +40,10 @@ import Range from './Range';
let _range = new WeakMap(); let _range = new WeakMap();
/** /**
* Subclass of Range to represent ranges over timestamps in 90k format. * Class like Range to represent ranges over timestamps in 90k format.
* *
* This mostly means added some getters with names that make more sense. * A composed member of the Range class is use for the heavy lifting, while
* this class provides a different interface.
*/ */
export default class Range90k { export default class Range90k {
/** /**

View File

@ -32,7 +32,7 @@
import moment from 'moment-timezone'; import moment from 'moment-timezone';
export const internalTimeFormat = 'YYYY-MM-DDTHH:mm:ss:FFFFFZ';
export const defaultTimeFormat = 'YYYY-MM-DD HH:mm:ss'; export const defaultTimeFormat = 'YYYY-MM-DD HH:mm:ss';
/** /**
@ -117,50 +117,3 @@ export default class TimeFormatter {
return moment.tz(ms, this._tz).format(format); return moment.tz(ms, this._tz).format(format);
} }
} }
/**
* Specialized class similar to TimeFormatter but forcing a specific time format
* for internal usage purposes.
*/
export class TimeStamp90kFormatter {
/**
* Construct from just a timezone specification.
*
* @param {String} tz Timezone
*/
constructor(tz) {
this._formatter = new TimeFormatter(internalTimeFormat, tz);
}
/**
* Format a timestamp in 90k units using internal format.
*
* @param {Number} ts90k timestamp in 90,000ths of a second resolution
* @return {String} Formatted timestamp
*/
formatTimeStamp90k(ts90k) {
return this._formatter.formatTimeStamp90k(ts90k);
}
/**
* Given two timestamp return formatted versions of both, where the second
* one may have been shortened if it falls on the same date as the first one.
*
* @param {Number} ts1 First timestamp in 90k units
* @param {Number} ts2 Secodn timestamp in 90k units
* @return {Array} Array with two elements: [ ts1Formatted, ts2Formatted ]
*/
formatSameDayShortened(ts1, ts2) {
let ts1Formatted = this.formatTimeStamp90k(ts1);
let ts2Formatted = this.formatTimeStamp90k(ts2);
let timePos = this._formatter.formatStr.indexOf('T');
if (timePos != -1) {
const datePortion = ts1Formatted.substr(0, timePos);
ts1Formatted = datePortion + ' ' + ts1Formatted.substr(timePos + 1);
if (ts2Formatted.startsWith(datePortion)) {
ts2Formatted = ts2Formatted.substr(timePos + 1);
}
}
return [ts1Formatted, ts2Formatted];
}
}

View File

@ -0,0 +1,83 @@
// vim: set et sw=2 ts=2:
//
// This file is part of Moonfire NVR, a security camera digital video recorder.
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
//
// 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.
//
// In addition, as a special exception, the copyright holders give
// permission to link the code of portions of this program with the
// OpenSSL library under certain conditions as described in each
// individual source file, and distribute linked combinations including
// the two.
//
// You must obey the GNU General Public License in all respects for all
// of the code used other than OpenSSL. If you modify file(s) with this
// exception, you may extend this exception to your version of the
// file(s), but you are not obligated to do so. If you do not wish to do
// so, delete this exception statement from your version. If you delete
// this exception statement from all source files in the program, then
// also delete it here.
//
// 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 <http://www.gnu.org/licenses/>.
import TimeFormatter from './TimeFormatter';
export const internalTimeFormat = 'YYYY-MM-DDTHH:mm:ss:FFFFFZ';
/**
* Specialized class similar to TimeFormatter but forcing a specific time format
* for internal usage purposes.
*/
export default class TimeStamp90kFormatter {
/**
* Construct from just a timezone specification.
*
* @param {String} tz Timezone
*/
constructor(tz) {
this._formatter = new TimeFormatter(internalTimeFormat, tz);
}
/**
* Format a timestamp in 90k units using internal format.
*
* @param {Number} ts90k timestamp in 90,000ths of a second resolution
* @return {String} Formatted timestamp
*/
formatTimeStamp90k(ts90k) {
return this._formatter.formatTimeStamp90k(ts90k);
}
/**
* Given two timestamp return formatted versions of both, where the second
* one may have been shortened if it falls on the same date as the first one.
*
* @param {Number} ts1 First timestamp in 90k units
* @param {Number} ts2 Secodn timestamp in 90k units
* @return {Array} Array with two elements: [ ts1Formatted, ts2Formatted ]
*/
formatSameDayShortened(ts1, ts2) {
let ts1Formatted = this.formatTimeStamp90k(ts1);
let ts2Formatted = this.formatTimeStamp90k(ts2);
let timePos = this._formatter.formatStr.indexOf('T');
if (timePos != -1) {
const datePortion = ts1Formatted.substr(0, timePos);
ts1Formatted = datePortion + ' ' + ts1Formatted.substr(timePos + 1);
if (ts2Formatted.startsWith(datePortion)) {
ts2Formatted = ts2Formatted.substr(timePos + 1);
}
}
return [ts1Formatted, ts2Formatted];
}
}

View File

@ -31,20 +31,10 @@
// along with this program. If not, see <http://www.gnu.org/licenses/>. // along with this program. If not, see <http://www.gnu.org/licenses/>.
import $ from 'jquery'; import $ from 'jquery';
import 'jquery-ui/themes/base/button.css';
import 'jquery-ui/themes/base/core.css';
import 'jquery-ui/themes/base/datepicker.css';
import 'jquery-ui/themes/base/dialog.css';
import 'jquery-ui/themes/base/resizable.css';
import 'jquery-ui/themes/base/theme.css';
import 'jquery-ui/themes/base/tooltip.css';
import 'jquery-ui/ui/widgets/datepicker';
import 'jquery-ui/ui/widgets/dialog';
import 'jquery-ui/ui/widgets/tooltip';
import DatePickerView from './DatePickerView'; import DatePickerView from './DatePickerView';
import CalendarTSRange from '../models/CalendarTSRange'; import CalendarTSRange from '../models/CalendarTSRange';
import {TimeStamp90kFormatter} from '../support/TimeFormatter'; import TimeStamp90kFormatter from '../support/TimeStamp90kFormatter';
import Time90kParser from '../support/Time90kParser'; import Time90kParser from '../support/Time90kParser';
/** /**
@ -177,10 +167,11 @@ export default class CalendarView {
if (this._sameDay) { if (this._sameDay) {
fromPickerView.option({ fromPickerView.option({
dateFormat: $.datepicker.ISO_8601, dateFormat: DatePickerView.datepicker.ISO_8601,
minDate: minDateStr, minDate: minDateStr,
maxDate: maxDateStr, maxDate: maxDateStr,
onSelect: (dateStr, picker) => this._updateRangeDates(dateStr, dateStr), onSelect: (dateStr /* , picker */) =>
this._updateRangeDates(dateStr, dateStr),
beforeShowDay: beforeShowDay, beforeShowDay: beforeShowDay,
disabled: false, disabled: false,
}); });
@ -188,21 +179,21 @@ export default class CalendarView {
toPickerView.activate(); // Default state, but alive toPickerView.activate(); // Default state, but alive
} else { } else {
fromPickerView.option({ fromPickerView.option({
dateFormat: $.datepicker.ISO_8601, dateFormat: DatePickerView.datepicker.ISO_8601,
minDate: minDateStr, minDate: minDateStr,
onSelect: (dateStr, picker) => { onSelect: (dateStr /* , picker */) => {
toPickerView.option('minDate', this.fromDateISO); toPickerView.minDate = this.fromDateISO;
this._updateRangeDates(dateStr, this.toDateISO); this._updateRangeDates(dateStr, this.toDateISO);
}, },
beforeShowDay: beforeShowDay, beforeShowDay: beforeShowDay,
disabled: false, disabled: false,
}); });
toPickerView.option({ toPickerView.option({
dateFormat: $.datepicker.ISO_8601, dateFormat: DatePickerView.datepicker.ISO_8601,
minDate: fromPickerView.dateISO, minDate: fromPickerView.dateISO,
maxDate: maxDateStr, maxDate: maxDateStr,
onSelect: (dateStr, picker) => { onSelect: (dateStr /* , picker */) => {
fromPickerView.option('maxDate', this.toDateISO); fromPickerView.maxDate = this.toDateISO;
this._updateRangeDates(this.fromDateISO, dateStr); this._updateRangeDates(this.fromDateISO, dateStr);
}, },
beforeShowDay: beforeShowDay, beforeShowDay: beforeShowDay,
@ -228,7 +219,7 @@ export default class CalendarView {
* The change requires updating the selected range and then informing * The change requires updating the selected range and then informing
* any listeners that the range has changed (so they can update data). * any listeners that the range has changed (so they can update data).
* *
* @param {Object} event Time Event on DOM that triggered calling this * @param {event} event DOM event that triggered us
* @param {Boolean} isEnd True if this is for end time * @param {Boolean} isEnd True if this is for end time
*/ */
_pickerTimeChanged(event, isEnd) { _pickerTimeChanged(event, isEnd) {
@ -239,7 +230,7 @@ export default class CalendarView {
? selectedRange.setEndTime(newTimeStr) ? selectedRange.setEndTime(newTimeStr)
: selectedRange.setStartTime(newTimeStr); : selectedRange.setStartTime(newTimeStr);
if (parsedTS === null) { if (parsedTS === null) {
console.log('bad time change'); console.warn('bad time change');
$(pickerElement).addClass('ui-state-error'); $(pickerElement).addClass('ui-state-error');
return; return;
} }

View File

@ -83,7 +83,7 @@ export default class CameraView {
this._enabled = enabled; this._enabled = enabled;
this.recordingsView.show = enabled; this.recordingsView.show = enabled;
console.log( console.log(
'Camera ', 'Camera %s %s',
this.camera.shortName, this.camera.shortName,
this.enabled ? 'enabled' : 'disabled' this.enabled ? 'enabled' : 'disabled'
); );

View File

@ -32,10 +32,27 @@
import $ from 'jquery'; import $ from 'jquery';
import 'jquery-ui/themes/base/core.css';
import 'jquery-ui/themes/base/datepicker.css';
import 'jquery-ui/themes/base/theme.css';
import 'jquery-ui/ui/widgets/datepicker';
/** /**
* Class to encapsulate datepicker UI widget from jQuery. * Class to encapsulate datepicker UI widget from jQuery.
*/ */
export default class DatePickerView { export default class DatePickerView {
/**
* Get the singleton datepicker instance.
*
* This is useful for accessing implementation constants, such as
* date formats etc.
*
* @return {jQuery.datepicker} JQuery datepicker instance
*/
static get datepicker() {
return $.datepicker;
}
/** /**
* Construct wapper an attach to a specified parent DOM node. * Construct wapper an attach to a specified parent DOM node.
* *
@ -85,11 +102,11 @@ export default class DatePickerView {
* *
* @return {Any} Function result * @return {Any} Function result
*/ */
_apply() { _apply(...args) {
if (!this._alive) { if (!this._alive) {
console.log('WARN: datepicker not constructed yet [' + this.domId + ']'); console.warn('datepicker not constructed yet [%s]', this.domId);
} }
return this._pickerElement.datepicker(...arguments); return this._pickerElement.datepicker(...args);
} }
/** /**

View File

@ -267,8 +267,8 @@ export default class RecordingsView {
$('tr.r', tbody).remove(); $('tr.r', tbody).remove();
this._recordings.forEach((r) => { this._recordings.forEach((r) => {
let row = $('<tr class="r" />'); let row = $('<tr class="r" />');
row.append(_columnOrder.map((k) => $('<td/>'))); row.append(_columnOrder.map(() => $('<td/>')));
row.on('click', (e) => { row.on('click', () => {
console.log('Video clicked'); console.log('Video clicked');
if (this._clickHandler !== null) { if (this._clickHandler !== null) {
console.log('Video clicked handler call'); console.log('Video clicked handler call');

View File

@ -0,0 +1,99 @@
// vim: set et sw=2 ts=2:
//
// This file is part of Moonfire NVR, a security camera digital video recorder.
// Copyright (C) 2018 Dolf Starreveld <dolf@starreveld.com>
//
// 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.
//
// In addition, as a special exception, the copyright holders give
// permission to link the code of portions of this program with the
// OpenSSL library under certain conditions as described in each
// individual source file, and distribute linked combinations including
// the two.
//
// You must obey the GNU General Public License in all respects for all
// of the code used other than OpenSSL. If you modify file(s) with this
// exception, you may extend this exception to your version of the
// file(s), but you are not obligated to do so. If you do not wish to do
// so, delete this exception statement from your version. If you delete
// this exception statement from all source files in the program, then
// also delete it here.
//
// 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 <http://www.gnu.org/licenses/>.
import $ from 'jquery';
import 'jquery-ui/themes/base/core.css';
import 'jquery-ui/themes/base/dialog.css';
import 'jquery-ui/themes/base/theme.css';
// This not needed for pure dialog, but we want it resizable
import 'jquery-ui/themes/base/resizable.css';
// Get dialog ui widget
import 'jquery-ui/ui/widgets/dialog';
/**
* Class to implement a simple jQuery dialog based video player.
*/
export default class VideoDialogView {
/**
* Construct the player.
*
* This does not attach the player to the DOM anywhere! In fact, construction
* of the necessary video element is delayed until an attach is requested.
* Since the close of the video removes all traces of it in the DOM, this
* apprach allows repeated use by calling attach again!
*/
constructor() {}
/**
* Attach the player to the specified DOM element.
*
* @param {Node} domElement DOM element to attach to
* @return {VideoDialogView} Returns "this" for chaining.
*/
attach(domElement) {
this._videoElement = $('<video controls preload="auto" autoplay="true" />');
this._dialogElement = $('<div class="playdialog" />').append(
this._videoElement
);
$(domElement).append(this._dialogElement);
return this;
}
/**
* Show the player, and start playing the given url.
*
* @param {String} title Title of the video player
* @param {Number} width Width of the player
* @param {String} url URL for source media
* @return {VideoDialogView} Returns "this" for chaining.
*/
play(title, width, url) {
this._dialogElement.dialog({
title: title,
width: width,
close: () => {
const videoDOMElement = this._videoElement[0];
videoDOMElement.pause();
videoDOMElement.src = ''; // Remove current source to stop loading
this._videoElement = null;
this._dialogElement.remove();
this._dialogElement = null;
},
});
// Now that dialog is up, set the src so video starts
console.log('Video url: ' + url);
this._videoElement.attr('src', url);
return this;
}
}

View File

@ -48,32 +48,41 @@ module.exports = (env, args) => {
publicPath: '/', publicPath: '/',
}, },
module: { module: {
rules: [{ rules: [
{
test: /\.js$/, test: /\.js$/,
loader: 'babel-loader', loader: 'babel-loader',
query: { query: {
'presets': ['env'], presets: ['env', {modules: false}],
}, },
exclude: /(node_modules|bower_components)/, exclude: /(node_modules|bower_components)/,
include: ['./ui-src'], include: ['./ui-src'],
}, { },
{
test: /\.png$/, test: /\.png$/,
use: ['file-loader'], use: ['file-loader'],
}, { },
{
test: /\.ico$/, test: /\.ico$/,
use: [ use: [
{ {
loader: 'file-loader', loader: 'file-loader',
options: { options: {
name: '[name].[ext]' name: '[name].[ext]',
} },
} },
] ],
}, { },
{
test: /\.png$/,
use: ['file-loader'],
},
{
// Load css and then in-line in head // Load css and then in-line in head
test: /\.css$/, test: /\.css$/,
loader: 'style-loader!css-loader', use: ['style-loader', 'css-loader'],
}], },
],
}, },
plugins: [ plugins: [
new webpack.DefinePlugin({ new webpack.DefinePlugin({
@ -83,14 +92,20 @@ module.exports = (env, args) => {
new HtmlWebpackPlugin({ new HtmlWebpackPlugin({
title: nvrSettings.app_title, title: nvrSettings.app_title,
filename: 'index.html', filename: 'index.html',
template: path.join(nvrSettings._paths.app_src_dir, 'assets', 'index.html'), template: path.join(
nvrSettings._paths.app_src_dir,
'assets',
'index.html'
),
}), }),
new webpack.NormalModuleReplacementPlugin( new webpack.NormalModuleReplacementPlugin(
/node_modules\/moment\/moment\.js$/, /node_modules\/moment\/moment\.js$/,
'./min/moment.min.js'), './min/moment.min.js'
),
new webpack.NormalModuleReplacementPlugin( new webpack.NormalModuleReplacementPlugin(
/node_modules\/moment-timezone\/index\.js$/, /node_modules\/moment-timezone\/index\.js$/,
'./builds/moment-timezone-with-data-2012-2022.min.js'), './builds/moment-timezone-with-data-2012-2022.min.js'
),
], ],
}; };
}; };

View File

@ -44,6 +44,7 @@ module.exports = (env, args) => {
}, },
devtool: 'inline-source-map', devtool: 'inline-source-map',
optimization: { optimization: {
minimize: false,
namedChunks: true, namedChunks: true,
}, },
devServer: { devServer: {
@ -54,11 +55,11 @@ module.exports = (env, args) => {
hot: true, hot: true,
clientLogLevel: 'info', clientLogLevel: 'info',
proxy: { proxy: {
'/api': `http://${nvrSettings.moonfire.server}:${nvrSettings.moonfire.port}`, '/api': `http://${nvrSettings.moonfire.server}:${
nvrSettings.moonfire.port
}`,
}, },
}, },
plugins: [ plugins: [new webpack.HotModuleReplacementPlugin()],
new webpack.HotModuleReplacementPlugin(),
],
}); });
}; };

View File

@ -42,34 +42,34 @@ const merge = require('webpack-merge');
* found), we throw an exception. * found), we throw an exception.
* *
* If the module that is require-d is a function, it will be executed, * If the module that is require-d is a function, it will be executed,
* passing the "env" and "args" parameters from the settingsConfig to it. * passing the "env" and "args" parameters to it.
* The function should return a map. * The function should return a map.
* *
* @param {String} path Path to be passed to require() * @param {String} requiredPath Path to be passed to require()
* @param {object} settingsConfig Settings passed to new Settings() * @param {object} env webpack's "env" on invocation
* @param {object} args webpack's "args" on invocation (options)
* @param {Boolean} optional True file not to exist * @param {Boolean} optional True file not to exist
* @return {object} The module, or {} if not found (optional) * @return {object} The module, or {} if not found (optional)
*/ */
function requireHelper(path, settingsConfig, optional) { function requireHelper(requiredPath, env, args, optional) {
let module = {}; let module = {};
try { try {
require.resolve(path); // Throws if not found require.resolve(requiredPath); // Throws if not found
try { try {
module = require(path); module = require(requiredPath);
if (typeof(module) === 'function') { if (typeof module === 'function') {
module = module(settingsConfig.env, settingsConfig.args); module = module(env, args);
} }
// Get owned properties only: now a literal map // Get owned properties only: now a literal map
module = Object.assign({}, require(path).settings); module = Object.assign({}, require(requiredPath).settings);
} catch (e) { } catch (e) {
throw new Error('Settings file (' + path + ') has errors.'); throw new Error('Settings file (' + requiredPath + ') has errors.');
} }
} catch (e) { } catch (e) {
if (!optional) { if (!optional) {
throw new Error('Settings file (' + path + ') not found.'); throw new Error('Settings file (' + requiredPath + ') not found.');
} }
} }
const args = settingsConfig.args;
const webpackMode = (args ? args.mode : null) || 'none'; const webpackMode = (args ? args.mode : null) || 'none';
const modes = module.webpack_mode || {}; const modes = module.webpack_mode || {};
delete module.webpack_mode; // Not modifying original module. We have a copy! delete module.webpack_mode; // Not modifying original module. We have a copy!
@ -188,13 +188,14 @@ class Settings {
const secondaryPath = path.resolve(projectRoot, secondaryFile); const secondaryPath = path.resolve(projectRoot, secondaryFile);
// Check if we can resolve the primary file and if we can, require it. // Check if we can resolve the primary file and if we can, require it.
const _settings = const _settings = requireHelper(primaryPath, env, args, optional);
requireHelper(primaryPath, this.settings_config, optional);
// Merge secondary override file, if it exists // Merge secondary override file, if it exists
this.settings = merge(_settings, this.settings = merge(
requireHelper(secondaryPath, this.settings_config, true)); _settings,
}; requireHelper(secondaryPath, env, args, true)
);
}
/** /**
* Take one or more webpack configurations and merge them. * Take one or more webpack configurations and merge them.
@ -212,12 +213,14 @@ class Settings {
*/ */
webpackMerge(...packs) { webpackMerge(...packs) {
const unpack = (webpackConfig) => { const unpack = (webpackConfig) => {
if ((typeof(webpackConfig) === 'string') || if (
(webpackConfig instanceof String)) { typeof webpackConfig === 'string' ||
webpackConfig instanceof String
) {
webpackConfig = require(webpackConfig); webpackConfig = require(webpackConfig);
} }
const config = this.settings_config; const config = this.settings_config;
if (typeof(webpackConfig) === 'function') { if (typeof webpackConfig === 'function') {
return webpackConfig(config.env, config.args); return webpackConfig(config.env, config.args);
} }
return webpackConfig; return webpackConfig;

View File

@ -42,34 +42,76 @@ module.exports = (env, args) => {
const nvrSettings = settingsObject.settings; const nvrSettings = settingsObject.settings;
return settingsObject.webpackMerge(baseConfig, { return settingsObject.webpackMerge(baseConfig, {
//devtool: 'cheap-module-source-map', devtool: 'cheap-module-source-map',
module: { module: {
rules: [{ rules: [
{
test: /\.html$/, test: /\.html$/,
loader: 'html-loader', loader: 'html-loader',
query: { query: {
minimize: true, minimize: true,
}, },
}], },
],
}, },
optimization: { optimization: {
minimize: true, minimize: true,
minimizer: [
{
apply: (compiler) => {
/**
* Setup the UglifyJsPlugin as webpack4 does, plus options
* we decide to override.
*/
// Lazy load the uglifyjs plugin
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
new UglifyJsPlugin({
cache: true, // webpack4: default
parallel: true, // webpack4: default
sourceMap:
(args.devtool && /source-?map/.test(args.devtool)) ||
(args.plugins &&
args.plugins.some(
(p) => p instanceof webpack.SourceMapDevToolPlugin
)),
uglifyOptions: {
compress: {
drop_console: true, // Remove all console.log etc.
keep_infinity: true, // Do not change to 1/0
warnings: false, // Do not warn when dropping
},
output: {
// Eliminate most comments, but not marked ones
comments: 'some',
},
},
}).apply(compiler);
},
},
],
splitChunks: { splitChunks: {
minSize: 30000, minSize: 30000,
minChunks: 1, minChunks: 1,
maxAsyncRequests: 5, maxAsyncRequests: 5,
maxInitialRequests: 3, maxInitialRequests: 4,
cacheGroups: { cacheGroups: {
default: { 'default': {
minChunks: 2, minChunks: 2,
priority: -20, priority: -20,
}, },
commons: { 'jquery-ui': {
name: 'commons', test: /[\\/]node_modules[\\/]jquery-ui[\\/]/,
name: 'jquery-ui',
chunks: 'all', chunks: 'all',
minChunks: 2, priority: -5,
}, },
vendors: { 'jquery': {
test: /[\\/]node_modules[\\/]jquery[\\/]/,
name: 'jquery',
chunks: 'all',
priority: -5,
},
'vendors': {
test: /[\\/]node_modules[\\/]/, test: /[\\/]node_modules[\\/]/,
name: 'vendor', name: 'vendor',
chunks: 'all', chunks: 'all',
@ -89,9 +131,6 @@ module.exports = (env, args) => {
threshold: 10240, threshold: 10240,
minRatio: 0.8, minRatio: 0.8,
}), }),
new webpack.NormalModuleReplacementPlugin(
/node_modules\/jquery\/dist\/jquery\.js$/,
'./jquery.min.js'),
], ],
}); });
}; };

View File

@ -865,6 +865,17 @@ babel-plugin-transform-flow-strip-types@^6.8.0:
babel-plugin-syntax-flow "^6.18.0" babel-plugin-syntax-flow "^6.18.0"
babel-runtime "^6.22.0" babel-runtime "^6.22.0"
babel-plugin-transform-imports@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-imports/-/babel-plugin-transform-imports-1.5.0.tgz#3105082ab489b1cee162e42d2ffe7b8f7c685f2e"
dependencies:
babel-types "^6.6.0"
is-valid-path "^0.1.1"
lodash.camelcase "^4.3.0"
lodash.findkey "^4.6.0"
lodash.kebabcase "^4.1.1"
lodash.snakecase "^4.1.1"
babel-plugin-transform-inline-consecutive-adds@^0.3.0: babel-plugin-transform-inline-consecutive-adds@^0.3.0:
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/babel-plugin-transform-inline-consecutive-adds/-/babel-plugin-transform-inline-consecutive-adds-0.3.0.tgz#f07d93689c0002ed2b2b62969bdd99f734e03f57" resolved "https://registry.yarnpkg.com/babel-plugin-transform-inline-consecutive-adds/-/babel-plugin-transform-inline-consecutive-adds-0.3.0.tgz#f07d93689c0002ed2b2b62969bdd99f734e03f57"
@ -1095,7 +1106,7 @@ babel-traverse@^6.24.1, babel-traverse@^6.26.0:
invariant "^2.2.2" invariant "^2.2.2"
lodash "^4.17.4" lodash "^4.17.4"
babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0: babel-types@^6.19.0, babel-types@^6.24.1, babel-types@^6.26.0, babel-types@^6.6.0:
version "6.26.0" version "6.26.0"
resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497" resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
dependencies: dependencies:
@ -3625,6 +3636,12 @@ is-glob@^4.0.0:
dependencies: dependencies:
is-extglob "^2.1.1" is-extglob "^2.1.1"
is-invalid-path@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/is-invalid-path/-/is-invalid-path-0.1.0.tgz#307a855b3cf1a938b44ea70d2c61106053714f34"
dependencies:
is-glob "^2.0.0"
is-number@^2.1.0: is-number@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f"
@ -3737,6 +3754,12 @@ is-utf8@^0.2.0:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72" resolved "https://registry.yarnpkg.com/is-utf8/-/is-utf8-0.2.1.tgz#4b0da1442104d1b336340e80797e865cf39f7d72"
is-valid-path@^0.1.1:
version "0.1.1"
resolved "https://registry.yarnpkg.com/is-valid-path/-/is-valid-path-0.1.1.tgz#110f9ff74c37f663e1ec7915eb451f2db93ac9df"
dependencies:
is-invalid-path "^0.1.0"
is-windows@^1.0.1, is-windows@^1.0.2: is-windows@^1.0.1, is-windows@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-1.0.2.tgz#d1850eb9791ecd18e6182ce12a30f396634bb19d"
@ -4043,14 +4066,26 @@ lodash.camelcase@^4.3.0:
version "4.3.0" version "4.3.0"
resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6" resolved "https://registry.yarnpkg.com/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz#b28aa6288a2b9fc651035c7711f65ab6190331a6"
lodash.findkey@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.findkey/-/lodash.findkey-4.6.0.tgz#83058e903b51cbb759d09ccf546dea3ea39c4718"
lodash.isplainobject@^4.0.6: lodash.isplainobject@^4.0.6:
version "4.0.6" version "4.0.6"
resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb" resolved "https://registry.yarnpkg.com/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz#7c526a52d89b45c45cc690b88163be0497f550cb"
lodash.kebabcase@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz#8489b1cb0d29ff88195cceca448ff6d6cc295c36"
lodash.memoize@^4.1.2: lodash.memoize@^4.1.2:
version "4.1.2" version "4.1.2"
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
lodash.snakecase@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz#39d714a35357147837aefd64b5dcbb16becd8f8d"
lodash.some@^4.6.0: lodash.some@^4.6.0:
version "4.6.0" version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d" resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d"