How to: An npm Package of React Components Written in TypeScript
Creating an npm package of React components written in TypeScript was a challenge, here's how I did it for react-modal-queue. While creating the package there were several goals I had in mind:
- Code written in TypeScript. I find TS helpful because as projects grow it's much easier to keep track of Objects' data and associated code while still allowing the flexibility of JavaScript when needed.
- Include TypeScript declaration files in the package to improve code completion support for consumers of the package, especially when using VS Code.
- The package needed to be consumable through: the ES2015
import
statement, alink
tag in HTML, and UNPKG to support online coding web sites like Codepen.
Prevent Duplicate Packages
One of the first problems I ran into during testing was duplicate copies of React; this happened because react-modal-queue imported React and so did the application. The solution was to make React and its related packages "peer dependencies" in react-modal-queue. This actually involved two changes. First moving "react" and "prop-types" so they were only under the peerDependencies
property of "package.json". Second marking them as externals
in the Webpack configuration:
externals: [{
"prop-types": {
commonjs: "prop-types",
commonjs2: "prop-types",
amd: "prop-types",
root: "PropTypes"
},
react: {
commonjs: "react",
commonjs2: "react",
amd: "react",
root: "React"
}
}]
Configuring tsc
When working with TypeScript files my preferred usage of tsc is simply to clean up the TypeScript syntax leaving only JavaScript code; later Babel will be used to transpile ES20XX code to ES3. tsc will also be used in a separate step to generate the declaration files (*.d.ts) that will be included in the package to define types. I configured tsc to remove TypeScript syntax elements in the tsconfig file by setting jsx
to "preserve" and module
and target
to the same module type (I used "esnext"). I removed declaration
(the need for declaration files will be specified on the command line). The output of the tsc clean up step is placed into a temporary directory named "tsc-temp." The declaration files are output to the "dist" directory.
Transpiling using Babel
The Babel package is used to transpile from the code generated by tsc to another ECMAScript version; this might be ES3 or you might target specific browser versions using the Babel preset-env's targets
property. For Babel 7 you must also set preset-env's modules
property to "false" otherwise modules with export default
followed by a function will result in a module with no exports. The inputs for Babel are the files in the "tsc-temp" directory that were created previously by tsc. I set the output location for Babel as the packages's "dist" directory. This is the directory that will be accessed when the package is used via an import
statement because the "package.json" main
property points to a file in "dist." Now if the application that consumes react-modal-queue is built using Webpack only the files that are actually used will be included in the application package file.
Generate a library file with Webpack
I also wanted to include a single file that could be used by other module systems, or no modules at all so I setup Webpack to output a library file that supports commonjs, AMD, and UMD. Another benefit of generating the library is that now the package can be used with UNPKG. UNPKG is especially useful because it makes npm packages available for use in online code editing sites like Codepen through a link
element in the head
of an HTML document. According to the UNPKG documentation it requires a UMD library to exist in a subdirectory of your package's root and the subdirectory must be named "umd". I altered the Webpack configuration so its entry point used the files generated by Babel in "dist" during the transpile step and output the library file to the "umd" directory.
entry: {
index: "./dist/index.js"
},
output: {
filename: `${_MODULE_NAME}${mode === "production" ? ".min" : ""}.js`,
globalObject: "(typeof self !== 'undefined' ? self : this)", // temporary workaround for https://github.com/webpack/webpack/issues/6642
library: { amd: _MODULE_NAME, commonjs: _MODULE_NAME, root: "ReactModalQueue" },
libraryTarget: "umd",
path: path.resolve(__dirname, "umd"),
umdNamedDefine: true
}
Define the package's files
Now that there were defined processes to create all the required files, "package.json" needed to be updated to include them. I simply created the files
property and included the two output directories: "files": ["dist", "umd"]
.
Building the package
To actually build the files that make up the package the following steps happen in order:
- Clean the "dist" and "umd" directories using rimraf.
- Convert from TypeScript to JavaScript using tsc.
- Generate TypeScript definition files using tsc.
- Transpile the converted files using Babel.
- Generate a debug version of the library.
- Generate a production version of the library.
The steps are accomplished using a script named "build" in "package.json." The "build" script is a combination of other scripts - each one accomplishes one of the tasks defined above. There is also a "debug" task that can be used during development to import the package into a host application using a link.
"scripts": {
"babel": "babel tsc-temp --out-dir dist",
"build": "npm run clean && npm run tsc && npm run tsc-declaration && npm run babel && npm run lib-debug && npm run lib-production",
"clean": "rimraf ./dist ./tsc-temp ./umd",
"debug": "npm run clean && npm run tsc && npm run tsc-declaration && npm run babel && npm link",
"tsc": "tsc --outDir tsc-temp",
"tsc-declaration": "tsc --declaration --emitDeclarationOnly --outDir dist",
"test": "echo \"No tests.\"",
"lib-debug": "webpack --config webpack-lib.config.js",
"lib-production": "webpack --config webpack-lib.config.js --env.NODE_ENV=production"
}
Publishing and debugging
When it's time to publish the package I like using np which helps minimize errors. It also automates several tasks such as choosing a semver string, adding tags in git, and creating release notes in Github.
For integration debugging the package can be imported into a host using npm link. The first step is to run npm link
from the package's root directory (where "package.json" is located). The second step is to move into the host project's root directory and run the command again but include the package name at the end: npm link react-modal-queue
. The downside is that when your package is rebuilt the link will be broken and both steps will need to be run again (creating the link is done automatically by the "debug" script above). There are other ways to import your package, they all have benefits and drawbacks.
So now go ahead and create npm packages for React using TypeScript!