Search code examples
javascriptmodulepurescript

How to load BigInteger.min.js into browser so that Data.BigInt finds it?


I have the following PureScript program that prints a BigInt to the log:

module Main where

import Prelude
import Effect
import Effect.Console

import Data.Integral (fromIntegral)
import Data.BigInt

main :: Effect Unit
main = log $ show (fromIntegral 0x42 :: BigInt)

I would like to use it from the browser. So I made the following HTML file:

<!doctype html>
<html>
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>

<body>
  <script src="index.js" type="module"></script>
</body>
</html>

The contents of index.js is:

import * as Main from '../output/Main/index';

console.log("Before");
Main.main();
console.log("After");

This fails because Data.BigInt uses FFI to a Javascript module called big-integer:

Uncaught TypeError: Failed to resolve module specifier "big-integer". Relative references must start with either "/", "./", or "../".

How do I load BigInteger.min.js into the "big-integer" module specifier?

What I've tried so far

I tried loading BigInteger.min.js by importing it manually in index.js:

import * as Main from '../output/Main/index';
import * as BigInteger from './BigInteger.min.js';

console.log("Before");
Main.main();
console.log("After");

But the problem here is that I can't import it as big-integer, only BigInteger, because the former is not lexically valid.

I also tried an unqualified import:

import {bigInt} from './BigInteger.min.js';

but that doesn't work either (same error about missing "big-integer" module).

I've also tired loading BigInteger.minjs as a JS source file directly:

<body>
  <script src="BigInteger.min.js"></script>
  <script src="index.js" type="module"></script>
</body>

But still, same problem.

I've also tried adding an import map to my HTML file:

<head>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
  <script type="importmap">
    {
        "imports": {
            "big-integer": "./BigInteger.min.js"
        }
    }
</script>
</head>

This then fails with:

Uncaught SyntaxError: The requested module 'big-integer' does not provide an export named 'default' (at foreign.js:3:8)


Solution

  • I'm not sure where that BigInteger.min.js file came from, but based on its name and the way you're trying to use it, I'm going to assume that it's a package specially prepared to be a "universal module" - i.e. a module that can load in either Node, or under UMD/AMD loader, or in the browser. Perhaps this is the link you got it from?

    The way this package works in the browser is old-school: it creates a global variable named bigInt, which can be then accessed by whoever wants to use it. To check this, try loading your page, opening Dev Tools -> Console, and typing window.bigInt. Is it defined?

    The problem is, most modern tools, including PureScript, don't work with that kind of interface anymore. Instead, everybody moved to ES modules, where there are no global variables of any sort, and every module can import other modules by name. Which is kind of what you tried to do by import {bigInt} from './BigInteger.min.js', and it failed because BigInteger.min.js is not an ES module.

    As to how to solve this - you have options.


    Option 0: (false start) install the Node module

    Normally in a situation like this, one would install the module in Node with npm install big-integer, and then reference it from there:

    <script type="importmap">
      {
        "imports": {
          "big-integer": "../node_modules/big-integer/index.js"
        }
      }
    </script>
    

    However, the problem with that particular package is that, even when installed in Node, it's not an ES module. The package is written as a non-ES module from the start, so it can never be imported as ES.

    In Node this would work fine, because Node can work with both ES and CommonJS modules combined, so it would just load the big-integer package old-style and run with it. Sure, it's a bit less efficient, but it works.

    But not in the browser. Modern browsers, when faced with an import ... from ... construct, expect the target to be an ES module, and if it's not - that's that.


    Option 1: a stub

    The package is available as a global variable in your browser, the only problem is that the ES import system can't access it like that. So we can help it out a bit. Create a new module, say big-int-stub.js, and export the global variable from it:

    // big-int-stub.js
    export default window.bigInt
    

    Then use importmap to remap big-integer to that stub:

    <script type="importmap">
      {
        "imports": {
          "big-integer": "./big-int-stub.js"
        }
      }
    </script>
    <script src="./BigInteger.min.js"></script>
    
    <script type="module">
      import bigInt from "big-integer"
      console.log(bigInt)
    </script>
    

    One problem with this is that, if you want to be able to import stuff other than the default export, e.g. import { min, max } from "big-integer", you'd have to enumerate every one of them in the export too:

    export default window.bigInt
    export const min = window.bigInt.min
    export const max = window.bigInt.max
    ...
    

    Another problem - not with the package itself, but more generally - is that you're asking the browser to load a lot of small files with your page: first index.js, then from there - output/Main/index.js, then everything it imports, then everything those imports import, and so on. This is ok for toy projects and exercises, but for any real application this is more likely to be unacceptable than not.


    Option 2: bundling

    Instead of including index.js directly in your page, first bundle it, then include the bundle.

    The idea of "bundling" is to take your "root" module, then take everything it imports, and everything those imports import, and so on, and so on, - and then combine it all in one big JS file that doesn't import anything at all, but is completely self-contained, has all the code that it needs to run. Such combined file is called a "bundle". And a program that performs this combining process - a "bundler".

    The advantages of this approach are numerous: not only does this mean that the browser only needs to load one file instead of dozens, but a good bundler (and especially with ES modules) will be able to include not all the code, but only those functions and variables that may actually be referenced. This is called "dead code elimination", or "tree shaking" if you want to sound cool 😎.

    But the key advantage for your particular situation is that a bundler also recognizes CommonJS modules, which means it can incorporate the big-integer package into the bundle, even though it's not an ES module.

    First, install yourself a bundler. There are many different ones out there - the most historically popular is WebPack, but I personally (as well as the PureScript community) like esbuild best. It's orders of magnitude faster and its API is actually sane. To install it, use npm:

    npm install esbuild
    

    After that, just give it your index.js and ask it to bundle. It's also usually a good idea to specify the output file name:

    esbuild --bundle ./index.js --outfile=./bundle.js
    

    This will produce the bundle.js file, which you can include in the page:

    <script src="bundle.js" type="module"></script>