Skip to content

Commit

Permalink
Merge 20349a6 into 7c02893
Browse files Browse the repository at this point in the history
  • Loading branch information
homer0 committed Jul 31, 2019
2 parents 7c02893 + 20349a6 commit 535ce2a
Show file tree
Hide file tree
Showing 11 changed files with 525 additions and 131 deletions.
2 changes: 2 additions & 0 deletions src/plugins/index.js
@@ -1,7 +1,9 @@
const ProjextWebpackBundleRunner = require('./bundleRunner');
const ProjextWebpackOpenDevServer = require('./openDevServer');
const ProjextWebpackRuntimeDefinitions = require('./runtimeDefinitions');

module.exports = {
ProjextWebpackBundleRunner,
ProjextWebpackOpenDevServer,
ProjextWebpackRuntimeDefinitions,
};
137 changes: 137 additions & 0 deletions src/plugins/runtimeDefinitions/index.js
@@ -0,0 +1,137 @@
const ObjectUtils = require('wootils/shared/objectUtils');
const { DefinePlugin } = require('webpack');
/**
* This is a webpack plugin that works with `webpack.DefinePlugin` in order to reload all
* definitions when the bundle changes.
*/
class ProjextWebpackRuntimeDefinitions {
/**
* @param {Array} files The list of files that need to
* change in order to reload
* the definitions.
* @param {Function():Object} definitionsFn When this function is called,
* it should return the object
* with the definitions.
* @param {ProjextWebpackRuntimeDefinitionsOptions} [options={}] The options to customize the
* plugin instance.
* @throws {Error} If `files` is not an Array.
* @throws {Error} If `files` is empty.
* @throws {Error} If `definitionsFn` is not a function.
*/
constructor(files, definitionsFn, options = {}) {
if (!Array.isArray(files) || !files.length) {
throw new Error('You need to provide a valid files list');
}

if (typeof definitionsFn !== 'function') {
throw new Error('You need to provide a valid definitions function');
}

/**
* The list of files that need to change in order to reload the definitions.
* @type {Array}
* @access protected
* @ignore
*/
this._files = files;
/**
* The function that will generate the definitions.
* @type {Function():Object}
* @access protected
* @ignore
*/
this._definitionsFn = definitionsFn;
/**
* The options to customize the plugin instance.
* @type {ProjextWebpackRuntimeDefinitionsOptions}
* @access protected
* @ignore
*/
this._options = ObjectUtils.merge(
{
name: 'projext-webpack-plugin-runtime-definitions',
},
options
);
/**
* This is where the plugin will "refresh" the definitions when the bundle changes.
* @type {Object}
* @access protected
* @ignore
*/
this._values = {};
}
/**
* Get the options that customize the plugin instance.
* @return {ProjextWebpackRuntimeDefinitionsOptions}
*/
getOptions() {
return this._options;
}
/**
* This is called by webpack when the plugin is being processed. The method will create a new
* instance of `webpack.DefinePlugin` and set all the values as "runtime values", so they'll
* get evaluated every time the bundle is generated.
* The method will also tap into the `compile` hook, so this plugin can refresh the values (by
* calling the `definitionFn`) when the bundle is about to be generated.
* @param {Object} compiler The compiler information provided by webpack.
*/
apply(compiler) {
const plugin = new DefinePlugin(this._getDefinePluginSettings());
plugin.apply(compiler);
compiler.hooks.compile.tap(
this._options.name,
this._onCompilationStarts.bind(this)
);
}
/**
* Reloads the saved values on the instance by calling the `definitionsFn`.
* @access protected
* @ignore
*/
_reloadValues() {
this._values = this._definitionsFn();
return this._values;
}
/**
* Generates the settings for `webpack.DefinePlugin`. It basically creates a "runtime value" for
* each of the definitions keys and sets a function that will call this instance.
* @return {Object}
* @access protected
* @ignore
*/
_getDefinePluginSettings() {
const keys = Object.keys(this._reloadValues());
return keys.reduce(
(settings, key) => Object.assign({}, settings, {
[key]: DefinePlugin.runtimeValue(
() => this._getValue(key),
this._files
),
}),
{}
);
}
/**
* Get a single value from a definition already loaded.
* @param {string} key The definition key.
* @return {string}
* @access protected
* @ignore
*/
_getValue(key) {
return this._values[key];
}
/**
* This is called by webpack when the bundle compilation starts. The method just reloads the
* definition values on the instance so they'll be available when `webpack.DefinePlugin` tries
* to access them.
* @access protected
* @ignore
*/
_onCompilationStarts() {
this._reloadValues();
}
}

module.exports = ProjextWebpackRuntimeDefinitions;
74 changes: 59 additions & 15 deletions src/services/building/configuration.js
Expand Up @@ -96,13 +96,8 @@ class WebpackConfiguration {
output.jsChunks = this._generateChunkName(output.js);
}

const definitions = this._getDefinitions(target, buildType);
const additionalWatch = [];
if (target.is.browser && target.configuration && target.configuration.enabled) {
const browserConfig = this.targets.getBrowserTargetConfiguration(target);
definitions[target.configuration.defineOn] = JSON.stringify(browserConfig.configuration);
additionalWatch.push(...browserConfig.files);
}
const definitions = this._getDefinitionsGenerator(target, buildType);
const additionalWatch = this._getBrowserTargetConfigurationDefinitions(target).files;

const params = {
target,
Expand Down Expand Up @@ -134,29 +129,78 @@ class WebpackConfiguration {
return config;
}
/**
* Get a dictionary of definitions that will be replaced on the generated bundle. This is done
* using the `webpack.DefinePlugin` plugin.
* @param {Target} target The target information.
* @param {string} env The `NODE_ENV` to define.
* Generates a function that when called will return a dictionary with definitions that will be
* replaced on the bundle.
* @param {Target} target The target information.
* @param {string} buildType The intended build type: `production` or `development`.
* @return {Function():Object}
* @access protected
* @ignore
*/
_getDefinitionsGenerator(target, buildType) {
return () => this._getTargetDefinitions(target, buildType);
}
/**
* Generates a dictionary with definitions that will be replaced on the bundle. These
* definitions are things like `process.env.NODE_ENV`, the bundle version, a browser target
* configuration, etc.
* @param {Target} target The target information.
* @param {string} buildType The intended build type: `production` or `development`.
* @return {Object}
* @access protected
* @ignore
*/
_getDefinitions(target, env) {
const targetVariables = this.targets.loadTargetDotEnvFile(target, env);
_getTargetDefinitions(target, buildType) {
const targetVariables = this.targets.loadTargetDotEnvFile(target, buildType);
const definitions = Object.keys(targetVariables).reduce(
(current, variableName) => Object.assign({}, current, {
[`process.env.${variableName}`]: JSON.stringify(targetVariables[variableName]),
}),
{}
);

definitions['process.env.NODE_ENV'] = `'${env}'`;
definitions['process.env.NODE_ENV'] = `'${buildType}'`;
definitions[this.buildVersion.getDefinitionVariable()] = JSON.stringify(
this.buildVersion.getVersion()
);

return definitions;
return Object.assign(
{},
definitions,
this._getBrowserTargetConfigurationDefinitions(target).definitions
);
}
/**
* This is a wrapper on top of {@link Targets#getBrowserTargetConfiguration} so no matter the
* type of target it recevies, or if the feature is disabled, it will always return the same
* signature.
* It also takes care of formatting the configuration on a "definitions object" so it can be
* added to the rest of the targets definitions.
* @param {Target} target The target information.
* @return {Object}
* @property {Object} definitions A dictionary with
* @property {Array} files The list of files involved on the configuration creation.
* @access protected
* @ignore
*/
_getBrowserTargetConfigurationDefinitions(target) {
let result;
if (target.is.browser && target.configuration && target.configuration.enabled) {
const parsed = this.targets.getBrowserTargetConfiguration(target);
result = {
definitions: {
[target.configuration.defineOn]: JSON.stringify(parsed.configuration),
},
files: parsed.files,
};
} else {
result = {
definitions: {},
files: [],
};
}

return result;
}
/**
* In case the target is a library, this method will be called in order to get the extra output
Expand Down
21 changes: 15 additions & 6 deletions src/services/configurations/browserDevelopmentConfiguration.js
@@ -1,4 +1,5 @@
const extend = require('extend');
const path = require('path');
const ObjectUtils = require('wootils/shared/objectUtils');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
Expand All @@ -7,13 +8,15 @@ const CopyWebpackPlugin = require('copy-webpack-plugin');
const ExtraWatchWebpackPlugin = require('extra-watch-webpack-plugin');
const {
NoEmitOnErrorsPlugin,
DefinePlugin,
HotModuleReplacementPlugin,
NamedModulesPlugin,
} = require('webpack');
const { provider } = require('jimple');
const ConfigurationFile = require('../../abstracts/configurationFile');
const { ProjextWebpackOpenDevServer } = require('../../plugins');
const {
ProjextWebpackOpenDevServer,
ProjextWebpackRuntimeDefinitions,
} = require('../../plugins');
/**
* Creates the specifics of a Webpack configuration for a browser target development build.
* @extends {ConfigurationFile}
Expand Down Expand Up @@ -99,7 +102,7 @@ class WebpackBrowserDevelopmentConfiguration extends ConfigurationFile {
} = params;
// Define the basic stuff: entry, output and mode.
const config = {
entry: extend(true, {}, entry),
entry: ObjectUtils.copy(entry),
output: {
path: `./${target.folders.build}`,
filename: output.js,
Expand Down Expand Up @@ -129,7 +132,13 @@ class WebpackBrowserDevelopmentConfiguration extends ConfigurationFile {
// To avoid pushing assets with errors.
new NoEmitOnErrorsPlugin(),
// To add the _'browser env variables'_.
new DefinePlugin(definitions),
new ProjextWebpackRuntimeDefinitions(
Object.keys(entry).reduce(
(current, key) => [...current, ...entry[key].filter((file) => path.isAbsolute(file))],
[]
),
definitions
),
// To optimize the SCSS and remove repeated declarations.
new OptimizeCssAssetsPlugin(),
// Copy the files the target specified on its settings.
Expand Down Expand Up @@ -255,7 +264,7 @@ class WebpackBrowserDevelopmentConfiguration extends ConfigurationFile {
*/
_normalizeTargetDevServerSettings(target) {
// Get a new copy of the config to work with.
const config = extend(true, {}, target.devServer);
const config = ObjectUtils.copy(target.devServer);
/**
* Set a flag to know if at least one SSL file was sent.
* This flag is also used when reading the `proxied` settings to determine the default
Expand Down
11 changes: 9 additions & 2 deletions src/services/configurations/browserProductionConfiguration.js
@@ -1,3 +1,4 @@
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ScriptExtHtmlWebpackPlugin = require('script-ext-html-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
Expand All @@ -6,9 +7,9 @@ const TerserPlugin = require('terser-webpack-plugin');
const OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const ExtraWatchWebpackPlugin = require('extra-watch-webpack-plugin');
const { DefinePlugin } = require('webpack');
const { provider } = require('jimple');
const ConfigurationFile = require('../../abstracts/configurationFile');
const { ProjextWebpackRuntimeDefinitions } = require('../../plugins');
/**
* Creates the specifics of a Webpack configuration for a browser target production build.
* @extends {ConfigurationFile}
Expand Down Expand Up @@ -122,7 +123,13 @@ class WebpackBrowserProductionConfiguration extends ConfigurationFile {
]
),
// To add the _'browser env variables'_.
new DefinePlugin(definitions),
new ProjextWebpackRuntimeDefinitions(
Object.keys(entry).reduce(
(current, key) => [...current, ...entry[key].filter((file) => path.isAbsolute(file))],
[]
),
definitions
),
// To optimize the SCSS and remove repeated declarations.
new OptimizeCssAssetsPlugin(),
// To compress the emitted assets using gzip, if the target is not a library.
Expand Down
11 changes: 9 additions & 2 deletions src/typedef.js
Expand Up @@ -195,8 +195,8 @@
* @property {WebpackConfigurationTargetOutput} output
* A dictionary with the filenames formats and paths of the different files the bundle can
* generate.
* @property {Object} definitions
* A dictionary of defined variables that will be replaced on the bundled code.
* @property {Function():Object} definitions
* A function that generates a dictionary of variables that will be replaced on the bundled code.
* @property {string} buildType
* The intended built type: `development` or `production`.
* @property {Array} copy
Expand Down Expand Up @@ -279,3 +279,10 @@
* Its default value is `projext-webpack-plugin-open-dev-server`.
* @property {?Logger} logger A custom logger to output the plugin's information messages.
*/

/**
* @typedef {Object} ProjextWebpackRuntimeDefinitionsOptions
* @property {?string} name The _"instance name"_, used to register the listeners on the
* webpack event hooks. Its default value is
* `projext-webpack-plugin-runtime-definitions`.
*/
13 changes: 13 additions & 0 deletions tests/mocks/webpack.mock.js
@@ -1,6 +1,8 @@
const mocks = {
noEmitOnErrorsPlugin: jest.fn(),
definePlugin: jest.fn(),
definePluginApply: jest.fn(),
definePluginRuntimeValue: jest.fn(),
hotModuleReplacementPlugin: jest.fn(),
namedModulesPlugin: jest.fn(),
};
Expand All @@ -13,10 +15,19 @@ class NoEmitOnErrorsPluginMock {
}

class DefinePluginMock {
static runtimeValue(...args) {
return mocks.definePluginRuntimeValue(...args);
}

constructor(...args) {
this.constructorMock = mocks.definePlugin;
this.applyMock = mocks.definePluginApply;
this.constructorMock(...args);
}

apply(...args) {
this.applyMock(...args);
}
}

class HotModuleReplacementPluginMock {
Expand Down Expand Up @@ -50,6 +61,8 @@ module.exports.NoEmitOnErrorsPlugin = NoEmitOnErrorsPluginMock;
module.exports.NoEmitOnErrorsPluginMock = mocks.noEmitOnErrorsPlugin;
module.exports.DefinePlugin = DefinePluginMock;
module.exports.DefinePluginMock = mocks.definePlugin;
module.exports.DefinePluginApplyMock = mocks.definePluginApply;
module.exports.DefinePluginRuntimeValueMock = mocks.definePluginRuntimeValue;
module.exports.HotModuleReplacementPlugin = HotModuleReplacementPluginMock;
module.exports.HotModuleReplacementPluginMock = mocks.hotModuleReplacementPlugin;
module.exports.NamedModulesPlugin = NamedModulesPluginMock;
Expand Down

0 comments on commit 535ce2a

Please sign in to comment.