How to import remote AMD modules with Angular2+WebPack? - javascript

In my Angular2+TypeScript+WebPack project I would like to use / import the Esri ArcGIS JavaScript API from the following url:
https://js.arcgis.com/4.1/
so that a import Map from 'esri/Map'; would import the following module
https://js.arcgis.com/4.1/esri/Map.js
I've seen people are typically using systemjs to load these modules, however, I would prefer not to use systemjs. Is there a way to do that with webpack only?
webpack.config:
var webpack = require("webpack");
module.exports = {
entry: {
'polyfills': './app/polyfills.js',
'vendor': './app/vendor.js',
'app': './app/boot.js'
},
output: {
path: __dirname,
filename: "./prod/[name].js"
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: ['app', 'vendor', 'polyfills']
}),
new webpack.optimize.UglifyJsPlugin({
beautify: false,
mangle: {screw_ie8: true, keep_fnames: true},
compress: {screw_ie8: true},
comments: false
})
]
};

webpack is a module bundler not a javascript loader. It package files from local disk and don't load files from the web (except its own chunks). Use a javascript loader, i.e. script.js
https://github.com/webpack/webpack/issues/150#issuecomment-32756166
It's also worth noting you won't be able to load a module from an external source and then import using es6 syntax as the remote module needs to go through Webpack. You'll have to make sure you fetch a UMD / global script.

I followed this sample and was able to load dojo, webpack, typescript and angular 2.
Just be patient, is still hellish with imports eg:
import * as Map from 'esri/Map';
import * as Point from 'esri/geometry/Point';
import * as Circle from 'esri/geometry/Circle';

Related

Tree-shaking with rollup

I have a project in which I bundle a components library using Rollup (generating a bundle.esm.js file). These components are then used in another project, that generates web pages which use these components - each page is using different components.
The problem is, that the entire components library is always bundled with the different page bundles, regardless of which components I'm using, unnecessarily increasing the bundle size.
This is my Rollup setup:
import resolve from 'rollup-plugin-node-resolve';
import commonjs from 'rollup-plugin-commonjs';
import babel from 'rollup-plugin-babel';
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import pkg from './package.json';
const extensions = [
'.js', '.jsx', '.ts', '.tsx',
];
export default [
{
input: './src/base/index.ts',
plugins: [
peerDepsExternal(),
resolve({ extensions }),
babel({
exclude: 'node_modules/**',
extensions,
}),
commonjs(),
],
output: [
{ file: pkg.main, format: 'cjs', sourcemap: true },
{ file: pkg.module, format: 'es', sourcemap: true },
],
watch: {
clearScreen: false,
},
},
];
I have "modules" set to false in webpack, as well.
There are things you will need to do to achieve treeshakable code from both sides - the built package and the project using it.
From your code snippet, I see that you have not add flag preserveModules: true in the rollup config file to prevent the build output from bundling. Webpack can not treeshake a bundled file FYI.
export default {
...
preserveModules: true,
...
}
On the side of the project that using it, you have to specify sideEffects in the package.json - read the doc to know how to config them. Beside that, the optimization in webpack has to has sideEffects: true, also read the doc here.
Hope this helps!
As you don't know which components of your Component Library (CL) will be needed by the adopters repositories you need to export everything but in a way
the adopters can execute a tree-shaking on your CL when they do their own build (and just include what they really need).
In a few words, you have to make your CL, tree-shakable. In order to achieve this, on your CL repo you have to:
Use bundlers that support tree-shaking (rollup, webpack, etc..)
Create the build for modules of type es/esm, NOT commonJS/cjs, etc..
Ensure no transpilers/compilers (babel,tsconfig, etc..) usually used as plugins, transform your ES module syntax to another module syntax.
By the default, the behavior of the popular Babel preset #babel/preset-env may break this rule, see the documentation for more details.
// babelrc.json example that worked for me
[
"#babel/preset-env",
{
"targets": ">0.2%, not dead, not op_mini all"
}
],
In the codebase, you always have to use import/export (no require) syntax, and import specifically the things you need only.
import arrayUtils from "array-utils"; //WRONG
import { unique, implode, explode } from "array-utils"; //OK
Configure your sideEffects on the package.json.
"sideEffects": ["**/*.css"], //example 1
"sideEffects": false, //example 2
DO NOT create a single-bundle file but keep the files separated after your build process (official docs don't say this but was the only solution that worked for me)
// rollup.config.js example
const config = [
{
input: 'src/index.ts',
output: [
{
format: 'esm', // set ES modules
dir: 'lib', // indicate not create a single-file
preserveModules: true, // indicate not create a single-file
preserveModulesRoot: 'src', // optional but useful to create a more plain folder structure
sourcemap: true, //optional
},
],
... }]
Additionally, you may need to change your module entry point in order the adopters can directly access to the proper index.js file where you are exporting everthing:
// package.json example
{
...
"module": "lib/index.js", //set the entrypoint file
}
Note: Remember that tree-shaking is executed by an adopter repository that has a build process that supports tree-shaking (eg: a CRA repo) and usually tree-shaking is just executed on prod mode (npm run build), no on dev mode. So be sure to properly test if this is working or not.

webpack import only variable value

I'm compiling code that needs the version value from package.json:
import {version} from '../package.json';
export default {version};
and when I look at the .js file that webpack outputs I see the whole package.json there!
How can I avoid this?
My setup is:
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"'
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
}),
new CompressionPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: /\.(js|css)$/,
threshold: 10240,
minRatio: 0.8
}),
]
My webpack version is 3.8.1
What I usually do is take advantage of the DefinePlugin
// webpack.config.js
// getting the version
const package = require("./package.json");
const version = package.version;
plugins: [
new webpack.DefinePlugin({
'process.env.NODE_ENV': '"production"',
'process.env.VERSION': version,
}),
new webpack.optimize.UglifyJsPlugin({
compress: {
warnings: false
}
}),
new CompressionPlugin({
asset: '[path].gz[query]',
algorithm: 'gzip',
test: /\.(js|css)$/,
threshold: 10240,
minRatio: 0.8
}),
]
Now in your code all you gotta do is use process.env.VERSION and you it will output your packages version.
Hope it helped
Modern webpack versions support Tree shaking (https://webpack.js.org/guides/tree-shaking/ ), but it works only if export directives are configured by special scheme, includes independent named import for each entity. In that case webpack can perform analyze dependencies graph and include only required entities.
Also, import directive does not support destructing - it's only syntax for named import, so large JS object will be always imported in monolithic style.
Import as value is unavailable by definition, because webpack performs only bundling for set of files with source code and maybe custom resources dependencies. Real imports in ES6 modules, which today are already supported on most platforms, also do not provide values imports - instead it's binding. https://ponyfoo.com/articles/es6-modules-in-depth#bindings-not-values.
Of course, original problem can be solved by webpack replace or alias plugins. Just store version in some independent file or string constant and substitute it due bundling. Most straightforward solution is
import version from 'PACKAGE_VERSION'
and then configure external with callback https://webpack.js.org/configuration/externals/ like that
externals: [
function(context, request, callback) {
if (request === 'PACKAGE_VERSION'){
return callback(null, 'exports.default = ' + JSON.stringify(JSON.parse(fs.readFileSync('package.json')).version));
}
callback();
}
],

Webpack bundles my files in the wrong order (CommonsChunkPlugin)

What I want is to bundle my JavaScript vendor files in a specific order via CommonsChunkPlugin from Webpack.
I'm using the CommonsChunkPlugin for Webpack. The usage from the official documentation is straight forward and easy. It works as intended but I believe the plugin is bundling my files in alphabetical order (could be wrong). There are no options for the plugin to specify the order they should be bundled.
Note: For those who are not familiar with Bootstrap 4, it currently
requires a JavaScript library dependency called Tether.
Tether must be loaded before Bootstrap.
webpack.config.js
module.exports = {
entry: {
app: './app.jsx',
vendor: ['jquery', 'tether', 'bootstrap', 'wowjs'],
},
output: {
path: __dirname + '/dist',
filename: 'bundle.js',
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: 'vendor.bundle.js'
}),
new webpack.optimize.UglifyJsPlugin(),
],
};
Two things are happening here:
vendor.bundle.js contains bootstrap, jquery, tether,
wowjs
bundle.js contains the rest of my application
Bundling order:
correct: jquery, tether, bootstrap, wowjs
incorrect: bootstrap, jquery, tether, wowjs
Notice in my webpack.config.js I ordered them exactly as they should but they are bundled in the incorrect order. It doesn't matter if I rearrange them randomly the result is the same.
After I use Webpack to build my application, the vendor.bundle.js shows me the incorrect order.
I know they're bundled incorrectly cause Chrome Dev. Tools tell me there are dependency issues. When I view the file through the tool and my IDE, it is bundled in the incorrect order.
My other approach also resulted in the same issue
I also tried import and require in my entry file (in this case, app.jsx) without the use of the CommonChunkPlugin and that also loads my JavaScript libraries in alphabetical order for some reason.
webpack.config.js
module.exports = {
entry: './app.jsx',
output: {
path: __dirname + '/dist',
filename: 'bundle.js',
},
plugins: [
new webpack.optimize.UglifyJsPlugin(),
],
};
app.jsx (entry)
import './node_modules/jquery/dist/jquery.min';
import './node_modules/tether/dist/js/tether.min';
import './node_modules/bootstrap/dist/js/bootstrap.min';
import './node_modules/wowjs/dist/wow.min';
or
require('./node_modules/jquery/dist/jquery.min');
require('./node_modules/tether/dist/js/tether.min');
require('./node_modules/bootstrap/dist/js/bootstrap.min');
require('./node_modules/wowjs/dist/wow.min');
The result?
Bootstrap > jQuery > Tether > wowjs
How do I load my vendor files in the correct order?
Success!
webpack.config.js
module.exports = {
entry: {
app: './app.jsx',
vendor: [
"script-loader!uglify-loader!jquery",
"script-loader!uglify-loader!tether",
"script-loader!uglify-loader!bootstrap",
"script-loader!uglify-loader!wowjs",
]
},
output: {
path: __dirname + '/dist',
filename: 'bundle.js',
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: 'vendor.bundle.js'
}),
new webpack.optimize.UglifyJsPlugin(),
],
};
What magic is happening here?
Webpack creates vendor.bundle.js by minifying & bundling my vendor
files which now execute in the global context.
Webpack creates bundle.js with all of its application code
entry file (app.jsx in this case)
import './script';
This script is just custom JavaScript that uses jQuery, Bootstrap, Tether and wowjs. It executes after vendor.bundle.js, allowing it to run successfully.
A mistake I made trying to execute my script.js was that I thought it had to be in the global context. So I imported it with script-loader like this: import './script-loader!script';. In the end, you don't need to because if you're importing through your entry file it will end up in the bundle file regardless.
Everything is all good.
Thanks #Ivan for the script-loader suggestion. I also noticed that the CommonsChunkPlugin was pulling the non-minified vendor versions so I chained uglify-loader into the process.
Although, I do believe some .min.js are created differently to get rid of extra bloat. Though that is for me to figure out. Thanks!
You can try https://webpack.js.org/guides/shimming/#script-loader - it looks like it will execute scripts in order and in global context.
Worked with htmlWebpackPlugin from official tutorials and switched the order form entry key. ( vendor then app )
In webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
vendor: [
'angular'
],
app: [
'./src/index.js',
'./src/users/users.controller.js',
'./src/users/users.directive.js',
]
},
plugins: [
new CleanWebpackPlugin(['dist']),
new HtmlWebpackPlugin({
template: './src/index-dev.html'
}),
new webpack.NamedModulesPlugin()
...
}
Now in the generated index.html file I have the correct order
<script src='vendor.bundle.js'></script>
<script src='app.bundle.js'></scrip
This worked for me https://www.npmjs.com/package/webpack-cascade-optimizer-plugin
const CascadeOptimizer = require('webpack-cascade-optimizer-plugin');
module.exports = {
entry: {
app: './app.jsx',
vendor: ['jquery', 'tether', 'bootstrap', 'wowjs'],
},
output: {
path: __dirname + '/dist',
filename: 'bundle.js',
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: 'vendor',
filename: 'vendor.bundle.js'
}),
new webpack.optimize.UglifyJsPlugin(),
new CascadeOptimizer({
fileOrder: ['jquery', 'tether', 'bootstrap', 'wowjs']
})
],
};

Webpack and toastr

I am trying to load and bundle toastr as a dependency using webpack.
here is the entire webpack config file
var path = require('path');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var webpack = require('webpack');
const DEVELOPMENT = process.env.NODE_ENV === 'development';
const PRODUCTION = process.env.NODE_ENV === 'production';
module.exports = {
entry: {
main: './wwwroot/js/mainEntry.js',
vendor: ['jquery', 'tether',
'bootstrap', 'jquery-colorbox',
'jquery-confirm', 'jquery-validation',
'jquery-validation-unobtrusive',
'toastr', 'jquery.nicescroll',]
},
output: {
filename: '/js/[name].js',
path: path.resolve(__dirname, 'wwwroot'),
},
module: {
rules: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
fallback: "style-loader",
use: "css-loader"
})
},
{
test: /\.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
loader: 'file-loader?name=[name].[ext]&publicPath=/fonts/&outputPath=/fonts/'
},
{
test: /\.(png|jpe?g|gif|ico)$/,
loader: 'file-loader?name=[name].[ext]&publicPath=/images/&outputPath=/images/'
}
]
},
plugins: [
new webpack.optimize.CommonsChunkPlugin({
name: "vendor",
// (the commons chunk name)
filename: "/js/vendor.js",
// (the filename of the commons chunk)
minChunks: 2,
}),
new ExtractTextPlugin({
filename: 'css/[name].min.css'
}),
new webpack.ProvidePlugin({
$: "jquery",
jQuery: "jquery"
})
],
};
and my entry js file as
//JS
import 'jquery-colorbox';
import 'slick-carousel';
import toastr from 'toastr';
//CSS
import './../../node_modules/bootstrap/dist/css/bootstrap.css';
import './../../node_modules/slick-carousel/slick/slick.css';
import './../../node_modules/jquery-colorbox/colorbox.css';
import './../../node_modules/toastr/build/toastr.css';
import './../../node_modules/jquery-confirm/css/jquery-confirm.css';
import './../../node_modules/font-awesome/css/font-awesome.css';
import './../../Modules/Components/Menu/menu.css';
import './../../wwwroot/css/lahuritv.css';
The bundle is created without any errors. And I can see that the toastr script is included in the bundle when I look at the output bundle. But the problem is that the variable toastr is not available in the browser window.
I tried looking for similar issue but couldn't find any. This is my first time trying to learn webpack. Any help is appreciated.
Simple solution :
//app.js
import toastr from 'toastr'
window.toastr = toastr
Webpack does not expose the modules you import, it bundles your code together with the needed dependencies, but they are still modular and have their own scope. Webpack does not just blindly combine the sources of all the files and certainly does not make everything global. If the entry file you posted is the whole file, then you're not doing anything at all.
Instead with webpack you would do the work in the JavaScript files you bundle. So to speak, all you include in your HTML are the bundles created by webpack, you don't include other scripts that try to access something in the bundle, but you rather do it directly in your bundle.
Of course you could expose it to window by explicitly defining it: window.toastr = toastr after importing toastr, but polluting the global scope is generally not a good idea and it's no different from just including toastr in a <script> tag. If the bundle is just supposed to be used as a library you could have a look at Authoring Libraries. But I think you're just doing a regular web app and you should get all the code together to be bundled by webpack and not rely on it in another <script> tag.
You should go through the Getting Started guide of the official docs. The example is very tiny (creates one DOM element) but shows you the concept of webpack apps and also uses an external dependency.

How to exclude vendor module peerDependencies from export in Webpack?

Question
When exporting a bundle in Webpack, how can I exclude 3rd-party module's peerDependency? (not 3rd-party module itself)
Background
I would like to create an UIkit with customized components on top of angular-material framework. With Webpack, I can bundle my customzied components and angular-material together into something like uikit.js, and then port to other application later on. However, I don't want to include angular module itself into this uikit.js.
Issue
It seems that Webpack is "clever" enough to notice that angular module is a dependency of angular-material module, and thus would export both angular module and angular-material module to the bundle. One can either use config.externals: {'angular': 'angular'} or new webpack.IgnorePlugin(/angular$/) to exclude angular module which are explicitly require in the app, but for peerDependency (i.e. the one require inside angular-material), it would still include it.
So, how could I exclude this 3rd-party depended modules out from export?
Example:
// app.js
var angular = require('angular');
var material = require('angular-material');
// ... other application logic
// webpack.config.js
var webpack = require('webpack');
module.exports = {
entry: {
app: './app.js'
},
module: {
loaders: [
// some module loaders
]
},
// This only excludes the angular module require by the app, not the one require by the angular-material
externals: {'angular': 'angular'},
plugins: [
// This is the same as externals, only the one required by app.js would be excluded
new webpack.IgnorePlugin(/angular$/)
]
};
In the webpack (version 4) config, I export vendor apps into a vendor bundle and chunk all like this:
entry: {
app: './app.js',
vendor: [ 'angular', 'angular-material', '...' ],
},
optimization: {
splitChunks: {
chunks: 'all',
}
},
Basically, this indicates which chunks will be selected for optimization and all means that chunks can be shared even between async and non-async chunks. Furthermore, How your modules are built and how you handle dependancies can further streamline your chunk size.
In addition you can provide a function whose return value will indicate whether to include each chunk:
module.exports = {
//...
optimization: {
splitChunks: {
chunks (chunk) {
// exclude `my-excluded-chunk`
return chunk.name !== 'my-excluded-chunk';
}
}
}
};
Here is a link to webpack explaining the splitChunks plugin.

Categories