Webpack: How do I get functions working globally? - javascript

For the past 3-4 years, I've been using gulp to build a production JS file from a series of disparate JS files. I'm not using React or any other libraries, and my only assets are JS files written entirely in vanilla JS. With the gulp-built file, I'm able to call functions throughout my code without any issues - my files can talk to each other, so to speak.
But now, I'm wanting to migrate to from gulp to webpack. And for such a simple use case, I'm running into trouble when it comes to global scope. Whenever I test the bundled file in the browser, I can see undefined errors for functions in my file.
I realize that this outcome is to be expected, as webpack modularizes code a bit differently than gulp to prevent collisions. But surely there's a way to modify my webpack configuration so that it works like gulp has for me?
I've read through posts about similar issues and also reviewed the webpack docs to try and solve this on my own, but no luck.
Here's what I've tried:
Explicitly assigning functions to window.
function foo() {
...
}
window.foo = foo;
While this technically works, it's not a sustainable approach given how many functions are in my code. I don't want to be in the business of manually assigning each function to window.
Changing the value of useBuiltIns: Just for sh*** and giggles, I tried "usage" and "entry" in addition to the default option of "false" -- none of this worked.
Adding node.global: true to my module.exports. This didn't work either. Not to mention, I think this approach is outdated as of webpack 5.
Does anyone have guidance for how to get webpack working the way I need it to? Below is my current config.
webpack.config.js
const path = require('path');
const glob = require('glob');
const apis = glob.sync('./code/api/*.js');
const legacy = glob.sync('./code/legacy/*.js');
const standardTags = glob.sync('./code/standardTags/*.js');
const endpoints = glob.sync('./code/core/endpoints/*.js');
const auxFunctions = glob.sync('./code/aux-functions/*.js');
const sourceArray = [...apis, ...legacy, ...standardTags, ...endpoints, ...auxFunctions];
const presets = [
[
"#babel/preset-env",
{
"corejs": {
"version":3
},
"useBuiltIns": "usage",
"targets": {
"edge": "17",
"firefox": "60",
"chrome": "67",
"safari": "11.1",
"ie": "9"
}
}
]
];
module.exports = {
entry: [...sourceFileArray],
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'js/nep-build.js'
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets
}
}
}
]
},
};

Related

Why does webpack 5 bundle includes non typescript target features such as arrow function? [duplicate]

I want to use TypeScript modules and bundle them via Webpack. Here are my config files:
webpack.config.js:
const path = require('path');
module.exports = () => {
return {
entry: './index.ts',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
}],
},
};
};
tsconfig.json:
{
"compilerOptions": {
"module": "commonjs",
"target": "es3"
},
"include": ["./**/*"],
"exclude": ["node_modules/**/*", "webpack.config.js"]
}
Maybe I got something wrong from the documentation. The aim was to generate code in ES5 (or even earlier). But here is my bundled file:
(()=>{var n=function(n,o){console.warn(o)};n(0,0),n(0,1)})();
It has an arrow function, which was added in ES6. I am confused. How can I get rid of that?
EDIT:
Here's the code I try to compile:
const func = (foo, bar: number) => {
console.warn(bar);
};
func(0, 0);
func(2, 1);
EDIT 2:
Also, I run the compilation process in production mode. (idk, maybe that's useful information)
The decision is simply adding target: ['web', 'es5'] to your webpack configuration.
You could also set target: 'es5', but in this case, there are some problems. At least in my case without specifying 'web' TerserWebpackPlugin refused to compress files in the production mode.
That's the problem I faced last week after upgrading the webpack to version 5.
By default, it's bundling it as ES6. In your webpack configuration, configuring output.environment should resolve the problem.
Edit: webpack only change it's internal usages with these configurations.
webpack.js.org/configuration/output/#outputenvironment
It does not compile the modules. For the module compilation, babel should be used.

Material-UI have different style result in production mode?

I am writing a React app that uses server-side rendering. I am following the instruction here to set up a file.
Here is my .babelrc configuration file
{
"presets": [
[
"#babel/preset-env",
{
"useBuiltIns": "usage",
"corejs": { "version": 3, "proposals": true },
"targets": {
"browsers": "> 1%, not ie 11, not op_mini all",
"node": 13
}
}
],
"#babel/preset-react"
],
"plugins": [
"#babel/plugin-proposal-class-properties",
"#babel/plugin-transform-runtime",
[
"import",
{
"libraryName": "#material-ui/icons",
"libraryDirectory": "utils", // default: lib
"camel2DashComponentName": false // default: true
},
"#material-ui/icons"
]
]
}
And this is webpack.config.js file
const path = require("path");
const nodeExternals = require("webpack-node-externals");
const commonConfig = {
devtool: "source-map",
module: {
rules: [{ test: /\.js$/, exclude: /node_modules/, loader: "babel-loader" }]
},
resolve: {
alias: {
"#material-ui/core": "#material-ui/core/es"
}
}
};
module.exports = [
{
...commonConfig,
entry: "./src/client",
output: {
path: path.resolve(__dirname, "public")
}
},
{
...commonConfig,
target: "node",
entry: "./src/server",
output: {
path: path.resolve(__dirname, "server")
},
externals: [nodeExternals()]
}
];
(Here is the full code in CodeSandBox and here is in Github if you want to try out)
The problem appear is when I bundle up the file, in development mode, everything work just fine. But when I try production mode, the CSS part starts to behave weirdly. When the file is first loaded from localhost, it is styled correctly (this happens in a very short time), then the style goes wrong as some styling is missing.
When I try to investigate, I find that all the style that is missing is the part I wrote with makeStyles(). All the built-in styles work just fine.
I tried to remove all the resolve property in webpack.config.js following this post, but it doesn't work. No matter what I try to change that property, nothing happens.
So now the only way I found that can make the app work in production build is to remove the code that removes the styling file (you can find that part at the end of client.js file), but I'm not sure what is the result of doing so.
So my questions are:
What can you suggest the fix for the app?
Why there is such a difference between two modes, development and production? I get that the production mode will include some minification, tree shaking, etc., and the development has most of that except for minification. So why there is a difference?
Edit: I found two possible and workable fixes for this bug: one is to stop removing those CSS files (code that I wrote in client.js file); the other one is to remove the nodeExternal() plugin in webpack.config.js and bundle everything for the server file. What do you think ?
I had a similar issue, although without server-side rendering. It was caused by a different order of stylesheets in dev and prod environments, causing unwanted overwrites. In dev env all stylesheets created by makeStyles() were injected after ALL MUI stylesheets, but in production they were mixed.
Solution:
Add an option: { index: 1 } to all makeStyles() invocations, in order to place those sheets after the MUI sheets which have an index of 0 (by default). This optional argument is passed directly to underlying JSS's jss.createStyleSheet() and dictates the injection order:
const useStyles = makeStyles(
(...), // styles
{ index: 1 }, // optional argument for JSS, to set position after MUI stylesheets
)
(after: https://stackoverflow.com/a/62646041/624597)

how can I remove unused code when I build my bundle?

I am not sure how to organize my js code.
Our front end is customized to different customers. Majority of the base code is common across all customers. However there are cases where certain functionality is overridden for each customer.
For example if we have 2 functions Function1 and Function2.
Customer1 uses Function1 while Customer2 uses Function2. How can I make sure that when I build the code for Customer, Function2 will not be included in the bundle? And when I build the code for Customer2, then Function1 will not be included int he bundle?
The other option I have, and that I am trying to avoid, is to have a separate code repo for each customer.
I think what you need is Tree-Shaking in webpack.
Tree shaking can be a stubborn process depending on how the library you are using in your application is developed.
If you find that you are using a module that does not shake dead code properly, you can always use the babel-plugin-import plugin. This plugin will build your bundle with only the code you import and nothing else. Here is an example of my babel 7.x config file. I use it to remove a lot of code that was not being tree-shaken by webpack from material-ui.
{
"presets": [
"#babel/preset-typescript",
"#babel/preset-react"
],
"plugins": [
[
"babel-plugin-import",
{
"libraryName": "#material-ui/core",
"libraryDirectory": "/",
"camel2DashComponentName": false
},
"core"
],
[
"babel-plugin-import",
{
"libraryName": "#material-ui/icons",
"libraryDirectory": "/",
"camel2DashComponentName": false
},
"icons"
]
]
}
When using this plugin in certain libraries, some of your imports also may break and you may need to import certain things on their own. I had to do this with material-ui's makeStyles function.
Feel free to remove what is unnecessary to you and keep the parts that help :).
At webpack configuration, optimization/usedExports: true will remove unused code.
webpack.config.js
module.exports = [
{
entry: "./main.js",
output: {
filename: "output.js"
},
optimization: {
usedExports: true, // <- remove unused function
}
},
{
entry: "./main.js",
output: {
filename: "without.js"
},
optimization: {
usedExports: false, // <- no remove unused function
}
}
];
lib.js
exports.usedFunction = () => {
return 0;
};
exports.unusedFunction = () =>{
return 1;
};
main.js
// Not working
// const lib = require("./lib");
// const usedFunction = lib.usedFunction;
// Working
const usedFunction = require("./lib").usedFunction;
usedFunction()
```shell
$ webpack
Generated Output file:
dist/output.js
(()=>{var r={451:(r,t)=>{t.W=()=>0}},t={};(0,function e(o){var n=t[o];if(void 0!==n)return n.exports;var p=t[o]={exports:{}};return r[o](p,p.exports,e),p.exports}(451).W)()})();
dist/without.js
(()=>{var n={451:(n,r)=>{r.usedFunction=()=>0,r.unusedFunction=()=>1}},r={};(0,function t(u){var e=r[u];if(void 0!==e)return e.exports;var o=r[u]={exports:{}};return n[u](o,o.exports,t),o.exports}(451).usedFunction)()})();
^^^^^^^^^^^

Named functions in a javascript file using Webpack 4

I just started using Webpack 4 in a project and am new to Webpack. Once Webpack 4 was implemented, I noticed that named functions kept erroring saying [functionName] is not defined.
I have looked quite extensively over the last few days and have tried multiple options with no success. I am hoping for someone to help me work through this in a more direct fashion way.
function openNav(obj) {
...do something
}
#foreach (object in list)
{
...create some HTML
}
const bundleFileName = 'bundle';
const dirName = 'wwwroot/dist';
module.exports = (env, argv) => {
return {
mode: argv.mode === "production" ? "production" : "development",
entry: [
'./src/Index.js',
'./src/css/site.css',
'./src/js/app.js'
],
output: {
filename: bundleFileName + '.js',
path: path.resolve(__dirname, dirName)
},
module: {
rules: [
{
test: /\.css$/,
use: [{ loader: 'style-loader' }, { loader: 'css-loader' }]
}
]
},
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
}),
new CleanWebpackPlugin(),
new MiniCssExtractPlugin({
filename: bundleFileName + '.css'
})
]
};
};
I expected the button in the razor file to send the object over to the named function so something can happen like it did previously before webpack
You've said you're trying to use openNav from an onclick attribute. The problem there is that openNav has to be a global to be used that way, but Webpack is a module bundler. Functions at the top level of modules aren't globals. (Which is a good thing.)
The solution is to not use onxyz-attribute-style event handlers. Instead, use modern event handling (addEventListener, probably with at least some event delegation).
To ease your transition from the old way to the modern way, you can expose a function globally from within a module like this (on browsers):
window.openNav = openNav;
I strongly recommend only doing that temporarily to make it easier to transition to modern event handling.
thats because your webpack config isnt set to understand how to handle javascript files, add this to your rules key:
{
test: /\.jsx?/,
loader: 'babel-loader'
}
you will also need to install babel loader that is in a version compatible with your babel core, for v7^ you need to install #babel/core #babel/loader etc, otherwise, babel-loader etc

Is there a way to hook into Webpack's AST to make it recognize a new module format?

Short version:
How do we manipulate the AST for the final output bundle, as well as the AST for a file from inside a loader? In both cases, I'd like to manipulate an existing AST rather than what I'm doing which is to parse the sources and make a new AST. What I'm doing is slow, and I know that Webpack must already have made an AST so I want to avoid duplicating efforts.
Long version:
For example, suppose I have a bunch of files written in a format similar to (but not quite) AMD modules:
module({
Foo: '/path/to/Foo',
Bar: '/path/to/Bar',
Baz: '/path/to/Baz',
}, function(imports) {
console.log(imports) // {Foo:..., Bar:... Baz:...}
})
The difference is that it is called module instead of define, the dependencies argument is a map of import names to module paths instead of an array of module paths, and the module body function receives an import object with all requested imports instead of one argument per requested import.
The above is similar to the following in AMD format, with the same output:
define([
'/path/to/Foo',
'/path/to/Bar',
'/path/to/Baz',
], function(Foo, Bar, Baz) {
console.log({Foo, Bar, Baz}) // {Foo:..., Bar:... Baz:...}
})
What is the recommended way to hook into Webpack to make Webpack be able to understand the files (be able to know what dependencies the file has) in order to finally build a bundle with files that are written in this module() format?
I've already tried one approach: I made a custom loader that receives a file's source as a string, parses it and creates and AST, transform the AST, then outputs the code in AMD define() format, which Webpack understands.
However, I feel like this is slow, because if there are many files and if they are big, then parsing and making an AST from each files seem redundant, because I bet Webpack is already doing that to begin with. Is there some way to get the AST from Webpack and transform it before Webpack wants to scan it's dependencies, so that I can transform the AST into AMD format (or any recognized format for that matter), so that Webpack can finally work with the file? Is there another approach?
I think you will find that the loader is used during dependency parsing.
Basically the parser needs source code to be able to do its job. Therefore any import/require statement (a dependency) that is encountered during the current parse phase needs to: a. be resolved and: b. be loaded before it can be parsed. If you hook into the enhanced-resolve package's "resolve-step" you can console.log out the state transitions that the resolver transitions through typically culminating in the "create-module" plugins being fired.
Hook into "resolve-step":
compiler.plugin('after-resolvers', (compiler) => {
compiler.resolvers.normal.plugin('resolve-step', function (type, request){
console.log("resolve-step type:["+type+"],
path:["+request.path+"], request:["+request.request+"]");
});
});
Hook into "create-module":
compiler.plugin('compilation', (compilation, params) => {
params.normalModuleFactory.plugin("create-module", (data) => {
console.log('create-module: raw-request: '+data.rawRequest);
}
}
Hope this helps.
I was looking for something like this, want to manipulate the ast but there is no example or useful documentation out there.
Googling for it just wasted 2 hours of my time but with the half completed and incomprehensible documentation I did come up with this (doesn't work though):
var acorn = require("acorn-dynamic-import").default;
function MyPlugin(options) {
// Configure your plugin with options...
}
MyPlugin.prototype.apply = function(compiler) {
compiler.plugin("compilation", function(compilation, data) {
data.normalModuleFactory.plugin(
"parser"
, function(parser, options) {
parser.plugin(
[
"statement if"
]
,(node)=>
Object.assign(
node
,{
test:{
type:"Literal",
start: node.test.start,
end: node.test.start+4,
loc: {
start: {
line: 7,
column: 3
},
end: {
line: 7,
column: node.test.loc.start.column+4
}
},
range: [
node.test.range,
node.test.range+4
],
value: true,
raw: "true"
}
}
)
);
});
});
};
module.exports = MyPlugin;
That would get me a node of an if statement, for other types of nodes you can look in Parser.js.
I try returning another node but creating a node is a lot of work and there doesn't seem to be an easy way to do this.
Why bother trying to do this in webpack anyway? The webpack contributors have made a nice product but nobody knows how it works because there is no documentation for many of it's features.
You're probably better off doing this as a babel plugin, that has well written and understandable documentation.
I got this working in about 20 minutes with babel:
module.exports = function ({ types: t }) {
return {
visitor: {
IfStatement(path, file) {
path.node.test = {
"type": "BooleanLiteral",
"start": path.node.test.start,
"end": path.node.test.start+4,
"loc": {
"start": {
"line": 7,
"column": path.node.test.loc.start.column
},
"end": {
"line": 7,
"column": path.node.test.loc.start.column+4
}
},
"value": true
}
// debugger;
// path.unshiftContainer('body', t.expressionStatement(t.stringLiteral('use helloworld')));
}
}
};
};
In webpack.config.js:
const path = require("path");
const webpack = require("webpack");
module.exports = {
entry: "./src",
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].chunk.js"
},
module:{
rules: [
{
test: /\.html$/,
use: [{ loader: './plugin/templateLoader' }]
},
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: ['env'],
plugins: [require("./plugin/myBabelPlugin")]
}
},
}
]
}
}

Categories

Resources