Skip to content

Build System: Bundles and Tabs

Lee Yi edited this page Sep 12, 2023 · 3 revisions

Why Bundle?

To run module code, js-slang would have to have all the dependencies of every single module, which would make building it tedious and bloated and also introduce an undesirable dependency between modules and js-slang. Instead, Source Modules are bundled before use.
Bundling refers to the process of combining all of a module's dependencies into a single file. You can refer to other projects that require bundling for more information.
Dependencies available at runtime aren't bundled and are handled differently (refer to later sections for more information)

Bundlers

Currently, there are several bundlers available such as RollupJS, Babel and Webpack. These bundlers trade speed for high configurability, giving the user a wide range of configuration options and plugins to customize the bundling process. Most of these options are unnecessary for bundling source modules.
esbuild is a Javascript bundler that trades configurability for speed. It is magnitudes faster than most other bundlers and suits the modules repository just fine. We use it to transpile module code from Typescript to Javascript and perform bundling.

How Source Modules are Bundled

Each Source module bundle is passed through esbuild, which converts it into an IIFE. Here is the curve module passed through esbuild:

// All of the code within src/bundles/curve is combined into a single file
// build/bundles/curve.js
var globalName = (() => {
  // Code from mat4
  function create() { /* implemntation details */ }
  function clone(a) { /* implemntation details */ }
  function copy(out, a) { /* implemntation details */ }
  // ... and other implementation details

  
  // The module's exports are returned as a single object
  return {
    draw_connected_2d,
    make_point,
    // etc...
  }
})();

Bundles are transpiled with esbuild using the following option set:

bundle: true,
external: ['js-slang*'],
format: 'iife',
globalName: 'module',
define: JSON.stringify({
  process: {
    NODE_ENV: 'production',
  }
}),
loader: {
  '.ts': 'ts',
  '.tsx': 'tsx',
},
platform: 'browser',
target: 'es6',
write: false,

Options Explained:

bundle: true

Tell esbuild to bundle the code into a single file.

external

Because the frontend is built using React, it is unnecessary to bundle React with the code for tabs. Similarly, js-slang/context is an import provided by js-slang at runtime, so it is not bundled with the code for bundles.
If you have any dependencies that are provided at runtime, use this option to externalize it.

format: 'iife'

Tell esbuild to output the code as an IIFE.

globalName: 'module'

By default, esbuild's IIFE output doesn't return its exports:

(function() {
  var exports = {}
  exports.add_one = function(x) {
    return x + 1;
  }
})()

By specifying a globalName, the generated code instead becomes:

var module = (function() {
  var exports = {}
  exports.add_one = function(x) {
    return x + 1;
  }
  return exports;
})()

It is then possible to extract the inner IIFE and use it to retreive the exports.

define

Module code that requires constructs such as process.env which are unavailable in the browser environment will cause the Source program to crash.

The define option tells esbuild to replace instances with process.env with { NODE_ENV; 'production' }, making that environment variable available at runtime

loader

Tell esbuild how to load source files.

platform: 'browser, target: 'es6'

Tell esbuild that we are bundling for the browser, and that we need to compile code down to the ES6 standard, which is the Javascript standard targeted by Source.

write: false

write: false causes esbuild to its compiled code into memory instead of to disk, which is necessary to finish building the bundle or tab.

After Esbuild

After esbuild bundling, both bundles and tabs are parsed using acorn to produce an AST. Esbuild will produce an IIFE that looks like the following:

var module = (function() {
  var exports = {}
  exports.add_one = function(x) {
    return x + 1;
  }

  return exports;
})()

Bundles then get transformed to

require => {
  var exports = {}
  exports.add_one = function(x) {
    return x + 1;
  }

  return exports;
}

Tabs get transformed to:

require => () => {
  // tab code...
}()['default']

When bundles and tabs are loaded, the IIFE is called with a function that simulates the require() function in CommonJS to provide the dependencies marked as external (that have to be provided at runtime). External dependencies are not bundled with the tab or bundle code.

For bundles, the only packages provided at runtime (by js-slang) are:

  • js-slang/index
  • js-slang/dist/types
  • js-slang/dist/stdlib

For tabs, the packages provided at runtime (by the frontend are):

  • js-slang
  • js-slang/dist
  • react and react/jsx-runtime (Both are needed to maintain compatibility across JSX transforms)
  • react-dom
  • react-ace
  • @blueprintjs/core
  • @blueprintjs/icons
  • @blueprintjs/popover2
Clone this wiki locally