-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #35 from homer0/homer0_runner
Refactor the dev server custom plugin and goodbye webpack-node-utils!
- Loading branch information
Showing
15 changed files
with
1,329 additions
and
410 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
Oops, something went wrong.