rafgraph / rollpkg

Zero-config build tool to create packages with Rollup and TypeScript
MIT License
175 stars 8 forks source link
npm-package package-development react rollup typescript zero-configuration

Rollpkg

🌎 Zero-config build tool to create packages with Rollup and TypeScript (supports JavaScript too).

🌏 Rollpkg creates esm, cjs and umd builds for development and production, and fully supports tree shaking.

🌍 Default configs are provided for TypeScript, Prettier, ESLint, and Jest for a complete zero decision setup.


gif of rollpkg in action


For an example package see rollpkg-example-package: package repo, and built and published code.

Used by: react-interactive, detect-it, event-from, react-router-hash-link, fscreen, and others.


Setup ⚡️ package.json ⚡️ Build options ⚡️ Default configs ⚡️ 🚫 TS type pollution ⚡️ FAQ ⚡️ Compare to TSdx


Setup rollpkg

Prerequisites

Initialize with git and npm. Note that the docs use npm, but it works just as well with yarn.

mkdir <package-name>
cd <package-name>
git init
npm init

Install rollpkg and typescript

TypeScript is a peerDependency of Rollpkg, and Rollpkg will use the version of TS that you install for its builds.

npm install --save-dev rollpkg typescript

Add main, module, types, and sideEffects fields to package.json

Rollpkg uses a convention over configuration approach, so the values in package.json need to be exactly as listed below, just fill in your <package-name> and you’re good to go. Note that for scoped packages where "name": "@scope/<package-name>", use <scope-package-name> for the main and module fields.

{
  "name": "<package-name>",
  ...
  "main": "dist/<package-name>.cjs.js",
  "module": "dist/<package-name>.esm.js",
  "types": "dist/index.d.ts",
  "sideEffects": false | true,
  ...
}

Note about sideEffects: most packages should set "sideEffects": false to fully enable tree shaking. A side effect is code that effects the global space when the script is run even if the import is never used, for example a polyfill that automatically polyfills a feature when the script is run would set sideEffects: true. For more info see the Webpack docs (note that Rollpkg doesn't support an array of filenames containing side effects like Webpack).

Add build, watch and prepublishOnly scripts to package.json

"scripts": {
  "build": "rollpkg build",
  "watch": "rollpkg watch",
  "prepublishOnly": "npm run build"
}

Create a tsconfig.json file

It is recommended to extend the Rollpkg tsconfig and add your own options after extending it (however extending the Rollpkg tsconfig is not a requirement).

// tsconfig.json
{
  "extends": "rollpkg/configs/tsconfig.json"
}

Note: you can specify a custom path or name for your tsconfig using the --tsconfig command line option.

Create an index.ts or index.tsx entry file in the src folder

This entry file is required by Rollpkg and it is the only file that has to be TypeScript (the rest of your source files can be JavaScript if you'd like). Note that you can write your entire code in index.ts or index.tsx if you only need one file.

package-name
├─node_modules
├─src
│  ├─index.ts | index.tsx
│  └─additional source files
├─.gitignore
├─package-lock.json
├─package.json
├─README.md
└─tsconfig.json

Add dist to .gitignore

Rollpkg creates a dist folder for the build files, and this shouldn't be checked into version control.

# .gitignore file
node_modules
dist

Add a files array with dist to package.json

This lets npm know to include the dist folder when you publish your package.

"files": [
  "dist"
]

Publish when ready

npm version patch | minor | major
npm publish

That’s it!

No complex options to understand or insignificant decisions to make, just a sensible convention for building packages with Rollup and TypeScript. This is what you get with Rollpkg:


Fully setup example package.json

This includes the optional Rollpkg default configs. Also see rollpkg-example-package for a fully set up example package.

{
  "name": "<package-name>",
  "version": "0.0.0",
  "description": "Some awesome package",
  "main": "dist/<package-name>.cjs.js",
  "module": "dist/<package-name>.esm.js",
  "types": "dist/index.d.ts",
  "sideEffects": false,
  "scripts": {
    "build": "rollpkg build",
    "watch": "rollpkg watch",
    "prepublishOnly": "npm run lint && npm test && npm run build",
    "lint": "eslint src",
    "test": "jest",
    "test:watch": "jest --watchAll",
    "coverage": "npx live-server coverage/lcov-report"
  },
  "files": ["dist"],
  "devDependencies": {
    "rollpkg": "^0.5.5",
    "typescript": "^4.2.3"
  },
  "prettier": "rollpkg/configs/prettier.json",
  "eslintConfig": {
    "extends": ["./node_modules/rollpkg/configs/eslint"]
  },
  "jest": {
    "preset": "rollpkg"
  }
}

Build options and info

Rollpkg uses the TypeScript compiler to transform your code to ES2018 (default) and Rollup to create esm, cjs and umd builds. The TypeScript compiler uses your tsconfig.json with a few added defaults to prevent global type pollution, create source maps, and generate *.d.ts type files.


rollpkg build

Options

ES Modules esm build

CommonJS cjs build

Universal Module Definition umd build


rollpkg watch


sideEffects: boolean


Dev mode code

Dev mode code is code that will only run in development and will be removed from production builds. You can use process.env.NODE_ENV or __DEV__ to gate dev mode code and Rollpkg will remove it from production builds:

if (process.env.NODE_ENV !== 'production') {
  // dev mode code here
}

if (__DEV__) {
  // dev mode code here
}

Note that __DEV__ is shorthand for process.env.NODE_ENV !== 'production' and Rollpkg will transform __DEV__ into process.env.NODE_ENV !== 'production' before proceeding to create development and production builds.


Setting target ECMAScript version in tsconfig

Note that for most projects extending the Rollpkg tsconfig is all that's required and you can ignore this section.

The Rollpkg default is to compile your code to ES2018 which is supported by all modern browsers. If you need to support legacy browsers, then you probably want to compile your code to ES5.

To control how your code is compiled and what JS APIs are available at runtime use the target and lib options in your tsconfig. The target option specifies the ECMAScript version that your code is compiled to (ES5, ES2018, etc). The lib option specifies the JS APIs that will be available at runtime, which is needed for using JS APIs that can't be compiled to the specified target. For example, array.includes and the Promise API cannot be compiled to ES5 but you may find it necessary to use them in your code (note that all JS APIs in your code will need to be available in the browser, either supported natively or provided by a polyfill).

For example, let's say your target is ES5 and you need to use the Promise API, your tsconfig.json would look like this:

Note that when using the lib option you need to specify all available JS APIs, including the base ES5 APIs and DOM APIs.

// example tsconfig.json to use ES5 and the Promise API
{
  "extends": "rollpkg/configs/tsconfig.json",
  "compilerOptions": {
    "target": "ES5",
    "lib": ["DOM", "ES5", "ES2015.Promise"]
  }
}

Using default configs (optional)

Rollpkg provides sensible defaults for common configs that can be used for a complete zero decision setup. You can also add you own overrides to the defaults if needed. Default configs are provided for TypeScript, Prettier, ESLint, and Jest (the configs are setup to work with TypeScript, JavaScript, and React). Use of these configs is optional and while they include support for React, using React is not a requirement (they work just fine without React).


TypeScript config

It is recommended to extend the Rollpkg tsconfig and add your own options after extending it (however extending the Rollpkg tsconfig is not a requirement).

// tsconfig.json
{
  "extends": "rollpkg/configs/tsconfig.json",
  // add your own options, etc...
}

Prettier config

If you want to use Prettier (recommended) you can extend the config provided by Rollpkg. There is no need to install Prettier as it is included with Rollpkg (alternatively if you need to use a specific version of Prettier, you can install it and that version will be used). In package.json add:

"prettier": "rollpkg/configs/prettier.json"

You may also want to set up a pre-commit hook using pre-commit or husky, and lint-staged so any changes are auto-formatted before being committed. See the rollpkg-example-package for an example pre-commit hook setup, as well as the Prettier docs for Git hooks.


ESLint config

If you want to use ESLint (recommended) you can extend the config provided by Rollpkg. It includes support for TypeScript, JavaScript, React, Prettier, and Jest. The provided ESLint config mostly just extends the recommended defaults for each plugin. There is no need to install ESLint or specific plugins as they are included with Rollpkg (alternatively if you need to use a specific version of ESLint or plugin, you can install it and that version will be used). In package.json add:

Note that the path includes ./node_modules/..., this is because in order for ESLint to resolve extends it requires either a path to the config, or for the config to be published in its own package named eslint-config-..., which may happen at some point, but for now it will remain a part of Rollpkg for easy development.

"eslintConfig": {
  "extends": ["./node_modules/rollpkg/configs/eslint"]
}

It is also recommended to add a lint script to package.json (the eslint src command tells ESLint to lint the src folder). As well as add npm run lint to the prepublishOnly script so your code is linted before publishing.

"scripts": {
  ...
  "prepublishOnly": "npm run lint && npm test && npm run build",
  "lint": "eslint src"
}

Jest config

If you want to use Jest (recommended) you can use the preset provided by Rollpkg. The preset uses ts-jest for a seamless and fully typed checked TypeScript testing experience. There is no need to install Jest as it is included with Rollpkg (alternatively if you need to use a specific version of Jest, you can install it and that version will be used). In package.json add:

"jest": {
  "preset": "rollpkg"
}

It is also recommended to add test, test:watch and coverage scripts to package.json (the coverage script will open the coverage report in your browser). As well as add npm test to the prepublishOnly script so tests are run before publishing.

"scripts": {
  ...
  "prepublishOnly": "npm run lint && npm test && npm run build",
  "lint": "eslint src",
  "test": "jest",
  "test:watch": "jest --watchAll",
  "coverage": "npx live-server coverage/lcov-report"
}

The Rollpkg Jest config will automatically generate a code coverage report when Jest is run and save it in the coverage folder, which shouldn't be checked into version control, so also add coverage to .gitignore.

# .gitignore
node_modules
dist
coverage

Rollpkg's approach to TypeScript's global type pollution

TypeScript's default behavior is to include all of the types in your node_modules/@types folder as part of the global scope. This allows you to use things like process.env.NODE_ENV and Jest's test(...) or expect(...) without causing a type error. However, it also adds a significant amount of global type pollution that you might not realize is there. This pollution can make it seem like you are writing type safe code that safely compiles to your target ECMAScript version, but in reality you are using APIs that won't be available at runtime. For example, your node_modules/@types folder probably includes node's types (they are required by Jest and others), so the TypeScript compiler thinks your code will have access to all of Node's APIs at runtime. If your compilation target is set to ES5, using APIs like array.includes, Map, Set or Promise won't produce a TypeScript error despite the fact that none of these can be compiled to ES5 🤦‍♀️ (the TypeScript compiler assumes your code will have access to these APIs at runtime).

TypeScript does provide a types compiler option for you to explicitly specify which packages are included in the global scope (note that these will be in addition to any imports in your code, for example if you import * as React from 'react' then react's types will always be included). However, failing to include types that are needed in development but won't be available at runtime (e.g. Node's process.env.NODE_ENV and Jest's test) will cause a TypeScript error in development 🤦‍♂️ Also you can only specify entire packages so it is not possible to only include the process.env type but exclude the rest of Node's types.

Ideally TypeScript would allow overrides based on file types like ESLint does, but until that happens the most widely used solution is multiple tsconfigs (e.g. tsconfig.build.json, tsconfig.test.json, etc) that include or exclude specific files and types (or alternatively ignoring the issue and accepting the global type pollution). Note that VS Code (and other editors) can only use one tsconfig.json per file tree section to provide type feedback in the UI, so when using multiple tsconfigs it is typical to have a tsconfig.json file that doesn't restrict global types or files so you can benefit from UI based type feedback without unwanted type errors in the UI. That is, even with multiple tsconfigs, global type pollution is unavoidable in the editor UI.

So how does Rollpkg handle global type pollution?


FAQ

Rollpkg compared to TSdx

Some background: I started creating npm packages in 2016, and I've mostly used my own build setup. Recently I started looking for a build tool that would provide a convention over configuration approach to reduce the number of decisions I needed to make. I used Microbundle for a bit, and experimented with TSdx, but neither was a good fit, so I created Rollpkg (this is essentially the origin story behind all of my open source projects, I just want the thing to exist so I can use it in another project 😆).

Without taking anything away from TSdx (I think it's generally a solid tool that gets a lot of things right), here are some areas where Rollpkg takes a different approach (as of TSdx v0.14.1):


Prior art