A basic typescript and webpack setup with cjs and esm target bundling.
- TSC or Babel
- Bundling: why?
- Bundling: Rollup or webpack
- Scripts
- Code splitting and the
vendor
chunk - Test your components
- Current Versions
Both, the official TypeScript compiler tsc
and Babel transpile TypeScript files *.ts
to JavaScript. I decide
to use tsc
as this is the official transpiler by the maintainer Microsoft.
If you prefer to use Babel I recommend to take a look at this quick tutorial: https://dev.to/remshams/rolling-up-a-multi-module-system-esm-cjs-compatible-npm-library-with-typescript-and-babel-3gjg
npm install typescript
Create a file tsconfig.json
in your project's root directory.
{
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"lib": [ "es2015", "dom" ],
"sourceMap": true,
"declaration": true,
// Additional settings to your fancy
"allowJs": false,
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": false,
"noImplicitThis": false,
"noImplicitAny": false,
"strictNullChecks": false,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": false,
"noUnusedParameters": false
},
"include": ["./src/ts/**/*.ts"],
"exclude": ["./dist", "node_modules"],
"removeComments": false
}
tsc -p tsconfig.json
This will transpile your .ts
files to JavaScript files in ./src/cjs/
. Note that these are CommonJS (cjs
) modules.
If you want to create, let's say esm
modules, you may change the target
option to "ES2015"
and module
option
to "ESNext"
. This is also possible by overriding the settings in the tsc
command itself, so you don't need two
separated config files:
tsc -p tsconfig.json --target ES2015 --module ESNext --outdir src/esm/ --moduleResolution node
Take a look a the demo files in ./src/ts/
:
index.ts
(exports all your stuff that should be exposed)myclasses.ts
(a class demo)myconstants.ts
(some constants)myfunction.ts
(two functions, a basic one and a secoond one that uses an external function from node_modules)
Afer the tsc
command was run the ./src/ts/*.ts
TypeScript files have been transpiled to JavaScript files
in ./src/cjs/*.js
.
To bundle your project means to pack all your generated files up into one single JavaScript library file.
Some people still like to load the cjs
version (which is the web browser compatible version) directly
into their HTML document using <script src="...">
tags.
I have seen people who add the bundle to their repository, some don't.
It's up to you, but imagine that not everyone has a build chain set up and ready and maybe just want to have a peek into your library. Imagine there is a bundled file that's ready to be used out of the box.
Cool, eh? So let's bundle our project.
Even though I had good results with Rollup.js I am somehow more familiar with webpack and prefer to use that one.
npm install webpack webpack-cli terser-webpack-plugin
Note: let's also install the terser plugin which gives us a bit more control over the generated output.
Create the file webpack.config.js
. Note that the webpack config in this repo is a bit dynamic
as is handles the env
setting (development
or production
). We use this switch
to emit two different builds: one for the dev environment (non-minified, larger file size) and
one for the production environment (minified, small file size).
const path = require('path');
const TerserPlugin = require("terser-webpack-plugin");
const { name } = require("./package.json");
module.exports = ({
return {
entry: './src/cjs/entry.js',
mode: env.development ? "development" : "production",
output: {
filename: name + (env.development ? '' : '.min') + '.js',
path: path.resolve(__dirname, 'dist'),
},
// Add this line to get the ./dist/build.js.map file
devtool: "source-map",
optimization: {
minimize: !env.development,
minimizer: [
// extractComment=false to prevent the generation of License.txt
new TerserPlugin({extractComments: false})
],
}
}
});
Take a look at the ./src/cjs/entry.js
file:
// Expose all your components to the global scope here.
globalThis.MyClass = require("./myclasses").MyClass;
This file is not generated, you will have to maintain it manually each time you add new components.
In this file you should bind all components/classes/functions/constants to the global scope which
your global app needs. The global scope is usually window
in the browser, or in a more general
case globalThis
. The latter one keeps your code compatible with node environments which may not
know the window
instance (and compatible with modern web browsers).
Webpack itself supports to export your code as libraries but I prefer to simpley define a wrapper around my components and export that one.
Example from ./src/ts/mylibrary.ts
:
import { MyClass } from "./myclasses";
export const MyLibrary = {
MyClass: MyClass
}
And then change webpack's entry point in ./src/cjs/entry.js
to this:
globalThis.MyLibrary = require("./mylibrary").MyLibrary;
This single line exposes your library, so each time you add components to the MyLibrary
object
it will automatically be accessible here, like var myObject = new MyLibrary.MyClass();
.
Doing this prohibits environment pollution and avoid naming clashes with other scripts.
npx webpack --config webpack.config.js
npm run compile:cjs
Emits the CommonJs cjs
Javascript files into the ./src/cjs/
directory.
npm run compile:esm
Emits the ES2016 module esm
Javascript files into the ./src/esm/
directory.
npm run webpack:dev
Bundles the cjs
files into a development package (non minified) into ./dist/tswebpack-main.js
. Note that
tswebpack
is the package name from the package.json
file.
npm run webpack:prod
Bundles the cjs
files into a production package (minified=all unneeded whitespace removed) into
./dist/tswebpack-main.min.js
.
There is a caveat you will notice when you use external libraries (usually from the node_modules
directory): webpack will bundle these libraries into your build, too! This will blow
up your code and harbours the danger of duplicate libraries being loaded (if a different
script includes the same libs).
On the one hand this js totally legitimate, because these parts are required to get your code running, and maybe you only have one single build containing everything you'll need, which is typical for larger web applications.
On the other hand you might wish to have shared libraries installed by your own (like a global jQuery, Axios, Bootstrap, Three, Paperjs, Twojs, TweenMax, ...), because they might be used by other code fragments too which are not bundled here.
The solution for this is: code splitting.
To obtain that just add this snippet to the optimization
part of your webpack config:
optimization {
// ...
splitChunks: {
cacheGroups: {
vendor: {
test: /node_modules/,
chunks: 'initial',
name: 'vendor',
enforce: true
},
}
}
// ...
}
When running webpack now this will result in two separate files being emitted into ./dist/
:
tswebpack-main.js
- Containing your code only
tswebpack-vendor.js
- Containing the code that's imported by your code from the
./node_modules/
- Containing the code that's imported by your code from the
Yay \o/
If you find any errors please report them in an issue.
A nice testing suite is Mocha. There is a test script test.js
which can be
run using the test
script from the package.json
:
npm run test
The output should look like something like this:
Basic library test
✔ should generate a constant string (1) and repeat it (2)
1 passing (3ms)
At the time of writing this these are the versions I used:
...
"typescript": "^4.4.3"
"webpack": "^5.54.0",
"webpack-cli": "^4.8.0"
...
Have fun and don't forget to support your friends!