mastilver/dynamic-cdn-webpack-plugin

[Bug] Module isn't auto loaded from CDN URL at runtime. Is it meant to be?

tSchubes opened this issue · 1 comments

Is this a bug report?

Yes, if the intended behaviour of this plugin is to facilitate loading external modules from CDN URL's automatically at runtime. Is it?

Environment

dynamic-cdn-webpack-plugin@4.0.0
webpack@4.30.0

Steps to Reproduce

  1. Create a simple hello world app which consumes an external module

    // src/app.js
    var _ = require('lodash');
    
    var foo = ['foo', 'foo', 'foo'];
    var foobar = _.map(foo, function(item) {
        return item + 'bar';
    });
    var foobarString = JSON.stringify(foobar, null, 2);
    
    var el = document.createElement('pre');
    el.innerHTML = foobarString;
    document.body.appendChild(el);
    console.log(foobarString);
    <!-- index.html -->
    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="UTF-8">
            <title>Foobar</title>
        </head>
        <body>
            <h1>Foobar</h1>
            <script src="build/app.js"></script>
        </body>
    </html>
  2. Configure plugin to load an external module from CDN using a custom resolver

    const path = require('path');
    const DynamicCdnWebpackPlugin = require('dynamic-cdn-webpack-plugin');
    const ManifestPlugin = require('webpack-manifest-plugin');
    
    module.exports = {
    
        entry: {
            app: path.resolve(__dirname, 'src/app.js')
        },
    
        output: {
            path: path.resolve(__dirname, './build'),
        },
    
        devtool: false,
    
        plugins: [
            new ManifestPlugin({
                fileName: 'webpack-manifest.json'
            }),
            new DynamicCdnWebpackPlugin({
                only: ['lodash'],
                verbose: true,
                resolver: (packageName, version, options) => {
                    if (packageName == 'lodash') {
                        return {
                            name: packageName,
                            url: 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js',
                            version: version,
                            var: '_'
                        };
                    }
                    return null;
                }
            })
        ]
    
    };
  3. Build & run
    webpack --mode=development

Expected Behavior

The external library (lodash) would be automatically loaded from the configured CDN url at runtime as "_"

Actual Behavior

The external library was not loaded automatically which resulted in a runtime error:
Uncaught ReferenceError: _ is not defined

Note that the CDN URL is referenced in the manifest JSON but not in the compiled js bundle and we see the following line in the console output for the build:
✔️ 'lodash' will be served by https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.1.0/lodash.js

Reproducible Demo

Fiddle of the compilation result

Source Repo

Thanks in advance

Hi,

TLDR
You can fix this by using the HtmlWebpackPlugin - It can read through the asset manifest and generate all the script tags for the external dependencies.

The longer version

This is essentially how the plugin works -

  • For every dependency, check if the resolver function returns the configuration object.
  • If object is returned, it marks this dependency as an ExternalModule to webpack.
  • Once marked as external, webpack does not bundle the dependency at all and assumes it will be available through the global window object.

You can read more about externals in the official webpack documentation.

Even if the asset manifest contains the CDN url, webpack itself doesn't do anything with it.

What is then left to do is to add the CDN url ourselves.

In your current index.html,

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Foobar</title>
    </head>
    <body>
        <h1>Foobar</h1>
+       <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"></script>
        <script src="build/app.js"></script>
    </body>
</html>

Now your application would work as expected. But we end up repeating the URL in two places which is less than ideal.

There are two paths we can take from here.

Use externals configuration

If you are really keen on not having the index.html generated as part of your webpack build then you can drop this plugin and tell webpack directly that lodash is external and available through window._.

const path = require('path');

module.exports = {
    entry: {
        app: path.resolve(__dirname, 'src/app.js')
    },
    output: {
        path: path.resolve(__dirname, './build'),
    },
    devtool: false,
+    externals: {
+       lodash: '_'
+    },
-    plugins: [
-        new ManifestPlugin({
-            fileName: 'webpack-manifest.json'
-        }),
-        new DynamicCdnWebpackPlugin({
-           only: ['lodash'],
-            verbose: true,
-           resolver: (packageName, version, options) => {
-                if (packageName == 'lodash') {
-                    return {
-                        name: packageName,
-                        //url: '../node_modules/lodash.js',
-                       url: 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js',
-                        version: version,
-                        var: '_'
-                    };
-                }
-                return null;
-            }
-        })
-    ]
};

Use HtmlWebpackPlugin

HtmlWebpackPlugin is a plugin that simplifies creation of HTML files to serve webpack bundles. You can use it to generate the final index.html instead of doing it manually. In fact, you can also supply your own index.html template for it to use.

In your index.html,

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <title>Foobar</title>
    </head>
    <body>
        <h1>Foobar</h1>
-       <script src="build/app.js"></script>
    </body>
</html>

In your webpack.config.js,

const path = require('path');
+ const HtmlWebpackPlugin = require('html-webpack-plugin');
const DynamicCdnWebpackPlugin = require('dynamic-cdn-webpack-plugin');
const ManifestPlugin = require('webpack-manifest-plugin');

module.exports = {

    entry: {
        app: path.resolve(__dirname, 'src/app.js')
    },

    output: {
        path: path.resolve(__dirname, './build'),
    },

    devtool: false,

    plugins: [
+       new HtmlWebpackPlugin({ template: "./index.html" }),
        new ManifestPlugin({
            fileName: 'webpack-manifest.json'
        }),
        new DynamicCdnWebpackPlugin({
            only: ['lodash'],
            verbose: true,
            resolver: (packageName, version, options) => {
                if (packageName == 'lodash') {
                    return {
                        name: packageName,
                        //url: '../node_modules/lodash.js',
                        url: 'https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js',
                        version: version,
                        var: '_'
                    };
                }
                return null;
            }
        })
    ]
};

Note the template option to HtmlWebpackPlugin which points to your index.html.

The newly generated build/index.html,

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <title>Foobar</title>
</head>

<body>
  <h1>Foobar</h1>
  <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.js"></script>
  <script type="text/javascript" src="app.js"></script>
</body>

</html>

Conclusion

The app behaves the same in both approaches. Currently, the first approach is simpler and requires way less config to achieve but if your application ends up using multiple dependencies which can be loaded through a CDN then it is beneficial to use the second approach since the plugin manages adding the appropriate script tags.