Webpack 2: shimming like RequireJS for jQWidgets? - javascript

I’m migrating from a RequireJS project to Webpack.
The latter is new to me, I’m using this as a learning exercise.
In RequireJS I could register stuff like this:
shim: {
'jqxcore': {
exports: "$",
deps: ["jquery"]
},
'jqxtree': {
exports: "$",
deps: ["jquery", "jqxcore"]
},
'jqxbutton': {
exports: "$",
deps: ["jquery", "jqxcore"]
},
'jqxsplitter': {
exports: "$",
deps: ["jquery", "jqxcore"]
},
'jqxmenu': {
exports: "$",
deps: ["jquery", "jqxcore"]
}
}
and then just require “jqxsplitter” for example like so:
import "jqxsplitter"
and stuff would be correctly registered and loaded.
Now I was looking at a couple of guides/tutorials/takes I found on migrating from RequireJS to Webpack, such as this one and this one.
So following those insights I’m trying something like this in my webpack.config.js:
"use strict";
// Required to form a complete output path
const path = require("path");
// Plagin for cleaning up the output folder (bundle) before creating a new one
const CleanWebpackPlugin = require("clean-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const webpack = require("webpack");
// Path to the output folder
const bundleFolder = "./wwwroot/";
// Path to the app source code
const appFolder = "./app/";
module.exports = {
// Application entry point
entry: {
main: appFolder + "index.ts",
vendor: [
"knockout",
"jquery",
"jqxcore"
],
jqxsplitter: "jqxsplitter"
},
// Output file
output: {
filename: "[name].js",
chunkFilename: "[name].js",
path: path.resolve(bundleFolder)
},
module: {
rules: [{
test: /\.tsx?$/,
loader: "ts-loader",
exclude: /node_modules/
}, {
test: /\.html?$/,
loader: "html-loader" //TODO: file-loader?
}],
loaders: [{
test: /jqxcore/,
loader: "imports?jquery!exports?$"
}, {
test: /jqxsplitter/,
loader: "imports?jquery,jqxcore!exports?$"
}]
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
alias: {
"jqxcore": "jqwidgets-framework/jqwidgets/jqxcore",
"jqxsplitter": "jqwidgets-framework/jqwidgets/jqxsplitter"
}
},
plugins: [
new CleanWebpackPlugin([bundleFolder]),
new HtmlWebpackPlugin({
filename: "index.html",
template: appFolder + "index.html",
chunks: ["main", "vendor"]
}),
new webpack.optimize.CommonsChunkPlugin({
name: "vendor",
filename: "vendors.js",
minChunks: Infinity
})
],
devtool: "source-map"
};
the relevant part (I assume) being
module: {
loaders: [{
test: /jqxcore/,
loader: "imports?jquery!exports?$"
}, {
test: /jqxsplitter/,
loader: "imports?jquery,jqxcore!exports?$"
}]
},
It’s pretty clear how the syntax of “imports/exports” is supposed to be the equivalent of RequireJS’ “deps” and “exports”.
However when I do this in my index.ts file (app root):
import "jqwidgets-framework/jqwidgets/jqxsplitter";
I get the “jqxBaseFramework is undefined” error when running my app.
I’ve found references to this error on the forums of jQWidgets, but none of the answers seem to REALLY tackle the issue or include things like the AOT compilation, which doesn’t apply to my situation because I’m not using Angular.
I've posted this same question on the jQWidges forums, but so far no actual answer (going on two weeks now), only a single generic answer saying I should load jqxcore.js before jqxwhateverplugin.js.
Well yes, obviously, that's what I'm trying to accomplish using the shimming after all.
Any ideas?

Well I ended up deep diving and figuring it out for myself.
Here's the solution should anyone find themselves in the same or a similar boat.
If you beautify the jQWidgets script files jqxcore.js, you'll see it creates a what would normally be a global variable called "jqxBaseFramework", which will of course never be exposed globally, only within its own module. And there lies the problem.
The solution is to use this configuration:
module: {
rules: [{
test: /jqxcore/,
use: "exports-loader?jqxBaseFramework"
}, {
test: /jqxknockout/,
use: ["imports-loader?jqxBaseFramework=jqxcore,ko=knockout", "exports-loader?jqxBaseFramework"]
}, {
test: /jqxsplitter/,
use: "imports-loader?jqxBaseFramework=jqxknockout"
}]
},
resolve: {
...
alias: {
"knockout": "knockout/build/output/knockout-latest",
"jqxcore": "jqwidgets-framework/jqwidgets/jqxcore",
"jqxknockout": "jqwidgets-framework/jqwidgets/jqxknockout",
"jqxsplitter": "jqwidgets-framework/jqwidgets/jqxsplitter"
}
},
I guess once it clicks, this all makes sense.
The jqxcore module will now export its jqxBaseFramework variable with the same name.
I added in knockout support while at it.
jqxknockout expects two global variables to work normally: ko (knockout) and jqxBaseFramework.
So now we tell webpack that whenever jqxknockout is loaded, it should load the jqxcore module and assign its export to a module-local variable called "jqxBaseFramework" and load the knockout module and assign its export to a module-local variable called "ko".
This effectively equates to prepending the following code to the jqxknockout.js script:
var jqxBaseFramework = require("jqxcore");
var ko = require("knockout");
The script can now execute again because those two variables are found.
I added the export loader to export the same, but now processed/augmented jqxBaseFramework variable from jqxknockout.
jqxSplitter normally only needs jqxCore to work, but I want to use it with knockout, always. So instead of importing jqxBaseFramework from jqxCore for jqxSplitter, I'm getting it from jqxKnockout, so all the pieces are in place.
So now when I add this code to whatever file I'm in:
import "jqwidgets-framework/jqwidgets/jqxsplitter";
Webpack will require jqxknockout and its export for it, being jqxBaseFramework, which in turn will require jqxcore and knockout et voilà, the whole thing is wired up beautifully.
Hope this helps someone!

Related

Webpack configuration to allow import

I am creating a basic structure where the controller.js would need some Views to be imported from view.js and hence this is what I am using in the controller.js
import View from './View'
However there was an issue with the bundler with webpack and I ended up visiting this page
https://webpack.js.org/loaders/imports-loader/
and below is webpack.config.js.. I've added the lines at the end to the RULES as metioned in the documentation
I ran npm install npm install imports-loader --save-dev
// SOURCE OF TUTORIAL
// https://www.youtube.com/watch?v=MpGLUVbqoYQ&t=1205s
const HtmlWebpackPlugin = require("html-webpack-plugin");
const path = require("path");
module.exports = {
mode: "development",
devtool: false,
entry: {
main: "./src/js/controller.js",
model: "./src/js/model.js",
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/main.html",
}),
],
module: {
rules: [
{
test: /\.scss$/,
use: [
"style-loader", // injects STYLES into DOM
"css-loader", // turns CSS into commonjs
"sass-loader", // turns SASS into CSS
],
},
{
test: /\.html$/,
use: ["html-loader"],
},
{
test: /\.(png|gif|svg|jpe?g)$/,
type: "asset/resource",
},
{
test: require.resolve("./path/to/example.js"),
loader: "imports-loader",
options: {
type: "module",
imports: "default lib myName",
},
},
],
},
};
The last section of the rule is what I have added.. it's a basic copy paste without really understanding what it does and it didn't work.. I am obviously doing somethibg wrong but I could not figure it out even after reading the section at the link above..
Could someone explain how the last section should be configured so that I am able to use import? I've recently created this webpack configuration based on a youtube tutorial so my knowledge on webpack is very basic..
Any help would be appreciated.

Webpack - 'velocity is not defined'

I'm using webpack along with gulp and this is my webpack config:
webpack.config.js
const path = require('path');
var HardSourceWebpackPlugin = require('hard-source-webpack-plugin');
module.exports = {
output: {
publicPath: "./dist/",
path: path.join(__dirname, "/js/"),
filename: "bundle.js"
},
module: {
loaders: [
{
test: /\.js$/,
exclude: /node_modules/,
loader: "babel-loader",
query: {
presets: ["env"]
}
},
{
test: /\.vue$/,
loader: 'vue-loader'
}
]
},
resolve: {
alias: {
moment: 'moment/src/moment'
}
},
externals: {
jquery: 'jQuery',
$: 'jQuery',
moment: 'moment',
"velocity-animate": 'velocity'
},
plugins: [
new HardSourceWebpackPlugin()
]
};
scripts.js ( This is all that's in this file )
import velocity from 'velocity-animate';
And I get this error
Uncaught ReferenceError: velocity is not defined
Error on this line:
module.exports = velocity;
Am I doing something wrong with the externals configuration?
This works for both moment.js and jQuery, but not for velocity...
I've tried
"velocity-animate": 'velocity'
and
"velocity-animate": 'velocity-animate'
and
"velocity-animate": '"velocity-animate"'
And none of these work. If the first one isn't 'velocity-animate' ( the name of the package ) then Velocity.js gets included in the script anyway. The documentation on this doesn't really explain how to properly configure this
Is it really possible that this use case is so niche that nobody on earth can explain it?
Thanks!
Lead dev of Velocity V2 here.
Doh - we'd missed updating the export of Velocity - I'll get that in later today. We're also in the process of module-ifying it, so you'll be able to import it "normally" within a Webpack project (including tree shaking etc) - that should be done in the next week or so.
Until I push an updated build the name it's exporting as is "Velocity" - note the capital "V" - hopefully later today it'll move over (2.0.2#beta will have the corrected name of "velocity-animate").

Use fullcalendar with webpack

I use npm, webpack and FullCalendar, but I get the following error in the browser console when using fullcalendar:
main.js:37556 Uncaught TypeError: (0 , _jquery2.default)(...).fullCalendar is not a function
How do I fix this?
I use FullCalendar 3.0.0-beta and jquery 3.1.0. My code is below.
index.js:
import $ from 'jquery'
import jQueryUI from 'jquery-ui'
import moment from 'moment'
import fullCalendar from 'fullcalendar'
$('#timetable').fullCalendar({
editable: true,
firstDay: 1,
droppable: true,
})
webpack.config.js:
var path = require("path")
var webpack = require("webpack")
var BundleTracker = require("webpack-bundle-tracker")
module.exports = {
context: __dirname,
entry: [
'fullcalendar',
'./static/index',
],
output: {
path: path.resolve('./static/bundles/'),
filename: "[name].js",
},
plugins: [
new BundleTracker({filename: './webpack-stats.json'}),
],
resolve: {
modulesDirectories: ['node_modules'],
extensions: ['', '.js'],
},
module: {
loaders:[
{ test: /\.js$/, exclude: /node_modules/, loader: 'babel', query: { presets: ['es2015'] } }
]
}
}
I know I am somewhat late to the party here, but I thought I'd answer anyway in case somebody hits this up on Google.
Whenever I run into a jQuery Plugin with Webpack (which FullCalendar is), I need to make sure that jQuery itself is exposed to the global namespace before the plugin will work through require/import.
My webpack.config.js:
var webpack = require("webpack")
var path = require("path")
var ExtractTextPlugin = require("extract-text-webpack-plugin")
var HtmlWebpackPlugin = require("html-webpack-plugin")
module.exports = {
entry: {
app: "./index.js",
vendor: [
"jquery",
"moment",
"fullcalendar"
]
},
output: {
path: path.join(__dirname, '../../public'),
publicPath: '/',
filename: "scripts/app.[chunkhash].js"
},
module: {
loaders: [
{ test: /\.css$/, loader: ExtractTextPlugin.extract("style", ["css"]) },
{ test: require.resolve('jquery'), loader: 'expose?$!expose?jQuery' },
{ test: require.resolve('moment'), loader: 'expose?moment' }
]
},
resolve: {
alias: {
jquery: path.resolve(path.join(__dirname, '../..', 'node_modules', 'jquery')),
fullcalendar: 'fullcalendar/dist/fullcalendar'
}
},
plugins: [
new webpack.optimize.DedupePlugin(),
new webpack.optimize.CommonsChunkPlugin({ names: ["vendor"], filename: "scripts/[name].[chunkhash].js" }),
new ExtractTextPlugin("styles/[name].[chunkhash].css"),
new HtmlWebpackPlugin({
template: "index.html.handlebars"
}),
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
]
};
The relevant part is where jquery and moment are forced to be in the global namespace via the loader: 'expose?$!expose?jQuery' syntax.
Second, because fullcalendar is packaged in a way that the require can't automatically pick it up, I setup an alias so that I can have a clean package name. This is the alias: { fullcalendar: 'fullcalendar/dist/fullcalendar' } bit.
These two let me load fullcalendar via require/import and use it as I normally would.
The styles also need to be loaded. For this one I have not created aliases yet, so I just did a relative path to the css file:
#import "../../../node_modules/fullcalendar/dist/fullcalendar.css";
You can replace fullcalendar.js with fullcalendar.min.js to avoid recompressing, but for my use case because I was bundling all the vendor JS files together, I thought I would get better compression if I had more files concatenated. (Ditto for CSS fullcalendar.css with fullcalendar.min.css)
Disclaimer: I don't know if this is the "correct" way of doing this, but I know it took me a fair bit of trial and error with webpack to get jQuery plug ins like FullCalendar and Select2 to work, and this shell and method did make it easy.
For reference, links to the relevant files in a public repo where I use this pattern:
webpack.config.js: https://github.com/thegrandpoobah/mftk-back-office/blob/e531de0a94130d6e9634ba5ab547a3e4d41c5c5f/app/src/public/webpack.config.js
style scss: https://github.com/thegrandpoobah/mftk-back-office/blob/e531de0a94130d6e9634ba5ab547a3e4d41c5c5f/app/src/public/styles/main.scss
module where I use fullcalendar: https://github.com/thegrandpoobah/mftk-back-office/blob/e531de0a94130d6e9634ba5ab547a3e4d41c5c5f/app/src/public/students/index.js#L277
This is a step by step guide based on the data from above and other sources. First make sure you have moment.js installed:
npm install moment
Then make sure you have the fullcalendar version 3.10.2, which is the latest in version 3 which is optimized not bundling jQuery nor moment.js , and although it's not the latest version, it uses the old syntax, which won't break compatibility with legacy code:
npm install fullcalendar#3.10.2
Then install script-loader
npm install --save-dev script-loader
If you are using Laravel, then in resources/js/bootstrap.js add the following lines below bootstrap and jquery (note the use of script-lader!) :
window.moment = require('moment');
require('script-loader!fullcalendar/dist/fullcalendar');
require('script-loader!fullcalendar/dist/locale-all');
Then add the css style in resources/sass/app.scss:
#import '~fullcalendar/dist/fullcalendar.min.css';
Finally do:
npm run dev
Or, for production:
npm run prod
That's all
I think I found an even easier solution.
We're using fullcalendar and scheduler. We're converting from Rails sprockets to webpack. Adding fullcalendar to a lazyloaded chunk with webpack caused it to introduce two additional moments and jquerys (yep two) which, of course, didn't pick up our configuration changes as those where done on the original version in our chunked vendor file.
Ideally we just wanted fullcalendar included with no module processing (it does absolutely nothing and is totally unnecessary). Fortunately you can do this with webpack's script-loader.
require('script-loader!fullcalendar/dist/fullcalendar.js')
And you're done. Same with the scheduler. It loads it in isolation and unprocessed which is exactly what you want with a jquery plugin.
With webpack 5, below code solves the issue:
module: {
rules: [
{
test: require.resolve('jquery'),
loader: 'expose-loader',
options: {
exposes: ["$", "jQuery"]
}
},
{
test: require.resolve('moment'),
loader: 'expose-loader',
options: {
exposes: "moment"
}
},
{
test: require.resolve('fullcalendar'),
use: [
{
loader: 'script-loader',
options: 'fullcalendar/dist/fullcalendar.js'
}
]
},
{
test: require.resolve('fullcalendar-scheduler'),
use: [
{
loader: 'script-loader',
options: 'fullcalendar/dist/fullcalendar-scheduler.js'
}
]
},
]
},
I used fullCalendar for example:
$("#fullcalendar-activities").fullCalendar({
header: {
left: 'prev,next today',
center: 'title',
right: 'month,basicWeek,basicDay'
},
events: events,
defaultView: 'month'
});

Automatically loading externals with Webpack

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']
}

Make webpack's library output compatible with babel6

Babel's 6th version changes the functioning of export default and in particular its relation with commonjs require.
To summarise, while until babel5, require('module') where giving the default export of the module, it now always returns the module object containing all of the exports of the module.
If one only wants the default, he/she must use require('module').default.
As explained here, there is very good reasons behind this and the aim of this question is not to break or hack this behaviour.
However, if one is building a library, he/she usually does not want to distribute a module but the export value of his library (e.g. a function, whatever module system is used internally).
This is well dealt with by webpack and the output.library configuration when using commonjs or AMD. Because prior babel's versions allowed the default export to be required with commonjs, babel was also compatible with this mechanism. However it is not the case anymore: the library now always provides an es6 module object.
Here is an example.
src/main.js
export default "my lib content";
webpack.config.js
var path = require("path");
var webpack = require("webpack");
module.exports = {
entry: {
lib: [ path.resolve(__dirname, "src/main.js") ],
},
output: {
path: path.join(__dirname, "dist"),
filename: "mylib-build.js",
library: 'myLib'
},
module: {
loaders: [
{
test: /\.js$/,
loader: "babel",
include: path.join(__dirname, "src"),
query: { presets: ['es2015'] }
}
]
}
};
test.html
<html>
<head></head>
<body>
<script src="dist/mylib-build.js"></script>
<!-- `myLib` will be attached to `window` -->
<script>
console.log(JSON.stringify(myLib)); // { default: "my lib content" }
</script>
</body>
</html>
This is a very simple example but I obviously want the export of mylib to be the string "my lib content" instead of { default: "my lib content" }.
One solution could be to create an export source file in commonjs to perform the transformation:
module.exports = require('./main').default;
However I find this solution quite poor. One should be able to solve it at the compilation level, without changing the source code.
Any idea?
Was just going at this my self. Whether one like to call it a workaround or solution, there seem to be a Babel plugin that "solve it".
Using the plugin babel-plugin-add-module-exports as referenced in https://stackoverflow.com/a/34778391/1592572
Example config
var webpackOptions = {
entry: {
Lib1: './src/Lib1.js',
Lib2: './src/Lib2.js'
},
output: {
filename: "Master.[name].js",
library: ["Master","[name]"],
libraryTarget: "var"
},
module: {
loaders: [
{
loader: 'babel',
query: {
presets: ['es2015'],
plugins: ["add-module-exports"]
}
}
]
}
};
This yields Master.Lib1 to be lib1 instead of Master.Lib1.default.
Webpack 2 now supports es6 modules which partially solves this issue. Migrating from webpack 1 to webpack 2 is relatively painless. One just needs to remember to disable babel's es6 module to commonjs conversion to make this work:
.babelrc
{
"presets": [
["es2015", {"modules": false}]
]
}
However, unfortunately, it does not work properly with export default (but an issue is opened, hopefully a solution will be released eventually).
EDIT
Good news! Webpack 3 supports the output.libraryExport option that can be used to directly expose the default export:
var path = require("path");
var webpack = require("webpack");
module.exports = {
entry: {
lib: [ path.resolve(__dirname, "src/main.js") ],
},
output: {
path: path.resolve(__dirname, "dist"),
filename: "mylib-build.js",
library: "myLib",
// Expose the default export.
libraryExport: "default"
},
module: {
loaders: [
{
test: /\.js$/,
loader: "babel",
include: path.resolve(__dirname, "src")
}
]
}
};
You can use this solution (this is more like workaround, but it allow you to keep your sources from change):
There is a loader called callback-loader. It allow you to change your sources in a build time by calling a callback and put a result instead of it. In other words you can turn all you require('module') into a require('module').default automatically in a build time.
Here is your config for it:
var webpackConfig = {
module: {
loaders: [
{ test: /\.js$/, exclude: /node_modules/, loader: 'callback' },
...
]
},
...
callbackLoader: {
require: function() {
return 'require("' + Array.prototype.join.call(arguments, ',') + '").default';
}
}
};

Categories

Resources