Skip to content

Commit

Permalink
Merge pull request #35 from homer0/homer0_runner
Browse files Browse the repository at this point in the history
Refactor the dev server custom plugin and goodbye webpack-node-utils!
  • Loading branch information
Leonardo Apiwan committed Jun 30, 2018
2 parents f7c6c3c + 8d20947 commit 746005e
Show file tree
Hide file tree
Showing 15 changed files with 1,329 additions and 410 deletions.
1 change: 0 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
"webpack": "4.12.0",
"webpack-cli": "3.0.8",
"webpack-dev-server": "3.1.4",
"webpack-node-utils": "3.0.0",
"mini-css-extract-plugin": "0.4.0",
"html-webpack-plugin": "3.2.0",
"script-ext-html-webpack-plugin": "2.0.1",
Expand Down
203 changes: 203 additions & 0 deletions src/plugins/bundleRunner/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
const path = require('path');
const { fork } = require('child_process');
const extend = require('extend');
const ProjextWebpackUtils = require('../utils');
/**
* This is a webpack plugin that executes a Node bundle when it finishes compiling.
*/
class ProjextWebpackBundleRunner {
/**
* @param {ProjextWebpackBundleRunnerOptions} [options={}] Settings to customize the plugin
* behaviour.
*/
constructor(options = {}) {
/**
* The plugin options.
* @type {ProjextWebpackBundleRunnerOptions}
* @access protected
* @ignore
*/
this._options = extend(
true,
{
entry: null,
name: 'projext-webpack-plugin-bundle-runner',
logger: null,
},
options
);
/**
* A logger to output the plugin's information messages.
* @type {Logger}
* @access protected
* @ignore
*/
this._logger = ProjextWebpackUtils.createLogger(this._options.name, this._options.logger);
/**
* The absolute path for the entry file that will be executed. This will be assigned after
* webpack emits its assets.
* @type {?string}
* @access protected
* @ignore
*/
this._entryPath = null;
/**
* Once the bundle is executed, this property will hold the reference for the process.
* @type {?ChildProcess}
* @access protected
* @ignore
*/
this._instance = null;
/**
* This flag is used during the process in which the plugin finds the file to execute.
* Since the process runs every time webpack emits assets, which happens every time the build
* is updated, the flag is needed in order to prevent it from running more than once.
* @type {boolean}
* @access protected
* @ignore
*/
this._setupReady = false;
}
/**
* Gets the plugin options.
* @return {ProjextWebpackBundleRunnerOptions}
*/
getOptions() {
return this._options;
}
/**
* This is called by webpack when the plugin is being processed. The method takes care of adding
* the required listener to the webpack hooks in order to get the file, execute it and stop it.
* @param {Object} compiler The compiler information provided by webpack.
*/
apply(compiler) {
const { name } = this._options;
compiler.hooks.afterEmit.tapAsync(name, this._onAssetsEmitted.bind(this));
compiler.hooks.compile.tap(name, this._onCompilationStarts.bind(this));
compiler.hooks.done.tap(name, this._onCompilationEnds.bind(this));
}
/**
* This is called by webpack every time assets are emitted. The first time the method is called,
* it will validate the entries and try to obtain the path to the file that will be executed.
* @param {Object} compilation A dictionary with the assets information. Provided by webpack.
* @param {Function} callback A function the method needs to call so webpack can continue the
* process.
* @access protected
* @ignore
*/
_onAssetsEmitted(compilation, callback) {
// Check if the assets weren't already validated.
if (!this._setupReady) {
// Make sure this block won't run again.
this._setupReady = true;

// Get the assets dictionary emitted by webpack.
const { assets } = compilation;
// Transform the dictionary into an array and clear invalid entries.
const entries = Object.keys(assets).filter((asset) => (
// No need for HMR entries.
!asset.includes('hot-update') &&
// Remove it if doesn't provide a path for a file.
compilation.assets[asset].existsAt &&
// Remove it if the file path is not for JS file.
compilation.assets[asset].existsAt.match(/\.jsx?$/i)
));
// Get the _"specified"_ entry from the plugin options.
let { entry } = this._options;
if (entry && !entries.includes(entry)) {
// If an entry was specified but is not on the list, show an error.
this._logger.error(`The required entry (${entry}) doesn't exist`);
entry = null;
// And show the list of available entries.
this._logAvailableEntries(entries);
} else if (!entry && entries.length === 1) {
// If no entry was specified, but there's only one, use it.
[entry] = entries;
// Prevent the output from being added on the same line as webpack messages.
setTimeout(() => {
this._logger.success(`Using the only available entry: ${entry}`);
}, 1);
} else if (!entry && entries.length > 1) {
// If no entry was specified and there are more than one, use the first one.
[entry] = entries;
// Prevent the output from being added on the same line as webpack messages.
setTimeout(() => {
this._logger.warning(`Doing fallback to the first entry: ${entry}`);
// And show the list of available entries.
this._logAvailableEntries(entries);
}, 1);
} else {
/**
* If this was reached, it means that an entry was specified and it was on the entries
* list.
*/
setTimeout(() => {
this._logger.success(`Using the selected entry: ${entry}`);
}, 1);
}
// After validating the entry, update the reference on the plugin options.
this._options.entry = entry;
// If after the validation, there's still an entry to use...
if (entry) {
// ...resolve its file path and save it on a local property.
this._entryPath = path.resolve(compilation.assets[entry].existsAt);
// Prevent the output from being added on the same line as webpack messages.
setTimeout(() => {
this._logger.success(`Entry file: ${this._entryPath}`);
}, 1);
}
}
// Invoke the callback to continue the webpack process.
callback();
}
/**
* This is called by webpack when it starts compiling the bundle. If there's a child process
* running for the bundle, it will stop it and delete the reference.
* @access protected
* @ignore
*/
_onCompilationStarts() {
// Make sure the child process is running.
if (this._instance) {
// Prevent the output from being added on the same line as webpack messages.
setTimeout(() => {
this._logger.info('Stopping the bundle execution');
}, 1);
// Kill the child process.
this._instance.kill();
// Remove its reference from the plugin.
this._instance = null;
}
}
/**
* This is called by webpack when it finishes compiling the bundle. If an entry was selected
* on the assets hook, and there's no child process already running, fork a new one.
* @access protected
* @ignore
*/
_onCompilationEnds() {
// Make sure an entry was selected and there's no child process already running.
if (this._options.entry && !this._instance) {
// Fork a new instance of the bundle.
this._instance = fork(this._entryPath);
// Prevent the output from being added on the same line as webpack messages.
setTimeout(() => {
this._logger.success('Starting the bundle execution');
}, 1);
}
}
/**
* This is called during the assets hook event if no entry was specified and there's more than
* one, or if the specified entry wasn't found. The method logs the list of available entries
* that can be used with the plugin.
* @param {Array} entries The list of available entries.
* @access protected
* @ignore
*/
_logAvailableEntries(entries) {
const list = entries.join(', ');
this._logger.info(`These are the available entries: ${list}`);
}
}

module.exports = ProjextWebpackBundleRunner;
7 changes: 7 additions & 0 deletions src/plugins/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const ProjextWebpackBundleRunner = require('./bundleRunner');
const ProjextWebpackOpenDevServer = require('./openDevServer');

module.exports = {
ProjextWebpackBundleRunner,
ProjextWebpackOpenDevServer,
};
117 changes: 117 additions & 0 deletions src/plugins/openDevServer/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
const extend = require('extend');
const opener = require('opener');
const ProjextWebpackUtils = require('../utils');
/**
* This is a webpack plugin that works as a tiny helper for the dev server: It logs clear messages
* when the bundle isbeing created, when it's available, in which URL and it even opens the
* browser.
* The reason it was created was because the dev server log messages are hard to find and,
* depending on the settings, when the dev server opens the browser, you can end up on
* `/webpack-dev-server`.
*/
class ProjextWebpackOpenDevServer {
/**
* @param {string} url The dev server URL.
* @param {ProjextWebpackOpenDevServerOptions} [options={}] Settings to customize the plugin
* behaviour.
* @throws {Error} If `url` is not specified or if isn't a `string`.
*/
constructor(url, options = {}) {
/**
* The plugin options.
* @type {ProjextWebpackOpenDevServerOptions}
* @access protected
* @ignore
*/
this._options = extend(
true,
{
openBrowser: true,
name: 'projext-webpack-plugin-open-dev-server',
logger: null,
},
options
);
// Validate the recevied URL.
if (!url || typeof url !== 'string') {
throw new Error(`${this._options.name}: You need to specify a valid URL`);
}
/**
* The dev server URL.
* @type {string}
*/
this._url = url;
/**
* A logger to output the plugin's information messages.
* @type {Logger}
* @access protected
* @ignore
*/
this._logger = ProjextWebpackUtils.createLogger(this._options.name, this._options.logger);
/**
* This flag is used to check if the browser was already open once. The method that opens the
* browser is called every time the bundle is ready, which means every time it changes, but the
* plugin will only open the browser the first time.
* @type {boolean}
* @access protected
* @ignore
*/
this._browserAlreadyOpen = false;
}
/**
* Gets the plugin options.
* @return {ProjextWebpackOpenDevServerOptions}
*/
getOptions() {
return this._options;
}
/**
* Gets the dev server URL.
* @return {string}
*/
getURL() {
return this._url;
}
/**
* This is called by webpack when the plugin is being processed. The method takes care of adding
* the required listener to the webpack hooks in order to log the information messages and
* open the browser..
* @param {Object} compiler The compiler information provided by webpack.
*/
apply(compiler) {
const { name } = this._options;
compiler.hooks.compile.tap(name, this._onCompilationStarts.bind(this));
compiler.hooks.done.tap(name, this._onCompilationEnds.bind(this));
}
/**
* This is called by webpack when it starts compiling the bundle. This method just logs a
* message with the server URL and that the user should wait for webpack to finish.
* @access protected
* @ignore
*/
_onCompilationStarts() {
this._logger.warning(`Starting on ${this._url}`);
this._logger.warning('waiting for webpack...');
}
/**
* This is called by webpack when it finishes compiling the bundle. It opens the browser, if
* specified, and logs a message saying the bundle is ready on the server URL.
* @access protected
* @ignore
*/
_onCompilationEnds() {
// Make sure the browser should be opened and that it wasn't already.
if (this._options.openBrowser && !this._browserAlreadyOpen) {
// Mark the flag in order to prevent the browser from being open again.
this._browserAlreadyOpen = true;
// Open the browser.
opener(this._url);
}
// Prevent the output from being added on the same line as webpack messages.
setTimeout(() => {
this._logger.success(`Your app is running on ${this._url}`);
}, 1);
}
}

module.exports = ProjextWebpackOpenDevServer;
46 changes: 46 additions & 0 deletions src/plugins/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
const { Logger } = require('wootils/node/logger');
/**
* This is a set of utility methods the Projext webpack plugins use.
*/
class ProjextWebpackUtils {
/**
* Validate and create a {@link Logger} instance for a plugin.
* If the logger the plugin received on its options is an instance of {@link Logger} or has the
* same interface, it will _"accept it"_ and return it; If the plugin didn't receive a logger,
* it will create a new instance of {@link Logger} and return it, but if the received logger
* is an invalid object, it will throw an error.
* @param {string} plugin The plugin's instance name.
* @param {?Logger} logger The logger the plugin received on its options.
* @return {Logger}
* @throws {Error} If the logger the plugin received is not an instance of {@link Logger} and it
* doesn't have the same methods.
* @static
*/
static createLogger(plugin, logger) {
let result;
// If no logger was sent, create a new instance and set it as the return value.
if (!logger) {
result = new Logger();
} else if (logger instanceof Logger) {
// If the received logger is an instance of `Logger`, set it as the return value.
result = logger;
} else {
// Validate if there's a `Logger` method the received logger doesn't support.
const unsupportedMethod = ['success', 'info', 'warning', 'error']
.find((method) => typeof logger[method] !== 'function');
/**
* If there's a method that doesn't support, throw and error, otherwise, set it to be
* returned.
*/
if (unsupportedMethod) {
throw new Error(`${plugin}: The logger must be an instance of the wootils's Logger class`);
} else {
result = logger;
}
}
// Return the logger for the plugin.
return result;
}
}

module.exports = ProjextWebpackUtils;

0 comments on commit 746005e

Please sign in to comment.