Building a UI Library Capable of Tree Shaking: From A to Z


A well-built UI library is often used to build webpages with abundant features. I am currently working to develop one of the libraries included in the TOAST UI, and I intend to share all of my experience and know-hows I accumulated along the way. The main purpose of this article is to introduce the readers to pragmatic information like how UI libraries are built, what the purpose and the features of the library are, and what kind of technology stack and webpack configurations were used. If you are interested in learning everything related to building a UI library, get your laptops out, and follow me on this journey.

image

Motive

The TOAST UI Grid's revolutionary improvement on the internal structure motivated me to create the v2 for TOAST UI Calendar. The Grid is a UI library that emphasizes performace as it has to render massive daa, and with the introduction of preact and Grid's own reactive system instigated incredible innovation. I was fortunate enough to watch the entire process from the sidelines, and because they were kind enough to share their meticulous weekly records, I built up the courage to start working on my own.

Before we dive deeper into the article, I would like to first express my gratitude to the Grid team.

Main Purpose, Features, and Technology Stack of a Modern UI Library

The improved UI libraries, in my opinion corroborated by the strengths of TOAST UI Grid, should have the following purposes and features.

Purpose of the Library

In terms of technology, the purpose, from the perspective of the users and the developers, is as follows.

  1. For users, the library should help users minimize the resulting size of the webpage by letting them use only what is desired.
  2. For developers, the library should enhance productivity by structuring a convenient development environment and should also be performant as well as easy to use.

Core Features of the Library

The core features of the library are as follow.

  1. Supports tree shaking
  2. Optimizes the rendering process through the virtual DOM
  3. Supports server side rendering

Both lodash and momnet are good libraries with great features. However, if you bundle everything including the features that are not used, the resulting size of the library increases, so optimization methods are provided to reduce the resulting size. Tree shaking is one of such methods, and we will explore how we can build a library that supports tree shaking in order to reduce the resulting size of the webpage.

Core Technology Stack

All libraries included in TOAST UI are bundled using the webpack. Let's see what other technology stacks we have integrated. The following stacks have been newly introduced to TOAST UI Calendar with respect to v1.

TypeScript Upon discussing with my coworker, we have elected to use TypeScript. Despite the discomfort it may present to the programmer, we have decide to adopt TypeScript due to the fact that it can lead to easier maintenance later on and the fact that it is less prone to errors.

Virtual Dom (preact) Previously, we used handlebars as the template engine. Because we had to update the entire DOM every time the rendering happens, we decided to adopt the virtual DOM in order to minimize unnecessary rendering.

ES6 Modules It is no longer a breakthrough to use ES6 modules. However, the ES6 modules are mandatory in order to perform tree shaking, and building a UI library that supports tree shaking is yet another challenge. This particular stack will be discussed more in the next section "Reasonings Behind the Core Technology Stack."

Server Side Rendering Support While it may be an overreach to call it a technology stack, with the introduction of preact, it became easier to convert virtual DOM to HTML strings.

sass and postcss Previously, we used stylus. However, we have decided to use sass so that the css can be more structured and postcss so that we could assign the library's unique identifiers to the css class.

In the next section, we further explore why we have elected to use aforementioned technology stack to build a UI library that supports tree shaking as well as our experience with it. If you just want to skip ahead to configuring your environment to start building right away, proceed to "Configuring Your Development Environment - Step By Step."

Reasonings Behind the Core Technology Stack

As I have explained earlier, my coworkers and I have collectively decided to adopt TypeScript. If you plan on not using TypeScript to build a UI library that is capable of tree shaking, you can achieve the same result by using babel, preact, and jsx. Now, let break down the reasonings behind the technology stack we have adopted from JavaScript's point of view.

Tree Shaking Support

While TOAST UI Calendar supports daily, weekly, monthly, and other periodical views, some users only opt to use a certain types of views. These users should not have to include the entire library just to use a small selection of the features. If the user wants to implement the monthly view feature, the user should not have to include the source code for the weekly view. In order to reduce the size of bundled JavaScript payload on the user-side, we implement our code so that the tree shaking is supported.

Module must be free of side effects in order to allow tree shaking.

In order to remove unused features among features exported from the module, the bundler must decide whether the feature truly has not been used. For example, if the exported function A references B, the library may think that only A was used. However, function B must be included in the bundle. This is what is known as a side effect, and even if a function was not used declaratively, other functions may have referenced it. The bundler must consider such cases to see whether the exported functions reference each other or not. This is a tricky issue, and can hurt the bundle performance.

In order to solve such issue, the author of the library must set a flag ensuring that the library is not designed to cause such side effects. Webpack trusts the flag, then, disregards unused modules from the bundle. To do so, set "sideEffects": false in the package.json.

Upon transpilation, the library must be able to be used only with pure JavaScript.

It may seem confusing at first, but webpack uses multiple tools like loaders and plugins when bundling JavaScript. JavaScript that requires a loader cannot be executed directly on the JavaScript VM, and the webpack must perform more miniscule operations to make it operational. When the library has been transpiled and bundled using the loader, it can finally be operated with pure JavaScript. Our library must also be able to be used only with pure JavaScript upon transpilation.

If so, it means that if we want to make an ES6 module that is capable of tree shaking, we cannot use anything that uses a webpack.

This is because webpack converts every JavaScript module into a webpack module. Even if you bundle using the UMD, the library is converted to use codes like __webpack_require__, and all modules are collected to one or more bundle files. Therefore, when the user uses the library, it is no longer an ES6 module. If the module is not of ES6, it cannot be optimized by tree shaking. Furthermore, if all modules are collected into a single bundle file, the chances of side effects increase significantly, so the possibility of tree shaking is close to none.

Then what about Rollup? Rollup is a bundler that lets you build ES6 libraries. Rollup does not use its own moduling method like webpack and maintains the ES6 module structure. When a library is bundled using rollup, all ES6 modules are bundled into one file. However, when the user tries tree shaking using webpack, it is interrupted by side effects.

Does this mean that we have to abandon all of the conveninent features of webpack loader? TOAST UI Calendar v1 uses handlebars for its template engine. While it allowed us write html intuitively, making it convenient for us, it also used webpack loader, so we had to find another solution. We can only use transpilers. The solution I found was using preact and jsx that supports virtual DOM. Babel and TypeScript converts jsx to h function, so when you use it to transpile JavaScript, it produces pure JavaScript.

Rendering Optimization Through Virtual DOM

While preact has a reputation already, it has never been adopted by the FE Development Lab, so we actually considered using snabbdom. However, as I have mentioned earlier preact has proved itself with Grid, so I felt confident using preact with this project.

The advantage of using the virtual DOM is that it removes unnecessary rendering to optimize the rendering process. The other major benefit is related to tree shaking. With the virtual DOM, we can use jsx without a separate template engine. Therefore, we no longer need to use a webpack loader. The jsx can be converted into JavaScript h() function via the transpiler because babel and TypeScript supports preact. Also, because we do not need to use the webpack loader, we can maintain the ES6 module structure. Furthermore, the fact that we do not need a separate template engine is another plus.

Server Side Rendering Support

When TOAST UI Calendar v1 was first developed, it was developed without considering the server side rendering. It was a frequently posed question on the Github community, and I had to reply to them with sorrow and frustration. We could have implemented server side rendering with v1, but because it was built on an actual DOM, it would have been incredibly difficult to execute it from node environment.

However, since we have elected to adopt preact, the server side rendering, which is basically translating virtual DOM to HTML string, was a piece of cake.

Configuring Your Development Environment - Step By Step

Now, let's get down to the business. You can follow along, step-by-step, to build a UI library with the features and purpose we mentioned earlier. Furthermore, you can learn more about the technology stack and the development environment that we actually use at FE Development Lab.

Peak at the Final Picture

First, let's take a peak at the final picture when it is done bundling and transpiling. Like we did with v1, we provide not only the single bundle file, but we also provide the ES6 module.

List of ES5 Single Bundle Files

  • dist/tui-calendar.js
  • dist/tui-calendar.css
  • dist/tui-calendar.min.js
  • dist/tui-calendar.min.css

ES6 Module Capable of Tree Shaking will be created under dist/esm folder like in the image below. (It is a prototype of v2, and in this article, we will only refer to a few simple classes). image (63)

Now, let's go through the core technology and development environment list, and proceed with installation and configuration. For the bundler, we used webpack v4.

Core Development Environment and its Details

  • Basic Configuration
  • Using Static Analysis Tools
  • 📑 Using the Documentation Generator Configuration @toastui/doc to Create API Documentation
  • TypeScript Configuration
  • CSS Configuration
  • Development Server Configuration
  • Bundle and Distribution Configuration
  • Using preact to Build the UI Library
  • Using the Library to Test Its Features

Basic Configuration

Create the project root folder esm-ui-library and initialize the package.

mkdir esm-ui-library
cd esm-ui-library
npm init // And Be The Yes Man

The source folder should look like the following.

  • dist
  • src

    • images
    • sass
    • ts

Install the basic webpack package.

npm i --save-dev webpack webpack-cli

Install TypeScript and its loader.

npm i --save-dev typescript ts-loader

Create the webpack configuration file. webpack.config.js.

const path = require("path");
const webpack = require("webpack");
const pkg = require("./package.json");

const isProduction = process.env.NODE_ENV === "production";
const FILENAME = pkg.name + (isProduction ? ".min" : "");

const config = {
  entry: "./src/ts/index.ts",
  output: {
    path: path.join(__dirname, "dist"),
    filename: FILENAME + ".js"
  },
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: "ts-loader"
      }
    ]
  }
};

module.exports = config;

Create the tsconfig.json File. It does not matter if you leave it blank, but an error will occur if you there is no file.

{
}

Create the entry file src/ts/index.ts.

export default {};

Now, your build should run successfully if you have gotten this far.

npx webpack --mode development

            Asset     Size  Chunks             Chunk Names
esm-ui-library.js  4.4 KiB    main  [emitted]  main
Entrypoint main = esm-ui-library.js
[./src/ts/index.ts] 66 bytes {main} [built]

Use the output found in webpack.config.js to configure settings related to the library like the module type and the namespace.

  output: {
    library: ['tui', 'Calendar'],  // Configuring the library namespace
    libraryTarget: 'umd',          // Configuring the library target
    libraryExport: 'default',     // Configuring the default export of the entry point to the namespace
    ...
  }

You do not need to write the libraryExport module with commonjs. However, if you default export to an ES6 module, this value must be configured. If you do not do so, you will have to jump through trivial hoops like the following.

Before Configuration

const calendar = new tui.Calendar.default();

After Configuration

const calendar = new tui.Calendar();

Configuring Module resolve and alias

You can configure the module resolution by adding resolve to the webpack.config.js. If you use a relative path when you import another module, it would be bothersome because you would have to figure out all of the relative paths. Therefore, you can add alias to make it easier for yourself.

  resolve: {
    extensions: ['.ts', '.tsx'],
    alias: {
      '@src': path.resolve(__dirname, './src/ts/')
    }
  }

Before Configuration

import Month from "../../view/month";

After Configuration

import Month from "@src/view/month";

Configuring the Bundle File Banner

Create a banner for the ES5 single bundle file to describe the version, build date, author, and the license.

You can do so by adding webpack.BannerPlugin to the webpack.config.js.

const BANNER = [
  'TOAST UI Calendar 2nd Edition',
  '@version ' + pkg.version + ' | ' + new Date().toDateString(),
  '@author ' + pkg.author,
  '@license ' + pkg.license
].join('\n');

const config = {
  ...,
  plugins: [
    new webpack.BannerPlugin({
      banner: BANNER,
      entryOnly: true
    })
  ]

With ES6 modules, if you utilize your comments at the top of the TypeScript source, the comments remain after the transpile, so make sure to comment effectively for each source file.

ex> src/ts/month.ts

/**
 * @fileoverview Month View Interface
 * @author NHN FE Development Lab <dl_javascript@nhn.com>
 */
export const Month = {};

Using Static Analysis Tools

Apply eslint, prettier, and stylelint so that JavaScript and CSS can all be analyzed statically. It is recommended that you apply static analysis from the beginning of the project.

Installing eslint

It is conveninent to simply inherit a well defined rules for eslint. Use eslint-config-tui.

npm i --save-dev eslint eslint-loader eslint-config-tui eslint-plugin-react

Since TypeScript also must be analyzed statically, install related packages.

npm i --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin

Installing Prettier

npm i --save-dev prettier eslint-config-prettier eslint-plugin-prettier

Installing stylelint

npm i --save-dev stylelint stylelint-config-recommended stylelint-scss stylelint-webpack-plugin

Configure eslint

Once the .eslintrc.js file is complete with TypeScript, eslint, and prettier, document the following.

module.exports = {
  root: true,
  env: {
    browser: true,
    es6: true,
    node: true
  },
  parser: "@typescript-eslint/parser",
  plugins: ["prettier", "react", "@typescript-eslint"],
  extends: [
    "tui/es6",
    "plugin:@typescript-eslint/recommended",
    "prettier/@typescript-eslint",
    "plugin:react/recommended",
    "plugin:prettier/recommended"
  ],
  parserOptions: {
    parser: "typescript-eslint-parser",
    ecmaVersion: 2018,
    sourceType: "module",
    project: "tsconfig.json"
  },
  settings: {
    react: {
      pragma: "h",
      version: "16.3"
    }
  }
};

Since @typescript-eslint/parser has been updated to 2.0, a frequent error displaying the import statement as an error is known to occur in the editor. However, it is only an error with the editor, and functions normally. Such issue has been registered and people are looking at it, so let's update it when the new patch is up.

Configuring prettier

Complete your .prettierrc.js file like the following. You can modify your rules to better suit the needs of your team. Recently, our team of 11 members had a 30 minute heated discussion on whether to set the printWidth to be 80, 100, or 120. LOL!

module.exports = {
  printWidth: 100,
  singleQuote: true
};

Configuring stylelint

Complete your stylelint.config.js file like the following.

module.exports = {
  extends: "stylelint-config-recommended",
  plugins: ["stylelint-scss"]
};

Configuring Webpack

Add eslint-loader for JavaScript static analysis. If the use attribute is of array type, the loader executes from the back of the array, so take necessary precaution. If the order is reversed, you will end up with statically analyzing what the TypeScript has transpiled.

const StyleLintPlugin = require('stylelint-webpack-plugin');

const config = {
  ...
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: ['ts-loader', 'eslint-loader']
      }
    ]
  },
  plugins: [
    ...
    new StyleLintPlugin()
  ]
};

Configuring Visual Studio Code

The following is a list of extensions you would need to install to view the results of static analysis immediately. Click on the following links, and install all of them.

Then, create a settings folder for the Visual Studio Code, and complete the .vscode/settings.json file like the below.

{
  "eslint.validate": [
    {
      "language": "typescript",
      "autoFix": true
    },
    {
      "language": "typescriptreact",
      "autoFix": true
    }
  ]
}

📑 Using the Documentation Generator Configuration @toastui/doc to Create API Documentation

API Documentation is incredibly important for any library. The difference between first glance of an API with a well composed documentation and an API without it is massive.

TOAST UI Doc is a newly published documentation generator that we are trying to apply to all of the TOAST UI products. It parses the JSDoc to create an API documentation and combines it with all of the examples into a single document. With a few configurations, you can create documentation for any JavaScript library easily. You will be more convinced once you take a look at our demo.

image

Installing the Package

npm i -g @toast-ui/doc

Completing The Configuration File

First, create your tuidoc.config.json file.

Since TypeScript is not yet supported, you can just create the API documentation with respect to the ES6 modules created in dist/esm folder. If you configure the Github repository, you can actually see the actual source directly. Since the following is just an example, you can adjust the image and the text to suit your needs.

{
  "header": {
    "logo": {
      "src": "https://uicdn.toast.com/toastui/img/tui-component-bi-white.png",
      "linkUrl": "/"
    },
    "title": {
      "text": "Calendar",
      "linkUrl": "https://github.com/nhn/toast-ui.doc"
    },
    "version": true
  },
  "footer": [
    {
      "title": "NHN",
      "linkUrl": "https://github.com/nhn"
    },
    {
      "title": "FE Development Lab",
      "linkUrl": "https://github.com/nhn/fe.javascript"
    }
  ],
  "main": {
    "filePath": "README.md"
  },
  "api": {
    "filePath": "dist/esm",
    "permalink": {
      "repository": "https://github.com/nhn/toast-ui.doc",
      "ref": "master"
    }
  }
}

Adding JSDoc

Add the JSDoc like the following.

/**
 * @class Calendar Calendar View
 */
export default class Calendar {}

Use npm scripts to make your life better.

If you add the script to package.json and run it, the documentation will be created under _latest folder.

{
  "scripts": {
    "doc": "tuidoc"
  }
}

TypeScript Configuration

Since we have already installed TypeScript package earlier, let's complete the TypeScript configuration file. Create two different configuration files. One will be transpiled with ES5 and will create a single bundle file. The other will be transpiled with ES6 and will create a ES6 module that is capable of tree shaking.

ES5 Single Bundle File

Because we have already added ts-loader, our webpack configuration is already finished. Then, complete the TypeScript configuration file like the following.

tsconfig.json for ES5

{
  "compilerOptions": {
    "noImplicitAny": true,
    "target": "es5",
    "jsx": "react",
    "jsxFactory": "h",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "baseUrl": ".",
    "paths": {
      "@src/*": ["src/ts/*"]
    },
    "sourceMap": true
  },
  "include": ["src/ts/**/*.ts", "src/ts/**/*.tsx"],
  "exclude": ["node_modules"]
}

Although we have added alias in the webpack configuration, Visual Studio Code will show an error saying that it cannot find the module. However, you can fix the error by adding TypeScript configuration file and setting the alias to the baseUrl and to paths.

image

Adding the Library Entry File - package.json Set the entry point of the library to ES5 single bundle file. It will be the main property of package.json.

{
  "main": "dist/esm-ui-library.js"
}

ES6 Module (Capable of Tree Shaking)

In order to transpile TypeScript into ES6 modules, we need to use TypeScript transpiler directly instead of using the webpack. The reasoning behind it has been explained in "Reasonings Behind the Core Technology Stack" earlier.

Add one more TypeScript configuration file to transpile TypeScript into ES6 module.

tsconfig.esm.json for ES6

{
  "compilerOptions": {
    "noImplicitAny": true,
    "target": "es6",
    "jsx": "react",
    "jsxFactory": "h",
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "moduleResolution": "node",
    "outDir": "dist/esm/",
    "strict": true,
    "baseUrl": ".",
    "paths": {
      "@src/*": ["src/ts/*"]
    }
  },
  "include": ["src/ts/**/*.ts", "src/ts/**/*.tsx"],
  "exclude": ["node_modules"]
}

Here are the differences between the file above and the file for ES5.

  • Change: "target": "es6"
  • Add: "moduleResolution": "node", add if library user develops under node environment or the code is executed in node.
  • Add: "outDir": "dist/esm/", add in order to configure the destination path for ES6 module
  • Remove: sourceMap, it is not necessary as ES6 module doe not need to be bundled

Configuring Module Path for ES6 - package.json

{
  "module": "dist/esm/",
  "sideEffects": false
}
  • module property: configures the search path if used as an ES6 module.
  • sideEffects property: It is possible that tree shaking cannot remove all unnecessary modules due to side effects. This flag ensures that this library is not subject to side effects. The author of the library is responsible for this flag, and can be executed declaratively when the webpack is performing tree shaking.

Changing the alias to a Relative Path for ES6 Modules

Even after transpiling TypeScript, the alias like @src still exist.

import Month from "@src/month";
import Week from "@src/week";

If the library user is importing ES6 modules, the @src cannot be read, so it will throw an error saying that the module cannot be found. Therefore, the alias must be converted back to the relative paths. You can add ttypescript - Transform Typescript and typescript-transform-paths plugin to do so.

npm i --save-dev ttypescript typescript-transform-paths

Add the tsconfig.esm.json for ES6 module to the plugin.

{
  "compilerOptions": {
    ...,
    "plugins": [
      {
        "transform": "typescript-transform-paths"
      }
    ]
  },
  ...
}

Before Configuration

import Month from "@src/month";
import Week from "@src/week";

After Configuration

import Month from "./month";
import Week from "./week";

Configuring the CSS

Since v1 uses the stylus to transpile the CSS, we used the stylus-loader. Earlier, I mentioned that in order to support JavaScript tree shaking, we cannot use any webpack loaders. However, it is a different scenario for the CSS. Any kind of loaders and tools can be used with CSS because the CSS can just be added to the final HTML bundle.

npm i --save-dev css-loader style-loader mini-css-extract-plugin

Importing CSS

The first thing to watch out for is not importing CSS from the JavaScript source. If you are familiar with using webpack for your projects, you are probably used to importing the CSS file from your JavaScript code. In this case, if the JavaScript imports CSS after it has been transpiled, the library user must take care of importing the CSS file from the user end. If not, the wrong CSS file path will cause the entire module import to fail. Therefore, simply add importing CSS file to a separate entry point instead of importing at the JavaScript source. If you add the webpack entry points as an array, you can include it in the bundle process by adding another dependency graph without having to import CSS from JavaScript.

webpack.config.js

module.exports = {
  entry: ['./src/sass/index.scss', './src/ts/index.ts'],
  ...
};

Using SCSS

While stylus is an incredible tool, the contributors of TOAST UI Calendar are more used to sass. Also, the higher ranking on the market played a role as well.

Install sass and sass-loader.

npm i --save-dev node-sass sass-loader

Registering Unique Library prefixes to the Class Selectors

For the TOAST UI Calendar's CSS, we use the tui-full-calendar- prefix to write our class selectors. We assign a unique identifiers to prevent duplicate selectors. For v1, we used preprocess-loader and substituted the stylus code strings in the bundle process.

const context = JSON.stringify({CSS_PREFIX: ‘tui-full-calendar-‘});
...
module: {
  rules: [
    {
      test: /\.styl$/,
      use: [
        `preprocess-loader?${context}`,
        ‘css-loader’,
        ‘stylus-loader’
      ]
    }
  ]
}

The {css-prefix} portion from stylus file will be substituted by tui-full-calendar-.

.{css-prefix}holiday {
  color: red;
  font-size: 15px;
}

Now, however, the code is not aesthetically pleasing, and we have to struggle to find the selectors in the code, making maintenance difficult. If you use postcss, the code can become much cleaner.

Install postcss-loader and postcss-prefixer.

npm i --save-dev postcss-loader postcss-prefixer

Now, the CSS looks a lot neater.

.holiday {
  color: red;
  font-size: 15px;
}

If you look at the changed results with postcss, all are prefaced like the following.

.tui-full-calendar-holiday {
  color: red;
  font-size: 15px;
}

Bundling Images to be Used With CSS

Use the url-loader to include the images to be used with CSS in base64 in the bundle.

npm i --save-dev url-loader

Configuring Webpack for CSS Bundling

Add the CSS configurations to the webpack config file's module.rules.

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const postcssPrefixer = require('postcss-prefixer');

...
const isDevServer = process.env.DEV_SERVER === 'true';
...

module: {
  rules: [
    ...
    {
      test: /\.s[ac]ss$/i,
      use: [
        isDevServer ? 'style-loader' : MiniCssExtractPlugin.loader,
        {
          loader: 'css-loader',
          options: {
            sourceMap: true
          }
        },
        {
          loader: 'postcss-loader',
          options: {
            sourceMap: true,
            plugins: [
              postcssPrefixer({
                prefix: 'tui-full-calendar-'
              })
            ]
          }
        },
        {
          loader: 'sass-loader',
          options: {
            sourceMap: true
          }
        }
      ]
    },
    {
      test: /\.(gif|png|jpe?g)$/,
      use: 'url-loader'
    }
  ]
},
plugins: [
  ...,
  new MiniCssExtractPlugin({
    filename: `${FILENAME}.css`
  })
]

Development Server Configuration

Install webpack-dev-server and use html-webpack-plugin to confirm that the page loads.

npm i --save-dev webpack-dev-server html-webpack-plugin

Add html-webpack-plugin to the webpack.config.js and to the development server configurations. Thing to watch out for here is that you have to add the '.js' extention to resolve.extensions. If you do not add '.js', none of the js modules read by webpack-dev-server will be executed; therefore the server will not run.

const HtmlWebpackPlugin = require('html-webpack-plugin');

const config = {
  ...,
  resolve: {
    extensions: ['.ts', '.tsx', '.js'], // Add '.js'.
    ...
  },
  plugins: [
    ...,
    new HtmlWebpackPlugin()
  ],
  devtool: 'source-map',
  devServer: {
    historyApiFallback: false,
    host: '0.0.0.0',
    disableHostCheck: true
  }

While nothing is displayed on the screen, if you run it, you will be able to see that the JavaScript and CSS have loaded well.

npx webpack-dev-server --mode development
<head>
  <link href="/dist/esm-ui-library.css" rel="stylesheet" />
</head>
<body style="">
  <script type="text/javascript" src="/dist/esm-ui-library.js"></script>
</body>

Bundle and Distribution Configuration

Now is the time to check whether ES5 bundle file and ES6 module files have been created successfully.

Add an npm script that runs bundles of different types and executes the server. The script ("doc") that uses @toastui/doc have been modified to build the ES6 module and then create the API documentation.

{
  "scripts": {
    "doc": "npm run build:esm && tuidoc",
    "serve": "DEV_SERVER=true webpack-dev-server --mode development",
    "build:dev": "webpack --mode development",
    "build:prod": "NODE_ENV=production webpack --mode production",
    "build:esm": "ttsc -p tsconfig.esm.json",
    "build": "rm -rf dist && npm run build:dev && npm run build:prod && npm run build:esm"
  }
}

Creating the ES5 Single Bundle File

Build for development and production versions. You should see the files under the dist folder.

npm run build:dev
npm run build:prod

Creating the ES6 Module

The file has been created under dist/esm folder.

npm run build:esm

What the created file should look like image (65)

Choosing the File to Publish on npm

You do not need to publish any unnecessary files on the npm, so choose only what you need.

package.json

{
  "files": [
    "src",
    "dist",
    "index.d.ts"
  ]
}

Using preact to build a UI Library

First install preact and preact-render-to-string for server side rendering.

npm i --save preact preact-render-to-string

First, simply create the entry file and a class that renders Month and Week. The renderToString is a function that converts preact components to HTML strings.

index.ts

import Month from "@src/month";
import Week from "@src/week";

export default {
  Month,
  Week
};

export { Month, Week };

base.ts

import { render, ComponentChild } from 'preact';
import renderToString from 'preact-render-to-string';

export default abstract class Base {
  private _container: Element;

  private _base?: Element;

  public constructor(container: Element) {
    this._container = container;
  }

  protected abstract getComponent(): JSX.Element;

  public render(): void {
    this._base = render(this.getComponent(), this._container, this._base);
  }

  public renderToString(): string {
    return renderToString.render(this.getComponent());
  }
}

month.tsx

import { h } from 'preact';
import Base from '@src/base';

export default class Month extends Base {
  protected getComponent(): JSX.Element {
    return <h2>Month View</h2>;
  }
}

week.tsx

import { h } from 'preact';
import Base from '@src/base';

export default class Week extends Base {
  protected getComponent(): JSX.Element {
    return <h2>Week View</h2>;
  }
}

Using the Library to Test Its Features

Now, let's actually use the library to test whether the library works as we intended it to. This step is to ensure that the library and its features adhere to the purpose laid out earlier.

  • Tree Shaking Test - Testing whether the size shrinks due to bundling only the necessary modules
  • Server Side Rendering Test - Testing the HTML string generation

Test Code

The following test code imports Week and Month modules to render, and renders the Month on the server side to generate HTML strings.

import { Month, Week } from "esm-ui-library";

const week = new Week(document.getElementById("app1"));
week.render();

const month = new Month(document.getElementById("app2"));
month.render();

document.getElementById("ssr").innerHTML = month.renderToString();

On Screen

Once you run the tests, you should be able to see the Week, Month, and the HTML, which is the result of rendering the Month on the server side.

image (66)

Tree Shaking Test

The size of the bundle file is 12.8 KiB, and you can see that the bundle file includes month.tsx module as well as the week.tsx.

      Asset       Size  Chunks             Chunk Names
 index.html  551 bytes          [emitted]
    main.js   12.8 KiB       0  [emitted]  main
main.js.map   49.8 KiB       0  [emitted]  main
new (class extends v {
  getComponent() {
    return Object(o.h)("h2", null, "Week View");
  }
})(document.getElementById("app1")).render();
const m = new (class extends v {
  getComponent() {
    return Object(o.h)("h2", null, "Month View");
  }
})(document.getElementById("app2"));

Let's remove the Month from the source and try bundling again. The tree shaking should step in to remove the Month modules.

import { Month, Week } from "esm-ui-library";

const week = new Week(document.getElementById("app1"));
week.render();

// const month = new Month(document.getElementById("app2"));
// month.render();

// document.getElementById("ssr").innerHTML = month.renderToString();

The bundle size has been reduced to 12.6KiB, and you should be able to see that the month.tsx file has been removed from the bundle.

      Asset       Size  Chunks             Chunk Names
 index.html  551 bytes          [emitted]
    main.js   12.6 KiB       0  [emitted]  main
main.js.map   49.4 KiB       0  [emitted]  main
new (class extends v {
  getComponent() {
    return Object(o.h)("h2", null, "Week View");
  }
})(document.getElementById("app1")).render();

Closing Remarks

In any language, removing the unnecessary source to reduce the resulting file size is incredibly important. This is because the increase in file size inevitably results in higher cost. As for JavaScript UI Library, the more feature a library has, bigger the size becomes. However, users may want to use only a certain selection of features a library provides. Therefore, wouldn't the optimization method for the web also lead to a better UI library? While I chose the tree shaking method, there are cetainly more out there.

TOAST UI Calendar has been loved ever since we opensourced the well written v1. It received more than 7k stars on Github, and I am genuinely happy that it became a beloved opensource. The process of planning the TOAST UI Calendar v2, deciding on the technology stack, and managing the possibilities were intriguing tasks. Now is the time to take yet another step. I sincerely hope that this article can help other people. The entire source code can be found here.

If you have a feature you want to see implemented with TOAST UI Calendar v2, please feel free to leave it on the Github issues. Let's keep the momentum going!