Skip to content

Latest commit

 

History

History
215 lines (173 loc) · 7.66 KB

how-it-works.mdx

File metadata and controls

215 lines (173 loc) · 7.66 KB

How in browser bundler works?

The core of the in-browser bundler is esbuild-wasm. We also implemented a custom plugin to resolve CSS files, relative files etc.

You can checkout the source code at packages/src/bundle.ts.

Resolve & transpile files

Each playground file is virtually represented by the following interface:

export interface PlaygroundInputFile {
  filename: string;
  code: string;
}

Bundling these files is starting from the entrypoint file, which is the first one if there are multiple files. An entrypoint file for a playground is a jsx/tsx module that has a default export and the default export is a React function component.

During bundling, esbuild will traverse the module tree and resolve all the import paths.

  • relative paths: if a file starts with ./, we believe it is a virtual relative file that can be found in the inputs. These files will be transpiled and concatenated into the final bundling result.
  • external identifiers: all other imports are considered as external dependencies. Eg., Material UI & React, etc. These imports will leave as is in the final bundling result as require() calls.

esbuild can transpile common React related modules by default, including jsx, tsx, json etc. For CSS, we need to have some special handling, otherwise the styles will pollute the components outside of preview.

Scoped, global CSS and CSS Modules

In Code Kitchen, we allow three types of CSS files for playground. By default, rules in a CSS file is scoped to the current preview. We also enabled Global and CSS Modules support with .global.css and .module.css extensions.

For all of the modes, we will use a style-loader approach that injects the transpiled CSS to the DOM.

For scoped & CSS Modules, we also relied on stylis to transpile CSS along with esbuild, which is a CSS preprocessor that we can hack into the intermediate CSS AST for rules and declarations.

Global CSS

Rules with global.css will be simply applied globally even outside of the preview.

Scoped CSS

Rules in a normal css without special extension will be wrapped with a random class name, and the same class name will be used to wrap the playground preview in a div element.

CSS Module

CSS file ends with module.css will be treated as CSS modules. @evanw said he wants to use "CSS module bundling part of esbuild MVP", but it did not happen yet. We attempted to use PostCSS to transpile CSS Module as well, but it seems it is not supported to be run in the browser. As a result, we implement a sub-set of CSS module on our own with stylis.

Typically, what we need to do for a module.css file:

  • add a prefix to every class name in the CSS
  • transpile the CSS into a JS object that is a mapping from original class name to the actual prefixed class name

This turns out to be straightforward with stylis:

function compileCssModule(css: string, buildId: string) {
  const classMapping = {};
  const res = serialize(
    compile(css),
    middleware([
      (element) => {
        if (element.length > -1) {
          if (element.type === RULESET && element.props) {
            element.props = (
              Array.isArray(element.props)
                ? [...element.props]
                : [element.props]
            ).map((prop) => {
              return prop.replaceAll(/\.-?[_a-zA-Z]+[_a-zA-Z0-9-]*/g, (m) => {
                const varName = m.slice(1);
                if (!classMapping[varName]) {
                  classMapping[varName] = varName + "_" + buildId;
                }
                return "." + classMapping[varName];
              });
            });
          }
        }
      },
      stringify,
    ])
  );

  return {
    contents: `${injectCSS(res, buildId)}
    export default ${JSON.stringify(classMapping)}`,
  };
}

Eval and Rendering the Preview

Now that all playground are bundled in a single large string, which is a common js module that its default export is the latest demo React function component. E.g., the following input playground file

```js
import { Button } from "my-private-button-lib";
import "./styles.css";

export default function Demo() {
  return <Button>Button</Button>;
}
```

```css styles.css
button {
  width: 200px;
}
```

Will be bundled as

var __create = Object.create;
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __getProtoOf = Object.getPrototypeOf;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __markAsModule = (target) =>
  __defProp(target, "__esModule", { value: true });
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __reExport = (target, module2, copyDefault, desc) => {
  if (
    (module2 && typeof module2 === "object") ||
    typeof module2 === "function"
  ) {
    for (let key of __getOwnPropNames(module2))
      if (!__hasOwnProp.call(target, key) && (copyDefault || key !== "default"))
        __defProp(target, key, {
          get: () => module2[key],
          enumerable:
            !(desc = __getOwnPropDesc(module2, key)) || desc.enumerable,
        });
  }
  return target;
};
var __toESM = (module2, isNodeMode) => {
  return __reExport(
    __markAsModule(
      __defProp(
        module2 != null ? __create(__getProtoOf(module2)) : {},
        "default",
        !isNodeMode && module2 && module2.__esModule
          ? { get: () => module2.default, enumerable: true }
          : { value: module2, enumerable: true }
      )
    ),
    module2
  );
};
var __toCommonJS = /* @__PURE__ */ ((cache) => {
  return (module2, temp) => {
    return (
      (cache && cache.get(module2)) ||
      ((temp = __reExport(__markAsModule({}), module2, 1)),
      cache && cache.set(module2, temp),
      temp)
    );
  };
})(typeof WeakMap !== "undefined" ? /* @__PURE__ */ new WeakMap() : 0);

// playground-input:App.jsx
var App_exports = {};
__export(App_exports, {
  default: () => Demo,
});

// playground-input:styles.css
(function () {
  const style = document.createElement("style");
  style.innerHTML = ".esscu button{width:200px;}";
  style.setAttribute("data-playground-style-id", "esscu");
  document.head.appendChild(style);
})();

// playground-input:App.jsx
var import_private_button_lib = __toESM(require("my-private-button-lib"));
function Demo() {
  return /* @__PURE__ */ React.createElement(
    import_private_button_lib.Button,
    null,
    "Button"
  );
}
module.exports = __toCommonJS(App_exports);

To render it, we need to:

  • evaluate the module and get the default export
  • if necessary, provide external dependencies

The above module will be wrapped as ((require, module) => { ... content }))(require, module) using new Function and then evaluate. Note we passed in two external variables, which is require and module.

You can see in the bundled code there is a require('my-private-button-lib') call, which is the CJS version of import 'my-private-button-lib'. Thus, we can provide a customized require to inject external modules.

import * as privateLib from "my-private-button-lib";
// ...

const dependencies = {
  "my-private-button-lib": privateLib,
  // ...
};

function require(key) {
  return dependencies[key];
}

After the bundled CJS module is evaluated, we then get the function component at module.exports.default and then render it to the preview panel.