Automatically loading externals with Webpack - javascript

I've done some searching but was wondering if there's an elegant solution here. When building a Webpack app, it's common to have dependencies that don't need to be compiled/bundled, like jQuery, React, ReactDOM, Angular, or Bootstrap, to name a few. You can list these in your Webpack config file in an externals object, but externals just assumes that these libraries will be available as namespaced globals at runtime.
This means that for each entry in your externals hash, you also need to toss in a script tag in your HTML. This makes sense if you're referencing an external CDN, but I'm thinking this could be automated if all you want to do is copy some dist file from a library in node_modules.
I've been looking for examples of how to do this but I haven't seen any yet. I messed with external-loader but I haven't had any luck integrating it (the documentation doesn't seem to provide a complete example).
Essentially, this would need to happen:
Libraries that shouldn't be bundled should be added to resolve.alias, e.g. {"react": "react/dist/react.js"}
A loader copies the dist files to the public directory (maybe this could just be done with file-loader?)
An HTML loader or maybe plugin inserts the script tags before the bundle.js script tag
If something like this doesn't exist, I might look into trying to make one; I'm just posting this here to see if anyone might know of a pre-baked solution, as it seems like it'd be a common problem for building web apps and I figured I'm probably missing something.

var path = require("path");
var webpack = require('webpack');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var helpers = require('./helpers');
var WebpackNotifierPlugin = require('webpack-notifier');
module.exports = {
entry: {
'index-ref': './app/index-ref.ts',
'vendor': './app/vendor.ts',
'app': './app/main.ts',
},
resolve: {
extensions: ['', '.ts', '.js']
},
module: {
loaders: [
{
test: /\.ts$/,
loaders: ['awesome-typescript-loader', 'angular2-template-loader']
},
{
test: /\.html$/,
loader: 'html'
},
{
test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/,
loader: 'file?name=assets/[name].[hash].[ext]'
},
{
test: /\.css$/,
exclude: helpers.root('app'),
loader: ExtractTextPlugin.extract('style', 'css?sourceMap')
},
{
test: /\.css$/,
include: helpers.root('app'),
loader: 'raw'
}
]
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ['app', 'vendor', 'index-ref']
}),
new HtmlWebpackPlugin({
filename: '../index.html',
template: 'template' + '/default.html',
lib: ['jQuery'],
chunks: ['entry-name']
}),
new HtmlWebpackExternalsPlugin([
// Using a CDN for a JS library
{
name: 'jquery',
var: 'jQuery',
url: 'https://cdnjs.cloudflare.com/ajax/libs/jquery/1.12.4/jquery.js'
}
],
{
basedir: 'node_modules',
dest: 'lib'
}),
new WebpackNotifierPlugin()
]
};
Am I missing anything here?

I didn't find a pre-existing solution, so I wrote a plugin to supplement the HtmlWebpackPlugin. It takes an array of externals and appends script/link tags to the HTML file, generates the externals hash, and can use CDNs or local files.
https://github.com/mmiller42/html-webpack-externals-plugin

If you don't want to add extra package bloat then HtmlWebpackPlugin has templating features, so you could do something like this:
//template.html
<html>
<head>
<%= htmlWebpackPlugin.options.externals %>
</head>
...
</html>
and then something like this in your webpack config:
//webpack.config.js
const EXTERNALS = [
{
name: 'react',
globalVarName: 'React',
src: 'https://cdn.example.com/react#18',
},
...
]
module.exports = {
...,
plugins: [
new HtmlWebpackPlugin({
filename: 'index.html',
template: 'template.html',
externals: EXTERNALS.reduce(
(scripts, external) => (
`${scripts}<script src="${external.src}"></script>`
), ''
)
})
],
externals: EXTERNALS.reduce(
(res, external) => ({
...res,
[external.name]: external.globalVarName,
}), {}
),
}
(You can obviously add any environment customisations/finer details/etc. you want in the webpack config file.)
EDIT:
For some extra panache you could also get the current package versions from your node_modules rather than hard-coding them into the webpack file:
const fs = require('fs')
function getPackageVersion(packageName) {
const pkgPath = `node_modules/${packageName}`
const pkg = JSON.parse(fs.readFileSync(`${pkgPath}/package.json`, 'utf8'))
return pkg['version']
}

Related

How to import jquery in webpack

I have a problem with jquery when i using it on webpack.
My code:
const path = require('path');
const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const CompressionPlugin = require("compression-webpack-plugin");
module.exports = {
entry: {
vendor: [
'./src/main/webapp/js/vendor/jquery-3.3.1.min.js',
// './src/main/webapp/js/vendor/fs.js',
'./src/main/webapp/js/vendor/google-adsense.js',
'./src/main/webapp/js/vendor/jquery.menu-aim.min.js',
'./src/main/webapp/js/vendor/jquery.touchSwipe.min.js',
],
app: './src/main/assets/js/desktop/app.js',
mobile: './src/main/assets/js/mobile/app.js',
touch: './src/main/assets/js/touch/app.js',
},
module: {
rules: [{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['#babel/preset-env']
}
}
},
{
test: require.resolve('jquery'),
loader: 'expose-loader?jQuery!expose-loader?$'
}
],
},
plugins: [
// new CleanWebpackPlugin(['src/main/webapp/assets']),
new webpack.optimize.CommonsChunkPlugin({
name: 'common' // Specify the common bundle's name.
}),
new UglifyJsPlugin({
test: /\.js$/,
sourceMap: process.env.NODE_ENV === "development"
}),
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery"
})
],
output: {
filename: '[name].js',
path: path.resolve(__dirname, './src/main/webapp/js')
}
};
When above code compiles , console throws this error
vendor.js:1 Uncaught ReferenceError: webpackJsonp is not defined
at vendor.js:1
And when i try to use this
externals: {
jquery: 'jQuery'
}
It throws
vendor.js:1 Uncaught ReferenceError: jQuery is not defined
at Object.231 (vendor.js:1)
at o (common.js:1)
at Object.228 (vendor.js:1)
at o (common.js:1)
at window.webpackJsonp (common.js:1)
at vendor.js:1
And i using jquery in my core js file import $ from 'jquery'.
What did i do wrong ? any help ? Thank you.
So there are few themes in your webpack.config.js, some of which conflict. Just going to list them so I can better understand what I think you are trying to achieve.
Theme 1
You have an entry called vendor that is clearly referencing a minified jQuery library you have presumably downloaded and placed in the directory specified.
Theme 2
You also have an expose-loader that is essential exposing the jquery library from its node_modules probably listed in the dependencies of your package.json.
This makes the jquery in the node_modules available as $ and jQuery in the global scope of the page where your bundle is included.
Theme 3
You also have included the ProvidePlugin with configuration for jQuery.
The ProvidePlugin is supposed to inject dependencies into the scope of your module code, meaning you do not need to have import $ from 'jquery' instead $ and jQuery will already be available in all of your modules.
Conclusion
From what I have gathered I think you're trying to bundle jQuery from the static file at ./src/main/webapp/js/vendor/jquery-3.3.1.min.js in a vendor bundle.
You are then trying to expose the libraries in the vendor bundle to the global scope (jQuery).
Then also have your application code able to import jQuery from what is made available by the vendor bundle in the global scope.
Answer
So if that is what you are doing you need to do the following things.
Firstly, check in your package.json files dependencies for jquery. If its there you want to remove it, there's no need for it if you're going to use your jquery-3.3.1.min.js file instead to provide jQuery to your application.
Secondly, change your test of the expose-loader to trigger when it sees your jquery-3.3.1.min.js file in your entries, not resolve it from the jquery dependency from your node_modules.
This regex pattern should do the trick.
{
test: /jquery.+\.js$/,
use: [{
loader: 'expose-loader',
options: 'jQuery'
},{
loader: 'expose-loader',
options: '$'
}]
}
Thirdly, remove the ProvidePlugin if you're going to import the library explicitly with import $ from 'jquery' you do not need it.
Lastly, you need to tell webpack when it sees an import for jquery it can resolve this from window.jQuery in the global scope. You can do this with the externals configuration you already referenced.
externals: {
jquery: 'jQuery'
}
With all these changes you should end up with a webpack.config.js file that looks like this.
const path = require('path');
const webpack = require('webpack');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const ExtractTextPlugin = require("extract-text-webpack-plugin");
const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
const CompressionPlugin = require("compression-webpack-plugin");
module.exports = {
entry: {
vendor: [
'./src/main/webapp/js/vendor/jquery-3.3.1.min.js',
// './src/main/webapp/js/vendor/fs.js',
'./src/main/webapp/js/vendor/google-adsense.js',
'./src/main/webapp/js/vendor/jquery.menu-aim.min.js',
'./src/main/webapp/js/vendor/jquery.touchSwipe.min.js',
],
app: './src/main/assets/js/desktop/app.js',
mobile: './src/main/assets/js/mobile/app.js',
touch: './src/main/assets/js/touch/app.js',
},
module: {
rules: [{
test: /\.js$/,
exclude: /(node_modules|bower_components)/,
use: {
loader: 'babel-loader',
options: {
presets: ['#babel/preset-env']
}
}
},
{
test: /jquery.+\.js$/,
use: [{
loader: 'expose-loader',
options: 'jQuery'
},{
loader: 'expose-loader',
options: '$'
}]
}
],
},
plugins: [
// new CleanWebpackPlugin(['src/main/webapp/assets']),
new webpack.optimize.CommonsChunkPlugin({
name: 'common' // Specify the common bundle's name.
}),
new UglifyJsPlugin({
test: /\.js$/,
sourceMap: process.env.NODE_ENV === "development"
})
],
output: {
filename: '[name].js',
path: path.resolve(__dirname, './src/main/webapp/js')
},
externals: {
jquery: 'jQuery'
}
};
I hope that does not just give you the answer but enough explanation as to where you were going wrong.

Webpack with requirejs/AMD

I'm working on a new module for an existing project that still uses requireJS for module loading. I'm trying to use new technologies for my new module like webpack (which allows me to use es6 loaders using es6 imports). It seems like webpack can't reconcile with requireJS syntax. It will say things like: "Module not found: Error: Can't resolve in ".
Problem: Webpack won't bundle files with requireJS/AMD syntax in them.
Question: Is there any way to make webpack play nice with requireJS?
My final output must be in AMD format in order for the project to properly load it. Thanks.
I had the same question and I managed to achieve it. Below is the same webpack.config.js file.
const fs = require('fs');
const path = require('path');
const webpack = require('webpack');
let basePath = path.join(__dirname, '/');
let config = {
// Entry, file to be bundled
entry: {
'main': basePath + '/src/main.js',
},
devtool: 'source-map',
output: {
// Output directory
path: basePath + '/dist/',
library: '[name]',
// [hash:6] with add a SHA based on file changes if the env is build
filename: env === EnvEnum.BUILD ? '[name]-[hash:6].min.js' : '[name].min.js',
libraryTarget: 'amd',
umdNamedDefine: true
},
module: {
rules: [{
test: /(\.js)$/,
exclude: /(node_modules|bower_components)/,
use: {
// babel-loader to convert ES6 code to ES5 + amdCleaning requirejs code into simple JS code, taking care of modules to load as desired
loader: 'babel-loader',
options: {
presets: ['es2015'],
plugins: []
}
}
}, { test: /jQuery/, loader: 'expose-loader?$' },
{ test: /application/, loader: 'expose-loader?application' },
{ test: /base64/, loader: 'exports-loader?Base64' }
]
},
resolve: {
alias: {
'jQuery': 'bower_components/jquery/dist/jquery.min',
'application': 'main',
'base64': 'vendor/base64'
},
modules: [
// Files path which will be referenced while bundling
'src/**/*.js',
'src/bower_components',
path.resolve('./src')
],
extensions: ['.js'] // File types
},
plugins: [
]
};
module.exports = config;

Optimal webpack.config with webpack 2 for AngularJS 1.x app?

I want to set up an Angular 1.x app from scratch using webpack 2.
I am having trouble finding the best configuration for webpack.config, with optimal entry and output for production (meaning, all code, style and templating minified and gziped with no code repetition).
My main problem is how to set up webpack.config so that it recognizes all partials within the folder structure of my project, like these:
My current config file, for reference (which can't see subfolders):
var HtmlWebpackPlugin = require( 'html-webpack-plugin' );
var ExtractTextPlugin = require( 'extract-text-webpack-plugin' );
var path = require( 'path' );
module.exports = {
devServer: {
compress: true,
contentBase: path.join( __dirname, '/dist' ),
open: true,
port: 9000,
stats: 'errors-only'
},
entry: './src/app.js',
output: {
path: path.join( __dirname, '/dist' ),
filename: 'app.bundle.js'
},
module: {
rules: [ {
test: /\.scss$/,
use: ExtractTextPlugin.extract( {
fallback: 'style-loader',
use: [
'css-loader',
'sass-loader'
],
publicPath: '/dist'
} )
} ]
},
plugins: [
new HtmlWebpackPlugin( {
hash: true,
minify: { collapseWhitespace: true },
template: './src/index.html',
title: 'Prov'
} ),
new ExtractTextPlugin( {
filename: 'main.css',
allChunks: true
} )
]
};
Note that this isn't an exhaustive solution, as there are many optimizations one can make in the frontend, and I've kept the code snippets fairly short.
With webpack, there are a few routes that you can take to include partials into your app.js.
Solution 1
You can import/require your partials within app.js as such:
app.js
var angular = require('angular');
var proverbList = require('./proverb/list/proverb.list');
// require other components
// set up your app as normal
This allows the app.bundle.js to include your component js files in the main bundle. You can also use html-loader to include templates in the final bundle.
This isn't ideal, as all it does is create a large bundle.js (which doesn't leverage multiple downloads with http2 nor does it allow loading of components/files when the user explicitly requires it).
Solution 2
Importing partials as separate entry files into your webpack bundle:
webpack.config.js
const globby = require('globby');
const sourceDir = 'src';
var webpackentry = {
app: `${__dirname}/src/app.js`
};
const glob = globby.sync(`${__dirname}/${sourceDir}/**/*.js`)
.map((file)=>{
let name = file.split('/').pop().replace('.js', '');
webpackentry[name] = file;
});
const config = {
entry: webpackentry,
...
}
The second solution is unorthodox but it can be useful if you wanted to split all your partials as <script> tags in your html (for example if your company/team uses that as a means to include your directive/components/controllers), or if you have an app-2.bundle.js.
Solution 3
Use CommonsChunkPlugin:
webpack.config.js
let webpackentry = {
vendor: [
'module1',
'module2',
'module3',
]
}
...
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ['vendor'] //... add other modules
})
]
CommonsChunkPlugin allows webpack to scrawl through your entry files and discern common modules that are shared among them. This means that even if you are importing module1 in different files, they will be compiled only once in your final bundle.

How to correctly use Webpack's CommonsChunkPlugin with multiple entry points?

I'm having a problem with maintaining stable module IDs for my explicit vendor chunk. Here's my setup (modeled after SurviveJS):
const gitrev = require('git-rev-sync');
const path = require('path');
const webpack = require('webpack');
const ChunkManifestPlugin = require('chunk-manifest-webpack-plugin');
const packageJson = require('./package.json');
const gulpPaths = require('./gulp/lib/gulp_paths');
module.exports = {
context: __dirname,
entry: {
vendor: Object.keys(packageJson.dependencies),
web: path.join(
__dirname,
gulpPaths.SCRIPT_SOURCE_DIR,
gulpPaths.SCRIPT_BUNDLE_SOURCE_FILE
),
// server: path.join(
// __dirname,
// gulpPaths.SCRIPT_SOURCE_DIR,
// gulpPaths.SCRIPT_SERVER_SIDE_SOURCE_FILE
// ),
},
output: {
path: path.join(__dirname, gulpPaths.SCRIPT_OUTPUT_DIR),
filename: '[name]_bundle.js',
},
plugins: [
new webpack.DefinePlugin({
__DEV__: false,
__TEST__: false,
__COMMIT__: JSON.stringify(gitrev.long()),
'process.env': {
NODE_ENV: JSON.stringify('production'),
},
}),
new webpack.optimize.CommonsChunkPlugin({
names: ['vendor', 'manifest'],
minChunks: Infinity,
}),
new webpack.optimize.UglifyJsPlugin(),
],
resolve: {
extensions: ['', '.js', '.jsx'],
},
module: {
loaders: [
{
test: /\.jsx?$/,
loaders: ['babel'],
exclude: /node_modules/,
},
{ // Turn off AMD module loading on eventemitter2
test: /eventemitter2/,
loader: 'imports?define=>false',
}
]
},
};
It works pretty well; I can ride back and forth through my repository history, and the vendor chunk changes only if the vendor libraries actually change in node_modules. However, once I uncomment the additional entry point, everything changes: the vendor chunk changes without any changes to the vendor libraries. If I manually whitelist only a couple of libraries, it helps, but doesn't remove the problem entirely. It looks like it also conflicts the same way with DedupePlugin and OccurenceOrderPlugin.
I also tried to use the approaches outlined in the Webpack documentation (recordsPath and ChunkManifestPlugin, without luck).
I wouldn't split hair over this, since the server entry point is anyway a failed experiment in server-side rendering and should probably be removed; however, soon I'm going to have multiple entry points anyway, for more fine-tuning of the page loading time, and it's probably going to get funny.
As advised by Juho Vepsäläinen, the best option for me was using the NamedModulesPlugin. For the record, the HashModuleIds plugin is also worth looking at, but it's only available in Webpack 2.

webpack build less files output one css minify file

Is webpack the tool that I need to pass several less files into one minified CSS file?
if so, I'm not sure what I'm doing wrong in the code below?
Is there a way of outputting to different file paths, right now my js file outputs to './assets/javascripts/bundle/', I would like my css file to output to './assets/stylesheets/bundle/' , how would I do this?
Update
I did a test and I can build my less files to one css file but still can't find out how to set multiple paths for the output folder, now I have to comment out the js entry part and change output path...
webpack config
var path = require('path');
var webpack = require("webpack");
var ExtractTextPlugin = require("extract-text-webpack-plugin");
module.exports = {
entry: {
'MediaIndex':['./assets/javascripts/Media/Index/Base.js'],
// stylesheets
'MediaIndexStyleSheet':['./assets/stylesheets/Media/Index/Base.js']
},
output: {
path: './assets/javascripts/bundle/',
filename: '[name].js'
},
resolve: {
alias: {
jquery: path.join(__dirname, "assets/javascripts/lib/jquery-1.11.1.min.js"),
"jquery-ui": path.join(__dirname, "assets/javascripts/lib/jquery-ui.min.js"),
}
},
plugins: [
new webpack.ProvidePlugin({
jQuery: "jquery",
$: "jquery"
}),
new ExtractTextPlugin("[name].css")
],
module: {
loaders: [
{
test: /\.css$/,
loader: ExtractTextPlugin.extract("style-loader", "css-loader")
},
{
test: /\.less$/,
loader: ExtractTextPlugin.extract("style-loader", "css-loader!less-loader")
}
]
},
};
assets/stylesheets/Media/Index/Base.js
require('../../../Global/Config.less');
require('../../../Global/Fp.less');
require('../../../Global/Urb.less');
require('../../../Global/Ad1.less');
require('./Nav0.less');
require('./Index.less');
require('./Content.less');
Config.less
#color: 'red';
Index.less
body {
background-color: #{color};
}
You're almost there. Like you already did, you can get all your styling into one css file using ExtractTextPlugin. To put your js and css into a different directories, you can just define a path in ExtractTextPlugin or output.filename that is relative to your output.path. For example:
output: {
path: './assets',
filename: './javascripts/bundle/[name].js'
},
plugins: [
new ExtractTextPlugin("./stylesheets/bundle/[name].css")
]

Categories

Resources