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,
"sourceType": "module"
},
"env": {
"es6": true,
"browser": true,
"node": true
},
"extends": "google",
"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-loader": "^7.1.4",
"babel-minify-webpack-plugin": "^0.3.0",
"babel-plugin-transform-imports": "^1.5.0",
"babel-preset-env": "^1.6.1",
"clean-webpack-plugin": "^0.1.18",
"compression-webpack-plugin": "^1.1.10",

View File

@ -34,31 +34,32 @@
// TODO: add error bar on fetch failure.
// 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/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/theme.css';
// This causes our custom css to be loaded after the above!
require('./assets/index.css');
import './assets/index.css';
import $ from 'jquery';
import 'jquery-ui/ui/widgets/datepicker';
import 'jquery-ui/ui/widgets/dialog';
// Get ui widgets themselves
import 'jquery-ui/ui/widgets/tooltip';
import Camera from './lib/models/Camera';
import CameraView from './lib/views/CameraView';
import CalendarView from './lib/views/CalendarView';
import VideoDialogView from './lib/views/VideoDialogView';
import NVRSettingsView from './lib/views/NVRSettingsView';
import CheckboxGroupView from './lib/views/CheckboxGroupView';
import RecordingFormatter from './lib/support/RecordingFormatter';
import TimeFormatter, {
TimeStamp90kFormatter,
} from './lib/support/TimeFormatter';
import TimeFormatter from './lib/support/TimeFormatter';
import TimeStamp90kFormatter from './lib/support/TimeStamp90kFormatter';
import MoonfireAPI from './lib/MoonfireAPI';
const api = new MoonfireAPI();
@ -130,27 +131,18 @@ function onSelectVideo(nvrSettingsView, camera, range, recording) {
trimmedRange,
nvrSettingsView.timeStampTrack
);
const video = $('<video controls preload="auto" autoplay="true" />');
const dialog = $('<div class="playdialog" />').append(video);
$('body').append(dialog);
let [formattedStart, formattedEnd] = timeFormatter90k.formatSameDayShortened(
const [
formattedStart,
formattedEnd,
] = timeFormatter90k.formatSameDayShortened(
trimmedRange.startTime90k,
trimmedRange.endTime90k
);
dialog.dialog({
title: camera.shortName + ', ' + formattedStart + ' to ' + formattedEnd,
width: recording.videoSampleEntryWidth / 4,
close: () => {
const videoDOMElement = video[0];
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);
const videoTitle =
camera.shortName + ', ' + formattedStart + ' to ' + formattedEnd;
new VideoDialogView()
.attach($('body'))
.play(videoTitle, recording.videoSampleEntryWidth / 4, url);
}
/**
@ -193,16 +185,19 @@ function fetch(selectedRange, videoLength) {
cameraView.recordingsReq = null;
});
r
.then(function(data, status, req) {
.then(function(data /* , status, req */) {
// Sort recordings in descending order.
data.recordings.sort(function(a, b) {
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;
})
.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))
.done((data) => onReceivedCameras(data))
.fail((req, status, err) => {
console.log('NVR load error: ', status, err);
console.error('NVR load error: ', status, err);
onReceivedCameras({cameras: []});
})
.catch((e) => {
console.log('NVR load exception: ', e);
console.error('NVR load exception: ', e);
onReceivedCameras({cameras: []});
});
}

View File

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

View File

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

View File

@ -59,7 +59,7 @@ export default class MoonfireAPI {
url.hostname = host;
url.port = port;
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/>.
import Time90kParser from '../support/Time90kParser';
import {TimeStamp90kFormatter} from '../support/TimeFormatter';
import TimeStamp90kFormatter from '../support/TimeStamp90kFormatter';
import Range90k from './Range90k';
/**

View File

@ -45,7 +45,7 @@ export default class Range {
*/
constructor(low, high) {
if (high < low) {
console.log('Warning range swap: ' + low + ' - ' + high);
console.warn('Warning range swap: ' + low + ' - ' + high);
[low, high] = [high, low];
}
this.low = low;
@ -65,7 +65,7 @@ export default class Range {
* Determine if value is inside the range.
*
* @param {Number} value Value to test
* @return {Boolean}
* @return {Boolean}
*/
isInRange(value) {
return value >= this.low && value <= this.high;

View File

@ -40,9 +40,10 @@ import Range from './Range';
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 {
/**

View File

@ -32,7 +32,7 @@
import moment from 'moment-timezone';
export const internalTimeFormat = 'YYYY-MM-DDTHH:mm:ss:FFFFFZ';
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);
}
}
/**
* 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/>.
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 CalendarTSRange from '../models/CalendarTSRange';
import {TimeStamp90kFormatter} from '../support/TimeFormatter';
import TimeStamp90kFormatter from '../support/TimeStamp90kFormatter';
import Time90kParser from '../support/Time90kParser';
/**
@ -177,10 +167,11 @@ export default class CalendarView {
if (this._sameDay) {
fromPickerView.option({
dateFormat: $.datepicker.ISO_8601,
dateFormat: DatePickerView.datepicker.ISO_8601,
minDate: minDateStr,
maxDate: maxDateStr,
onSelect: (dateStr, picker) => this._updateRangeDates(dateStr, dateStr),
onSelect: (dateStr /* , picker */) =>
this._updateRangeDates(dateStr, dateStr),
beforeShowDay: beforeShowDay,
disabled: false,
});
@ -188,21 +179,21 @@ export default class CalendarView {
toPickerView.activate(); // Default state, but alive
} else {
fromPickerView.option({
dateFormat: $.datepicker.ISO_8601,
dateFormat: DatePickerView.datepicker.ISO_8601,
minDate: minDateStr,
onSelect: (dateStr, picker) => {
toPickerView.option('minDate', this.fromDateISO);
onSelect: (dateStr /* , picker */) => {
toPickerView.minDate = this.fromDateISO;
this._updateRangeDates(dateStr, this.toDateISO);
},
beforeShowDay: beforeShowDay,
disabled: false,
});
toPickerView.option({
dateFormat: $.datepicker.ISO_8601,
dateFormat: DatePickerView.datepicker.ISO_8601,
minDate: fromPickerView.dateISO,
maxDate: maxDateStr,
onSelect: (dateStr, picker) => {
fromPickerView.option('maxDate', this.toDateISO);
onSelect: (dateStr /* , picker */) => {
fromPickerView.maxDate = this.toDateISO;
this._updateRangeDates(this.fromDateISO, dateStr);
},
beforeShowDay: beforeShowDay,
@ -228,7 +219,7 @@ export default class CalendarView {
* The change requires updating the selected range and then informing
* 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
*/
_pickerTimeChanged(event, isEnd) {
@ -239,7 +230,7 @@ export default class CalendarView {
? selectedRange.setEndTime(newTimeStr)
: selectedRange.setStartTime(newTimeStr);
if (parsedTS === null) {
console.log('bad time change');
console.warn('bad time change');
$(pickerElement).addClass('ui-state-error');
return;
}

View File

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

View File

@ -32,10 +32,27 @@
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.
*/
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.
*
@ -85,11 +102,11 @@ export default class DatePickerView {
*
* @return {Any} Function result
*/
_apply() {
_apply(...args) {
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();
this._recordings.forEach((r) => {
let row = $('<tr class="r" />');
row.append(_columnOrder.map((k) => $('<td/>')));
row.on('click', (e) => {
row.append(_columnOrder.map(() => $('<td/>')));
row.on('click', () => {
console.log('Video clicked');
if (this._clickHandler !== null) {
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: '/',
},
module: {
rules: [{
test: /\.js$/,
loader: 'babel-loader',
query: {
'presets': ['env'],
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
query: {
presets: ['env', {modules: false}],
},
exclude: /(node_modules|bower_components)/,
include: ['./ui-src'],
},
exclude: /(node_modules|bower_components)/,
include: ['./ui-src'],
}, {
test: /\.png$/,
use: ['file-loader'],
}, {
test: /\.ico$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]'
}
}
]
}, {
// Load css and then in-line in head
test: /\.css$/,
loader: 'style-loader!css-loader',
}],
{
test: /\.png$/,
use: ['file-loader'],
},
{
test: /\.ico$/,
use: [
{
loader: 'file-loader',
options: {
name: '[name].[ext]',
},
},
],
},
{
test: /\.png$/,
use: ['file-loader'],
},
{
// Load css and then in-line in head
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
plugins: [
new webpack.DefinePlugin({
@ -83,14 +92,20 @@ module.exports = (env, args) => {
new HtmlWebpackPlugin({
title: nvrSettings.app_title,
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(
/node_modules\/moment\/moment\.js$/,
'./min/moment.min.js'),
'./min/moment.min.js'
),
new webpack.NormalModuleReplacementPlugin(
/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',
optimization: {
minimize: false,
namedChunks: true,
},
devServer: {
@ -54,11 +55,11 @@ module.exports = (env, args) => {
hot: true,
clientLogLevel: 'info',
proxy: {
'/api': `http://${nvrSettings.moonfire.server}:${nvrSettings.moonfire.port}`,
'/api': `http://${nvrSettings.moonfire.server}:${
nvrSettings.moonfire.port
}`,
},
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
],
plugins: [new webpack.HotModuleReplacementPlugin()],
});
};

View File

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

View File

@ -42,34 +42,76 @@ module.exports = (env, args) => {
const nvrSettings = settingsObject.settings;
return settingsObject.webpackMerge(baseConfig, {
//devtool: 'cheap-module-source-map',
devtool: 'cheap-module-source-map',
module: {
rules: [{
test: /\.html$/,
loader: 'html-loader',
query: {
minimize: true,
rules: [
{
test: /\.html$/,
loader: 'html-loader',
query: {
minimize: true,
},
},
}],
],
},
optimization: {
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: {
minSize: 30000,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
maxInitialRequests: 4,
cacheGroups: {
default: {
'default': {
minChunks: 2,
priority: -20,
},
commons: {
name: 'commons',
'jquery-ui': {
test: /[\\/]node_modules[\\/]jquery-ui[\\/]/,
name: 'jquery-ui',
chunks: 'all',
minChunks: 2,
priority: -5,
},
vendors: {
'jquery': {
test: /[\\/]node_modules[\\/]jquery[\\/]/,
name: 'jquery',
chunks: 'all',
priority: -5,
},
'vendors': {
test: /[\\/]node_modules[\\/]/,
name: 'vendor',
chunks: 'all',
@ -89,9 +131,6 @@ module.exports = (env, args) => {
threshold: 10240,
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-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:
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"
@ -1095,7 +1106,7 @@ babel-traverse@^6.24.1, babel-traverse@^6.26.0:
invariant "^2.2.2"
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"
resolved "https://registry.yarnpkg.com/babel-types/-/babel-types-6.26.0.tgz#a3b073f94ab49eb6fa55cd65227a334380632497"
dependencies:
@ -3625,6 +3636,12 @@ is-glob@^4.0.0:
dependencies:
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:
version "2.1.0"
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"
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:
version "1.0.2"
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"
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:
version "4.0.6"
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:
version "4.1.2"
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:
version "4.6.0"
resolved "https://registry.yarnpkg.com/lodash.some/-/lodash.some-4.6.0.tgz#1bb9f314ef6b8baded13b549169b2a945eb68e4d"