Builds TypeScript packages into module and dist/bundle output with accompanying types, while also ensuring that code will run in both browser and node contexts while being fully tree-shakeable for webpack/parcel/etc builders to minimize client-side bundle size.
yarn add -D tachyon-package-builder
The base command is simply:
build-package
If everything is properly configured (as listed below), this should just work and you'll be ready to publish your package!
There are a couple of flags you can use to modify the build behavior:
- default option (technically
--pkgDir
/-p
for obscure use-cases) This defines the directory of the package to be built. Useful for CI or automation situations in which you might want to run the builder from outside of the package directory. Since this is the default argument you should not normally need the flag:
build-package path/to/package
-
--watch
/-w
This will cause the builder to run in watch mode, in which case it will incrementally rebuild any change files, greatly reducing subsequent build times. Useful for when you developing the package while it is linked to another application or in a monorepo. -
--notify
/-n
This will give you an OS notification when the build is done. Useful for large projects with longer build-times. -
--serverOnly
/-S
This will eliminate the module build from the output, for when you know that the package will never be run in the browser. See config notes below regarding this build method. -
--webpackOnly
/-W
This is a special mode that will skip TypeScript as well and only run webpack, outputting only a single bundle file. This is only for very specific use-cases like transpiling a vendored dependency and should not be generally used. -
--help
/-h
This shows the available options.
You can use this package inside another JavaScript file by importing it.
const buildPackage = require('tachyon-package-builder');
...
// Set the config values you want. Note that usage in node requires providing
// the pkgDir parameter.
const opts = { help, notify, pkgDir, serverOnly, watch, webpackOnly };
buildPackage(opts);
This package works by combining TypeScript, Babel, and Webpack to generate the appropriate build artifacts. It uses TypeScript only to strip types and emit .d.ts files, leaving the actual JS transpilation to Babel in order to be able to share @babel/runtime with community packages (instead of getting tied to ts.lib). Webpack is left to do the bundling for the dist output.
As a result of the above pipeline, there are a few requirements with regards to package structure:
-
tsconfig.build.json
in the root of the package. This will probably extend atsconfig.json
, and can be useful to exclude things like tests, mocks, and set-up scripts from the actual package distribution contents (using the top-levelexcludes
key). There are a few mandatorycompilerOptions
settings (you can change the rest as needed):
"compilerOptions": {
"declaration": true,
"declarationDir": "types",
"declarationMap": true,
"jsx": "preserve",
"module": "esnext",
"moduleResolution": "node",
"target": "esnext",
"outDir": "esnext"
}
Note that by default, tsc
will still emit output even if there are type errors
in the code. This is relevant for rebuilds within a --watch
setting, because
after the first successful build, tsc
will continue to emit transpiled JS
(which will be picked up by babel and webpack) regardless of any type errors;
the type errors will still be shown in the console. This can be useful for
exploratory coding and similar use-cases (and is the recommended configuration
for good developer experience), but you can opt-out of this behavior by adding
the noEmitOnError
option to the compilerOptions
. Normal (non-watch) builds
will always fail on type errors with or without this flag.
-
babel.config.js
in the root of the package. In order to ensure that ES module are transpiled for the dist build but left intact for the module build, there is a base configuration you will need (you can add transforms as needed):
module.exports = (api) => {
api.cache.using(() => process.env.NODE_ENV);
return {
presets: [
['@babel/preset-env', {
loose: true,
modules: !api.env('module') && 'auto',
}],
'@babel/preset-react',
'@babel/preset-typescript',
],
plugins: [
'@babel/plugin-transform-runtime',
],
};
};
-
webpack.config.js
in the root of the package. We'll be re-using the Babel config here via babel-loader, and here is the resulting minimal configuration you will need:
const { resolve } = require('path');
const webpack = require('webpack');
const nodeExternals = require('webpack-node-externals');
const { name: library } = require('./package.json');
module.exports = function(env = {}) {
return {
mode: 'production',
entry: resolve(__dirname, 'esnext', 'index'),
resolve: {
extensions: ['.js', '.jsx'],
},
externals: [nodeExternals(nodeExternalsOpts)],
module: {
rules: [
{
test: /\.jsx?$/,
use: 'babel-loader',
},
],
},
output: {
filename: 'index.js',
path: resolve(__dirname, 'dist'),
libraryTarget: 'commonjs2',
library,
},
};
};
If you are only supporting server builds, then you should add target: "node"
to your config.
-
src
directory should contain all code for packaging. As mentioned above, to prevent any unwanted files insrc
from being included in the final distributable package, use thetsconfig
exclude
key to keep files from entering the build process. -
package.json
will need a few entries after this is all done, making your package ready for publishing/distribution:
"main": "dist/index.js",
"module": "module/index.js",
"types": "types/index.d.ts",
"sideEffects": false,
"files": [
"dist/",
"module/",
"types/"
],
"browserslist": [
...
],
Use the browserslist values to
control which features you transpile and which you allow through (consumed by
Babel's preset-env
).
If you are only supporting server builds, then you should omit the module
key.
It is also common to include:
"scripts": {
"build": "build-package",
"build:watch": "build-package -w",
}