Expose javascript globals bundled via webpack
Asked Answered
L

6

30

The Problem

I feel like this should be more straightforward than it is. I need to access all my javascript libraries from the frontend and because I'm integrating it into an old system, I cannot call require("bundle.js"); from the frontend. Everything in the global scope of the bundled files must be accessible from the global scope of the frontend page importing them through the <script> tag.

So I need to change the old:

<script src="js/jquery.js"></script>
<script src="js/silly.js"></script>
<script>
    $(silly()); // Some function in silly.js's global scope
</script>

To the new:

<script src="js/bundle.js"></script>
<script>
    $(silly()); // Some function in silly.js's global scope
</script>

Things I've tried

  1. expose-loader: This would totally work if I didn't have 100 global variables that I don't want to explicitly tell it to look for.

  2. ProvidePlugin: Only really lets the libraries see the other libraries. I also cannot explicitly write all the globals I need with my current setup (More are added constantly).

What I need

So for more clarity, I need my webpack.config.js to look like one of these options:

// Everything is wrapped in module.exports and other irrelevant things
plugins: [
         new StaticLibraryMergerSuperNeatPlugin("js/*.js")
]
// ...

Or:

rules: [
        {
            test: /\.js$/,
            use: [
                "neat-merging-cool-loader",
                "babel-loader"]
            
        }
]
// ...

Am I going about this wrong?

Is there an obvious solution I am missing?

Tl;Dr: How do I make globals from my bundled js files, be exposed to the global scope when imported on a frontend html page via <script src="js/bundle.js"></script>?

Btw: If anyone is a webpack legend and knows why this is a bad approach, please post below with a brief explanation so I can fix my life.

Laurent answered 23/6, 2017 at 15:2 Comment(1)
You can declare them on the window object in your individual scripts that get bundled. You can also use a single file as an entry point and say window["silly"] = require("silly.js"), etc. That's what I do, anyway.Less
L
4

Note: This is not the ideal scenario but because I have a constant amount of new globals being added, I needed to make a plugin to bundle my javascript for me.

webpack-raw-bundler

This simply stacks your code together to be included on the frontend. Here is my usage example:

Usage

From the old:

<script src="js/jquery.js"></script>
<script src="js/silly.js"></script>
<script>
    $(silly()); // Some function in silly.js's global scope
</script>

To the new:

<script src="js/bundle.js"></script>
<script>
    $(silly()); // Some function in silly.js's global scope
</script>

Installing to the config

  var RawBundlerPlugin = require('webpack-raw-bundler');

  module.exports = {
    plugins: [
       new RawBundlerPlugin({
             excludedFilenames: [/angulartics/],
             readEncoding: "utf-8",
             includeFilePathComments: false,
             bundles: [ "vendor.js", "styles.css" ],
             "vendor.js": [
                'js/*.js'
             ],
             "styles.css": [
                'css/bootstrap.css',
                'css/edits.css'
             ]
       })
    ]
 }

A Fair Warning:

This should not be your go-to solution, but I had a bad case that made this the easiest option. Using expose-loader and import or window['module'] = require('module.js') is much safer as that is what webpack was built around. However, if you are having some headaches and just want a simple bundler, feel free to use this plugin.

Laurent answered 23/6, 2017 at 19:51 Comment(2)
Could minification and hashing be added?Overrule
I did this externally via another package to keep everything modularLaurent
L
16

Here's an example of how I do it in my own site. I'm not sure if it's the only way, or even the best way, but it's clean, simple, and it works for me.

Important side note - Use window["propName"] when declaring things on the window because when you run webpack -p it will uglify any non-strings, so if you define it as window.propName, it can get changed to something like s.c and the rest of your code does not know what it is. Declaring it with bracket notation as a string will force webpack to keep the name intact so you can access it from anywhere with the same name.

site.ts (can be .js, doesn't matter)

/*************************/
/*** JQUERY + JQUERYUI ***/
/*************************/
/* var declaration for typescript - not needed if not using .ts */
declare var $:JQueryStatic; declare var jQuery:JQueryStatic;
window["$"] = window["jQuery"] = require("jquery");
require("jquery-ui/effects/effect-slide");
require("jquery-ui/widgets/autocomplete");
require("jquery-ui/widgets/button");
require("jquery-ui/widgets/datepicker");
require("jquery-ui/widgets/tooltip");
/*************************/
/* END JQUERY + JQUERYUI */
/*************************/

/***************/
/*** ANGULAR ***/
/***************/
/* var declaration for typescript - not needed if not using .ts */
declare var angular:ng.IAngularStatic;
window["angular"] = require("angular");
require("angular-sanitize");
/***************/
/* END ANGULAR */
/***************/

/************************/
/*** MISC THIRD-PARTY ***/
/************************/
window["moment"] = require("moment");
window["saveAs"] = require("FileSaver").saveAs;
window["JSZip"] = require("jszip");
/************************/
/* END MISC THIRD-PARTY */
/************************/

/* var declaration for typescript - not needed if not using .ts */
declare var globals:Globals;
window["globals"] = require("./globals");

Layout.html (loaded on every page)

.....
<script src="/dist/scripts/site.bundle.js"></script>
.....

webpack.config.js

var path = require('path');
var resolve = path.resolve;
var AssetsPlugin = require('assets-webpack-plugin');
var WebpackCleanupPlugin = require("webpack-cleanup-plugin");
'use strict';

var babelOptions = {
    "presets": [
      [
        "es2015",
        {
            "modules": false
        }
      ],
      "es2016"
    ]
};

module.exports = [{
    cache: true,
    context: resolve('Scripts'),
    devtool: "source-map",
    entry: {
        site: './site.ts',
    },
    output: {
        path: path.resolve(__dirname, './dist/scripts'),
        filename: '[name].bundle.js',
    },
    module: {
        rules: [{
            test: /\.ts$/,
            exclude: /node_modules/,
            use: [
              {
                  loader: 'babel-loader',
                  options: babelOptions
              },
              {
                  loader: 'ts-loader'
              }
            ]
        }, {
            test: /\.js$/,
            exclude: /node_modules/,
            use: [
              {
                  loader: 'babel-loader',
                  options: babelOptions
              }
            ]
        }]
    },
    plugins: [
        new AssetsPlugin({ path: path.resolve(__dirname, './dist/assets') }),
        new WebpackCleanupPlugin({})
    ],
}];
Less answered 23/6, 2017 at 15:36 Comment(10)
Thanks for the answer and +1 cause this totally works. However, I don't know all the modules that I will need to require so I need to do it dynamically when webpack is run. There are too many constantly being added for me to use the window['module'] = require('module.js') trick unfortunately.Laurent
@Lofus If you are constantly adding things to your global scope, you are probably doing something wrong.. Add only what is truly global to your global bundle, and then use different entry points for the different modules in your application. Your global scope should not know about the individual things each page in your application needs. Each file should know what it and only it needs and should load its dependencies itself. Webpack will de-dupe any duplicate requires, so require away.Less
I realize it is not ideal but there are so many people on the team that the js/ folder is ever expanding every hour and I really don't want to inform literally everyone on how to update the webpack to accept these additionsLaurent
Actually, you did just give me an idea. On the entry script, make a function that loops through the files in a directory and requires them all by some regexp.Laurent
@Lofus I'm not sure how you would know what to name them on the window object, unless you maybe match the filename? But I suppose it could work.Less
@Lofus I think you might be able to accomplish what you're talking about by using On Demand Code Splitting with webpack. I personally have not experimented with it, but I have read a few articles by the webpack team about it. Basically, you can lazy-load .js files as you need them. So (I think) this would allow you to import all your js files into a global file, but each .js file wouldn't get loaded until it's used. So for the parts of your app that don't use certain files, they would never get loaded... I think hahaLess
Matching the filename or just using import would work fine. I really wish I could give you the green check but I kinda want to write the function up so the future googlers won't pull their hair. Also using import is basically On Demand Code Splitting so you're not too far off :)Laurent
@Lofus Well your question pretty explicitly states "How to make JavaScript global when bundled with webpack" and my answer addresses that exact question. That may not be your intent of the question, but the way it is worded now is a valid question that would be useful to future users (even more than your specific use-case).Less
Can't argue with that :)Laurent
You're actually a lifesaver @Less ! Trying to improve an old codebase at work and your solution works perfectly.Soliz
L
4

Note: This is not the ideal scenario but because I have a constant amount of new globals being added, I needed to make a plugin to bundle my javascript for me.

webpack-raw-bundler

This simply stacks your code together to be included on the frontend. Here is my usage example:

Usage

From the old:

<script src="js/jquery.js"></script>
<script src="js/silly.js"></script>
<script>
    $(silly()); // Some function in silly.js's global scope
</script>

To the new:

<script src="js/bundle.js"></script>
<script>
    $(silly()); // Some function in silly.js's global scope
</script>

Installing to the config

  var RawBundlerPlugin = require('webpack-raw-bundler');

  module.exports = {
    plugins: [
       new RawBundlerPlugin({
             excludedFilenames: [/angulartics/],
             readEncoding: "utf-8",
             includeFilePathComments: false,
             bundles: [ "vendor.js", "styles.css" ],
             "vendor.js": [
                'js/*.js'
             ],
             "styles.css": [
                'css/bootstrap.css',
                'css/edits.css'
             ]
       })
    ]
 }

A Fair Warning:

This should not be your go-to solution, but I had a bad case that made this the easiest option. Using expose-loader and import or window['module'] = require('module.js') is much safer as that is what webpack was built around. However, if you are having some headaches and just want a simple bundler, feel free to use this plugin.

Laurent answered 23/6, 2017 at 19:51 Comment(2)
Could minification and hashing be added?Overrule
I did this externally via another package to keep everything modularLaurent
C
2

It sounds like what the OP was looking for was the exports-loader instead of the the expose-loader.

To expose modules, use the expose-loader.

To expose global variables, use the exports-loader

This is one of those deals where the answer is clearly documented, but you had to be aware of the thing you're looking for first and know its name. It doesn't help matters that these two similar loaders were also given similar names.

Carlacarlee answered 31/12, 2019 at 15:40 Comment(0)
A
1

I've had the same problem and the best solution I've found is using webpack-concat-plugin.

What it does:

  • Concats everything to a single file
  • Allows me to specify the resulting file name, including [cache] template to do cache busting
  • Adds itself via HtmlWebpackPlugin to the resulting html

The only thing it doesn't do is not-leaking all the globals to the global scope.

Aliform answered 24/2, 2018 at 11:24 Comment(0)
S
0

If you are using webpack 2.x there is a built-in plugin

You define the global variable and then it can be accessed.

    plugins: [
        new webpack.ProvidePlugin({
            $: "jquery",
            jQuery: "jquery",
            "window.jQuery": "jquery",
            "window.Tether": 'tether',
            "Tether": 'tether'
        }),
        ...
    ]

here is my full config

    
    var webpack = require("webpack");
    var ExtractTextPlugin = require("extract-text-webpack-plugin");
    var path = require("path")


    module.exports = {
        entry: "./src/entry-js.js",
        devtool: 'source-map',
        output: {
            path: path.join(__dirname, "/public/dist/js"),
            publicPath: "/public/",
            filename: 'bundle.js',
            chunkFilename: 'chunk.[name].[id].js',
        },
        module: {
            rules: [
                {
                    test: /\.js$/,
                    loader: "babel-loader",
                    options: {
                        presets: ["es2015", "stage-0"]
                    },
                    exclude: [
                        path.resolve(__dirname, "node_modules")
                    ],
                },
                {
                    test: /\.css$/,
                    use: ExtractTextPlugin.extract({
                        fallback: "style-loader",
                        use: "css-loader"
                    })
                },
                {
                    test: /\.(scss|sass)$/,
                    use: ExtractTextPlugin.extract({
                        fallback: "style-loader",
                        use: [
                            "css-loader",
                            "sass-loader"
                        ]
                    })
                },
                {
                    test: /\.less$/,
                    use: ExtractTextPlugin.extract({
                        fallback: "style-loader",
                        use: [
                            "css-loader",
                            "less-loader"
                        ]
                    })
                },
                {
                    test: /\.(png|svg|jpg|gif)$/,
                    use: [{
                        loader:"file-loader",
                        options: {
                            limit: 500,
                            name: "../img/[name].[ext]"
                        }
                    }]
                },
                {
                    test: /\.(woff|woff2|eot|ttf|otf)$/,
                    use: [{
                        loader:"file-loader",
                        options: {
                            limit: 500,
                            name: "../fonts/[name].[ext]"
                        }
                    }]
                }
            ]
        },
        plugins: [
            new ExtractTextPlugin({
                filename: "../css/bundle.css",
                disable: false,
                allChunks: true
            }),
            new webpack.ProvidePlugin({
                $: "jquery",
                jQuery: "jquery",
                "window.jQuery": "jquery",
                "window.Tether": 'tether',
                "Tether": 'tether'
            })
        ]
    };

Here is my entry file

/********************
 *   CSS Libraries  *
 ********************/

// normalize v7
import "../node_modules/normalize.css/normalize.css";
// bootstrap v4.alpha-5
import "../node_modules/bootstrap/scss/bootstrap.scss";


/******************
 *   CSS Custom   *
 ******************/
import "./css/main.css";
import "./sass/main.scss";

/********************
 *   JS Libraries   *
 ********************/

//Jquery v3.2.1
import '../node_modules/jquery/src/jquery.js';
import Tether from 'tether';
//Bootstrap v4-alpha-5
import "../node_modules/bootstrap/dist/js/bootstrap.min.js";

import "./js/main.js";
Selection answered 23/6, 2017 at 16:41 Comment(1)
Yup I've tried ProvidePlugin. The issue is I do not know all of the globals that I need until webpack is called. I edited my post a bit to make this more apparent.Laurent
P
0

[2022] Do not use webpack (or similar tools) for bundling < script > tag links, which was common in older webs. Solving global variables manually in 50+ external libraries no way to go, and from personal experience in some cases its even impossible, there are also breaking issues when scripts contains 'require' and all other issues caused by webpack processing js code instead of just merging stuff together + minify.

Use uglifyJS (and or uglifyCSS)

npm install uglify-js -g

uglifyjs --compress --mangle --output bundle.js -- js/jquery.js js/silly.js

Then simply do as wanted. You can remove --compress or --mangle if do not wish extra minification

<script src="js/bundle.js"></script>
<script>
    $(silly()); // Some function in silly.js's global scope
</script>

https://github.com/mishoo/UglifyJS

For this specific cause, You can be in situation having numerous js files in single folder, this could be usefull to merge them all in single command https://github.com/ionutvmi/uglifyjs-folder

Procedure answered 4/11, 2022 at 11:50 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.