Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use iife for browser and cjs for node #470

Closed
bidoubiwa opened this issue Jun 22, 2020 · 1 comment · Fixed by #573
Closed

Use iife for browser and cjs for node #470

bidoubiwa opened this issue Jun 22, 2020 · 1 comment · Fixed by #573

Comments

@bidoubiwa
Copy link
Contributor

UMD bundle

In this issue I want to show you why the UMD is not working in its current state (466), why we will first go through a step where iife is used for browser and cjs for node, and later, a step where UMD works.

In the first step, we create a simple ES code that will be transpiled into UMD javascript. We follow the steps given here.

Base

First step, creation of ES script.
code

//index.js
export default 'Hello, world!';

Then, we create a rollup configuration to transpile the script in UMD.
rollup

    "build": "rollup index.js --file dist/bundle.js --format umd --name 'MyModuleName'"

UMD output:

(function (global, factory) {
	typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
	typeof define === 'function' && define.amd ? define(factory) :
	(global = global || self, global.MyModuleName = factory());
}(this, (function () { 'use strict';

	var index = 'Hello, world!';

	return index;

})));

[explanation on how UMD works](https://riptutorial.com/javascript/example/16339/universal-module-definition--umd-

Explaination on global and factory:

global could be global in Node or in the browser it could be window. It is passed by providing this. factory is the function that is after this. That is where the application code ("business logic" or "meat") is.
UMD should work in any JavaScript environment, it just adapts the logic for what ever module loading system is present.

Tried in the following applications:

build: ✅

node: ✅

node dist/bundle.js

browser : ✅

<!-- index.html -->
<script src="dist/bundle.js"></script>
<script>
  console.log(window.MyModuleName);
</script>

AMD: ✅

<!-- amd.html -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.3.6/require.min.js"></script>
<script>
  window.requirejs(['dist/bundle'], function(MyModuleName) {
    console.log(MyModuleName);
  });
</script>

Adding Axios

Now, we try to add axios to our ES script.

code

import axios from 'axios'

axios.get('https://google.com').then(res => {
    console.log(Object.keys(res));
    
})
export default 'Hello, world!';

This requires a more complete rollup configuration.
rollup:

// rollup.config.js
module.exports = [
    // browser-friendly UMD build
    {
      input: './index.js',
      output: {
        name: "MyModuleName", // module name
        file: "./dist/bundle.js", // output file
        format: 'umd', // format
        globals: {
          axios: 'axios', // accessible globals
        },
      },
    }
]

UMD output:
)

(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory(require('axios')) :
    typeof define === 'function' && define.amd ? define(['axios'], factory) :
    (global = global || self, global.MyModuleName = factory(global.axios));
}(this, (function (axios) { 'use strict';

    axios = axios && Object.prototype.hasOwnProperty.call(axios, 'default') ? axios['default'] : axios;

    axios.get('https://google.com').then(res => {
        console.log(Object.keys(res));
        
    });
    var index = 'Hello, world!';

    return index;

})));

build: ✅

node: ✅

node dist/bundle.js

browser : ❌

bundle.js:9 Uncaught TypeError: Cannot read property 'get' of undefined
    at bundle.js:9
    at bundle.js:4
    at bundle.js:5

used in axios.get. Meaning get is undefined.

AMD: ❌

require.min.js:1 GET file:///Users/charlottevermandel/rollup-umd/axios.js net::ERR_FILE_NOT_FOUND
require.min.js:1 Uncaught Error: Script error for "axios", needed by: dist/bundle
https://requirejs.org/docs/errors.html#scripterror
    at makeError (require.min.js:1)
    at HTMLScriptElement.onScriptError (require.min.js:1)

Cannot fetch axios. Try to import file that does not exist.

Solutions 1

Step 1 : node resolve plugin

Add @rollup/plugin-node-resolve: A Rollup plugin which locates modules using the Node resolution algorithm, for using third party modules in node_modules.
Rollup config:

// rollup.config.js
import resolve from '@rollup/plugin-node-resolve'

module.exports = [
    // browser-friendly UMD build
    {
      input: './index.js', // directory to transpilation of typescript
      output: {
        name: "MyModuleName",
        file: "./dist/bundle.js",
        format: 'umd',
        globals: {
          axios: 'axios',
        },
      },
      plugins: [
          resolve(),
      ]
    }
]

Build error

[!] Error: 'default' is not exported by node_modules/axios/index.js, imported by index.js
https://rollupjs.org/guide/en/#error-name-is-not-exported-by-module

Explaination for the above link:

Import declarations must have corresponding export declarations in the imported module. For example, if you have import a from './a.js' in a module, and a.js doesn't have an export default declaration, or import {foo} from './b.js', and b.js doesn't export foo, Rollup cannot bundle the code.

This error frequently occurs with CommonJS modules converted by @rollup/plugin-commonjs, which makes a reasonable attempt to generate named exports from the CommonJS code but won't always succeed, because the freewheeling nature of CommonJS is at odds with the rigorous approach we benefit from in JavaScript modules. It can be solved by using the namedExports option, which allows you to manually fill in the information gaps.

Apparently namedExports is not necessary anymore.

The namedExports option has been removed, now requires rollup >= 2.3.4. Instead of manually defining named exports, rollup now handles this automatically for you.

Step 2 : commonjs plugin

Add @rollup/plugin-commonjs: 🍣 A Rollup plugin to convert CommonJS modules to ES6, so they can be included in a Rollup bundle.

import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs';


module.exports = [
    // browser-friendly UMD build
    {
      input: './index.js', // directory to transpilation of typescript
      output: {
        name: "MyModuleName",
        file: "./dist/bundle.js",
        format: 'umd',
        globals: {
          axios: 'axios',
        },
      },
      plugins: [
          resolve(),
          commonjs()
      ]
    }
]

Build error

./index.js → ./dist/bundle.js...
[!] Error: Unexpected token (Note that you need @rollup/plugin-json to import JSON files)
node_modules/axios/package.json (2:8)

Issues mentionned:

To make rollup understand json files, just use rollup-plugin-json. I've just tried and it's working fine.

Step 3 : json plugin

Add @rollup/plugin-json: A Rollup plugin which Converts .json files to ES6 modules.

// rollup.config.js
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json'


module.exports = [
    // browser-friendly UMD build
    {
      input: './index.js', // directory to transpilation of typescript
      output: {
        name: "MyModuleName",
        file: "./dist/bundle.js",
        format: 'umd',
        globals: {
          axios: 'axios',
        },
      },
      plugins: [
          json(),
          resolve(),
          commonjs()
      ]
    }
]

build: ✅
Warning:

(!) Missing shims for Node.js built-ins
Creating a browser bundle that depends on 'http', 'https', 'url', 'assert', 'stream', 'tty', 'util' and 'zlib'. You might need to include https://github.com/ionic-team/rollup-plugin-node-polyfills
(!) Unresolved dependencies
https://rollupjs.org/guide/en/#warning-treating-module-as-external-dependency
http (imported by node_modules/axios/lib/adapters/http.js, node_modules/follow-redirects/index.js, http?commonjs-external)
https (imported by node_modules/axios/lib/adapters/http.js, node_modules/follow-redirects/index.js, https?commonjs-external)
url (imported by node_modules/axios/lib/adapters/http.js, node_modules/follow-redirects/index.js, url?commonjs-external)
zlib (imported by node_modules/axios/lib/adapters/http.js, zlib?commonjs-external)
stream (imported by node_modules/follow-redirects/index.js, stream?commonjs-external)
assert (imported by node_modules/follow-redirects/index.js, assert?commonjs-external)
supports-color (imported by node_modules/debug/src/node.js, supports-color?commonjs-external)
tty (imported by node_modules/debug/src/node.js, tty?commonjs-external)
util (imported by node_modules/debug/src/node.js, util?commonjs-external)
(!) Missing global variable names
Use output.globals to specify browser global variable names corresponding to external modules
http (guessing 'http')
https (guessing 'https')
url (guessing 'url')
assert (guessing 'assert')
stream (guessing 'stream')
tty (guessing 'tty')
util (guessing 'util')
supports-color (guessing 'supportsColor')
zlib (guessing 'zlib')

output: huge file

node: ❌

internal/modules/cjs/loader.js:1017
  throw err;
  ^

Error: Cannot find module 'supports-color'
Require stack:
- /Users/charlottevermandel/rollup-umd/dist/bundle.js
    at Function.Module._resolveFilename (internal/modules/cjs/loader.js:1014:15)
    at Function.Module._load (internal/modules/cjs/loader.js:884:27)
    at Module.require (internal/modules/cjs/loader.js:1074:19)
    at require (internal/modules/cjs/helpers.js:72:18)
    at /Users/charlottevermandel/rollup-umd/dist/bundle.js:2:213
    at Object.<anonymous> (/Users/charlottevermandel/rollup-umd/dist/bundle.js:5:2)
    at Module._compile (internal/modules/cjs/loader.js:1185:30)
    at Object.Module._extensions..js (internal/modules/cjs/loader.js:1205:10)
    at Module.load (internal/modules/cjs/loader.js:1034:32)
    at Function.Module._load (internal/modules/cjs/loader.js:923:14) {
  code: 'MODULE_NOT_FOUND',
  requireStack: [ '/Users/charlottevermandel/rollup-umd/dist/bundle.js' ]
}

browser: ❌

Uncaught ReferenceError: process is not defined
    at bundle.js:1616
    at createCommonjsModule (bundle.js:978)
    at bundle.js:1567
    at bundle.js:4
    at bundle.js:5

amd: ❌

require.min.js:1 GET file:///Users/charlottevermandel/rollup-umd/http.js net::ERR_FILE_NOT_FOUND
require.min.js:1 GET file:///Users/charlottevermandel/rollup-umd/https.js net::ERR_FILE_NOT_FOUND
require.min.js:1 GET file:///Users/charlottevermandel/rollup-umd/url.js net::ERR_FILE_NOT_FOUND
require.min.js:1 GET file:///Users/charlottevermandel/rollup-umd/assert.js net::ERR_FILE_NOT_FOUND
require.min.js:1 GET file:///Users/charlottevermandel/rollup-umd/stream.js net::ERR_FILE_NOT_FOUND
require.min.js:1 GET file:///Users/charlottevermandel/rollup-umd/tty.js net::ERR_FILE_NOT_FOUND
require.min.js:1 GET file:///Users/charlottevermandel/rollup-umd/util.js net::ERR_FILE_NOT_FOUND
require.min.js:1 GET file:///Users/charlottevermandel/rollup-umd/supports-color.js net::ERR_FILE_NOT_FOUND
require.min.js:1 GET file:///Users/charlottevermandel/rollup-umd/zlib.js net::ERR_FILE_NOT_FOUND
require.min.js:1 Uncaught Error: Script error for "http", needed by: dist/bundle
https://requirejs.org/docs/errors.html#scripterror
    at makeError (require.min.js:1)
    at HTMLScriptElement.onScriptError (require.min.js:1)
require.min.js:1 Uncaught Error: Script error for "https", needed by: dist/bundle
https://requirejs.org/docs/errors.html#scripterror
    at makeError (require.min.js:1)
    at HTMLScriptElement.onScriptError (require.min.js:1)
require.min.js:1 Uncaught Error: Script error for "url", needed by: dist/bundle
https://requirejs.org/docs/errors.html#scripterror
    at makeError (require.min.js:1)
    at HTMLScriptElement.onScriptError (require.min.js:1)
require.min.js:1 Uncaught Error: Script error for "assert", needed by: dist/bundle
https://requirejs.org/docs/errors.html#scripterror
    at makeError (require.min.js:1)
    at HTMLScriptElement.onScriptError (require.min.js:1)
require.min.js:1 Uncaught Error: Script error for "stream", needed by: dist/bundle
https://requirejs.org/docs/errors.html#scripterror
    at makeError (require.min.js:1)
    at HTMLScriptElement.onScriptError (require.min.js:1)
require.min.js:1 Uncaught Error: Script error for "tty", needed by: dist/bundle
https://requirejs.org/docs/errors.html#scripterror
    at makeError (require.min.js:1)
    at HTMLScriptElement.onScriptError (require.min.js:1)
require.min.js:1 Uncaught Error: Script error for "util", needed by: dist/bundle
https://requirejs.org/docs/errors.html#scripterror
    at makeError (require.min.js:1)
    at HTMLScriptElement.onScriptError (require.min.js:1)
require.min.js:1 Uncaught Error: Script error for "supports-color", needed by: dist/bundle
https://requirejs.org/docs/errors.html#scripterror
    at makeError (require.min.js:1)
    at HTMLScriptElement.onScriptError (require.min.js:1)
require.min.js:1 Uncaught Error: Script error for "zlib", needed by: dist/bundle
https://requirejs.org/docs/errors.html#scripterror
    at makeError (require.min.js:1)
    at HTMLScriptElement.onScriptError (require.min.js:1)

Step 4: add resolve options

Following the first solution that was suggested here.
We add the options to the node-resolve plugin.

mainFields: ['jsnext', 'main']

Specifies the properties to scan within a package.json, used to determine the bundle entry point. The order of property names is significant, as the first-found property is used as the resolved entry point. If the array contains 'browser', key/values specified in the package.json browser property will be used.

browser

If true, instructs the plugin to use the "browser" property in package.json files to specify alternative files to load for bundling. This is useful when bundling for a browser environment. Alternatively, a value of 'browser' can be added to the mainFields option. If false, any "browser" properties in package files will be ignored. This option takes precedence over mainFields.

config:

import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json'


module.exports = [
    // browser-friendly UMD build
    {
      input: './index.js', // directory to transpilation of typescript
      output: {
        name: "MyModuleName",
        file: "./dist/bundle.js",
        format: 'umd',
        globals: {
          axios: 'axios',
        },
      },
      plugins: [
          json(),
          resolve({
            browser: true,
            mainFields: ['jsnext', 'main']
          }),
          commonjs()
      ]
    }
]

build: ✅
output UMD: huge file

node: ❌

(node:40087) UnhandledPromiseRejectionWarning: ReferenceError: XMLHttpRequest is not defined
    at dispatchXhrRequest (/Users/charlottevermandel/rollup-umd/dist/bundle.js:799:21)
    at new Promise (<anonymous>)
    at xhrAdapter (/Users/charlottevermandel/rollup-umd/dist/bundle.js:791:12)
    at dispatchRequest (/Users/charlottevermandel/rollup-umd/dist/bundle.js:1098:12)
(Use `node --trace-warnings ...` to show where the warning was created)
(node:40087) UnhandledPromiseRejectionWarning: Unhandled promise rejection. This error originated either by throwing inside of an async function without a catch block, or by rejecting a promise which was not handled with .catch(). To terminate the node process on unhandled promise rejection, use the CLI flag `--unhandled-rejections=strict` (see https://nodejs.org/api/cli.html#cli_unhandled_rejections_mode). (rejection id: 3)
(node:40087) [DEP0018] DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

Browser: ✅

AMD: ✅

Following THIS stackoverflow post.

To make your package as small as possible I'd recommend using the Fetch API. A UMD library has the three types of consumers where fetch comes in handy;
Node.js - has not implemented but can use node-fetch to polyfill common behaviour using only node libraries (no heavy dependencies like superagent, unirest and axios etc - these add security concerns as well as bloat!).
Browser - Fetch is a WHATWG standard and is available in most modern browsers but might require an npm package such as whatwg-fetch to polyfill older browsers
Isomorphic/Universal - the same javascript running in browser and node.js which you find in progressive web apps.They need to use a library called isomorphic-fetch to load either whatwg-fetch or the node.js version of fetch.
It should be handled by the projects consumer though so README should include instructions to each of the three types of users above.
Node.js and isomorphic instructions are basically as below.

Possible solutions to this problem:

  1. Create two distinct rollup output, one for cjs that will be main in the package.json and one that will be iife that will be in the key browser of the package.json.
    If we opt for this solution users will have to :
<!-- index.html -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/meilisearch.iife.js"></script>
<script>
  console.log(window.MyModuleName);
</script>

instead of

<!-- index.html -->
<script src="https://cdn.jsdelivr.net/npm/[email protected]"></script>
<script>
  console.log(window.MyModuleName);
</script>

This file will be a little heavy as it is bundled with all its dependencies.

  1. Remove axios and use fetch as it seems that it makes the umd possible.

Conclusion

Fast solution

Replace package.json main key with cjs bundle and change browser key with iife bundle.

"main": "./dist/bundles/meilisearch.cjs.js",
"module": "./dist/bundles/meilisearch.esm.js",
"browser": "./dist/bundles/meilisearch.iife.js",

Change umd rollup config with the following:

  {
    input: 'src/meilisearch.ts', // directory to transpilation of typescript
    output: {
      name: LIB_NAME,
      file: getOutputFileName(
        // will add .min. in filename if in production env
        resolve(ROOT, pkg.browser),
        env === 'production'
      ),
      format: 'iife',
      sourcemap: env === 'production', // create sourcemap for error reporting in production mode
      globals: {
        axios: 'axios',
      },
    },
    plugins: [
      ...PLUGINS,
      nodeResolve({
        mainFields: ['jsnext', 'main'],
        preferBuiltins: true,
        browser: true
      }),
      commonjs({
        include: 'node_modules/axios/**',
      }),
      json(),
      env === 'production' ? terser() : {}, // will minify the file in production mode
    ],
  }

Best solution:

Remove axios and replace with fetch-polyfill and rollup-plugin-node-polyfills
This will resolve the following issues:

  • create UMD that works
  • Remove the whole error stack loss

Other related issues and stackoverflow posts:

Problem 2: meilisearch naming

To change the name it is done here:

const LIB_NAME = pascalCase(normalizePackageName(pkg.name))

the pascalCase transforms meilisearch into Meilisearch. The pascalCase does not work as intented as the pkg.name is retrieved from package.json and is meilisearch. I suggest we change it like this:

const LIB_NAME = "MeiliSearch"

as this will impact this the umd or soon to be iife bundle.

See name key in output key.

module.exports = [
  // browser-friendly UMD build
  {
    input: 'src/meilisearch.ts', // directory to transpilation of typescript
    output: {
      name: LIB_NAME,
      format: 'iife',
      ...
    },
    plugins: [
    ...
    ],
  },
@advename
Copy link

Sorry for hijacking your issue, but what a beautiful and extensive research you've done here @bidoubiwa, very detailed!

One thing i want to add is that the Rollup setup you initially had, just when you added the axios depdency and AMD and Browser failed, is suitable as well.

As Webpack writes on their website with an library example that uses the loadash dependency:

Now, if you run webpack, you will find that a largish bundle is created. If you inspect the file, you'll see that lodash has been bundled along with your code. In this case, we'd prefer to treat lodash as a peer dependency. Meaning that the consumer should already have lodash installed. Hence you would want to give up control of this external library to the consumer of your library.

Bootstrap 4 has done the same thing for years
, e.g. that they required the consumer to add jQuery and Popper dependencies too:

<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" ></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>

In my opinion, you can now start arguing when it makes sense to use IIFE which has all dependencies included, and when some other library is small enough, that it may only require 2-3 additional dependencies to run in the browser, to instruct the consumer to add these themself :)

Again, big up for your work and thanks a lot!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants