JavaScript – my first bundler experience with TypeScript & Vite

I needed a bundler, this is how I got on with Vite.

I’ve developed with JavaScript for years but oddly never needed a bundler. That’s because I try not to stack dependencies by using the npm install process of employing other peoples modules. I wrote a blog post about that which you can read here.

I value simplicity, stability, robustness and longevity highly in my dev environment, and my research around bundlers was getting scary

Before we get started we should note that we are in a time of flux around module wiring at time of writing [March 2023]. The package builders who wrote those great packages we can just grab via npm followed appropriate standards and I guess it all works fine when preparing code to run on the server in node. For me, targeting the browser and coming to the concept of bundling for the first time, it was a bit of a confusion, particularly as I had written a projected using ES6 imports without an issue (it did not have any npm-sourced packages in that instance). If you are reading this in the future it might be that this whole module mishmash is long since solve. So just in case, the version info for the main tech involved here are node.js version v16.17.0 and TypeScript compiler v4.9.3, and I’m using Chrome browser v111.0.5563.65 (Official Build) (64-bit).

Background

I was working on a side project related to handling text. I came to the bullet point in the requirements list that said emoji’s. It turns out that emoji’s represent an important edge case in which some of the shapes we see on screen as single letters do not map to exactly one character in a JavaScript string. Take the red heart ❤️ – this is present in a JavaScript string as the two characters u2764 and uFE0F. It’s not a trivial issue, and importantly it relates to several languages – hell even the ‘e’ in the name of the café you might be reading this in can be written as a double-character combo.

So what? Well, if you have a string containing the red heart emoji – that’s one visible character on screen – then the JavaScript string.length() will return 2. Yes, 2.

Like I said, so what? Better have another coffee my friend, the impacts of this are wide and far-reaching. If you ever have to do anything that depends on knowing the length of this string, or want to cut-out the heart from somewhere in a longer string, you will find it hard to know exactly what is going on – where does a multi-character ‘letter’ start and end – some are two chars long, some are three. Trust me on this and let’s move on with the Vite discussion!

So – for once I needed a library. Specifically Graphemer. Take a look at that link for a fuller explanation of the issue with multi-character graphemes.

My initial approach was to take the option to install the Graphemer lib as a simple JavaScript file, accessed via the a script tag in the index.html. And that’s where my day got complicated.

Typescript & Modules

Did I say this side-project was also my first foray into TypeScript? I’d started this project from an empty VSCode window in an empty folder. I’d written several thousand lines of code and was quite pleased with myself over how it had all gone to well.

I am using the ES6 modules syntax. Since all my modules are self-written and nothing sophisticated was going on, this was all working ok.

Then came the need for Graphene. The Graphemer github page says all one needs to do is to use

import Graphemer from 'graphemer';

const splitter = new Graphemer();

Which I did. No issues with TSC so I opened index.html in the browser and found this in the console:

main.js:6 Uncaught ReferenceError: require is not defined
    at main.js:6:21

This is a TypeScript project, so my original source is transpiled / modified by TSC – the file that error message is referring to looks like this

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const Graphemer_1 = __importDefault(require("Graphemer"));
function test() {
    const splitter = new Graphemer_1.default();
}

So that’s the line that includes ‘require(“Graphemer”)’. Hmmmm.

After some research it appears that there is a bit of a gray area around how the wiring between ES6 module syntax looks and how node’s modules / require operates. Ignoring the religious debate about what the one right approach might be, I just wanted to move forward.

If you Google the topic you will find much, often confecting, advice about changes to be made to the tsconfig.json file. For those who don’t know, this is the file that holds the config for the TypeScript transpiler – it holds settings like the target JS level, but also much more. I tried a few of the suggestions but they either produced other unfathomable errors or no beneficial change.

Flummoxed – I asked a question on StackOverflow. After a very kind answer from @T.J.Crowder, the penny dropped. The important part of that answer is quoted here for posterity:

“To use npm modules in a browser-hosted application, you’ll need to use a bundler like Vite, Webpack, Rollup, Browserify, etc. That’s because you won’t provide the entire node_modules directory in script tags in your application. The purpose of the bundler in this situation is to wrap up your code along with code from node_modules and put it into “bundles” that you then include in your browser application.”

@TJ also mentioned a bunder named Vite as a possible solution because of its low requirement for config changes / overheads.

It is worth saying that before TJ’s answer came in I had been aware of bunldlers and started looking at WebPack because that was what my research was leading me toward as the go-to bundler. However, I was seeing folks mentioning concerns about config complexity and performance. I also felt that I would be adding a ton of complexity to the tooling – before this it was me and VSCode – how would I ever get a workmate set up with the same environment? I value simplicity, stability, robustness and longevity highly in my dev environment, and my research was getting scary.

Don’t get me wrong, I have used require.js in other previous projects, so I’m not an entire newbie, I just didn’t like the feeling of the lunatics taking over the asylum.

I had come across Vite in my research and I decided on a experiment with it before I dived into WebPack.

Vite’s quickstart page should be checked in case there are changes there, but effectively this is all I had to do to set up a new vite project:

  1. Start VSCode and start a new terminal window – you do not need to create a project folder first!
  2. In the terminal window, cd to the folder that will be the parent folder of your project. Vite will create a project folder for you in the next step.
  3. Now run
 npm create vite@latest

which will produce a sequence of prompts.

  • project name : You will be asked the name of your project – whatever you type here becomes the name of the project folder AND the name of the project in the package.json file.
  • Select a framework : this is a list of library types like vanilla JS, React and Vue – select a TypeScript project select Vanilla JS here.
  • Select a variant : here you are asked whether your Vanilla JS project is Plain JS or TypeScript. Choose TypeScript.

It looks like this:

npm create vite@latest
Need to install the following packages:
  create-vite@4.1.0
Ok to proceed? (y) y
√ Project name: ... myProject
√ Package name: ... myProject
√ Select a framework: » Vanilla
√ Select a variant: » TypeScript

Scaffolding project in <path to parent folder>\myProject...
  1. On completion, you are prompted with the following:
Done. Now run:

  cd myProject
  npm install  <<< when you run this it will read the package.json that vite set up to know what to install
  npm run dev  <<< when you run this it prepares and starts the template project.
  

Pay special attention to the cd – it’s telling you to make the current folder the new project folder you just created. If you miss this then the following commands (reasonably) go wrong. So do not foget the cd!

The run dev command produces the following

npm run dev

> myProject@0.0.0 dev
> vite


  VITE v4.1.4  ready in 441 ms

  ➜  Local:   http://127.0.0.1:5173/
  ➜  Network: use --host to expose
  ➜  press h to show help

h
  Shortcuts
  press r to restart the server
  press u to show server url
  press o to open in browser
  press c to clear console
  press q to quit

The immediately interesting shortcut here is the o which opens a browser tab on the vite dev server IP, showing the current template index.html page.

Looking at the project folder setup, we have

myProject (root folder)
├── node_modules
├── public
├── src
│   └── counter.ts
│   └── main.ts
│   └── style.css
│   └── typescript.svg
│   └── vite-env.d.ts
└── index.html
└── package.json
└── package-lock.json
└── tsconfig.json

Vite uses index.html as the start point for its packaging / bundling process. Other bundlers put it away in a subfolder, but vite keeps it in the root.

You can now open the myProject folder in VSCode and see and interact with the files, meaning you can start developing, add modules in the src folder, etc.

At this point it should be noted that no config files were edited to get to this stage. No weird error messages appeared in the terminal window. No two-steps-forward-one-step-back or snakes-and-ladders issues.

So what of interaction with the sample npm package Graphemer ?

Oh yes! My original question – also good news. I opened a VSCode terminal window and ran

$ npm i graphemer

Which installed Graphemer. I then modified index.html to include a new para with id of ‘info’ and modified the /src/main.ts to include:

import Graphemer from 'graphemer';

const splitter = new Graphemer();
// split the string to an array of grapheme clusters (one string each)
const string = `Iñtërnâtiônàlizætiøn ☃ 💩`;
const graphemes = splitter.splitGraphemes(string);

...current contents of main.ts

document.querySelector<HTMLDivElement>('#info')!.innerHTML = 'Unicode string is [' + graphemes +']';

Which produced the following – notice the yellow highlit line showing the expected output. [Aside: Graphemer is used to handle multi-character Unicode such as is required by many emoji’s and languages] The Vite & TS images etc are all part of the standard Vite template. The counter is clickable and increments at each click thus confirming early that the TS transpiling works.

Again it should be noted that this all just worked out of the box.

So what about building for production?

Execute the command

npm run build

> test-vite-project@0.0.0 build
> tsc && vite build

vite v4.1.4 building for production...
✓ 21 modules transformed.
dist/index.html                       0.45 kB
dist/assets/typescript-f6ead1af.svg   1.44 kB
dist/assets/index-3443e464.css        1.24 kB │ gzip:  0.64 kB
dist/assets/index-366ad949.js        96.25 kB │ gzip: 16.48 kB
PS myProject> 

Which produces a /dist folder containing the index.html file and an /assets sub-folder containing the a file of bundled JS and another of bundled CSS.

Conclusion

It appears that Vite is viable for my use-case. So far it seems to be simple to work with, at least in this uncomplicated case, avoids configuration distractions, achieves the requirement of enabling npm packages with ES6 module syntax, and just plain works.

From reading at the Vite website I know that Vite uses Rollup under the covers and it remains to be seen what the limitations or extra config hassles are. However at this stage I am happy with Vite as a bundler.

Thanks for reading

VW. March 2023

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: