A smart build tool for libraries
DISCLAIMER:
While preconstruct is pretty stable in terms of bugs and is used in big projects, a lot of things are still in flux and will change so please be aware of that. We're in 0.0.x versions for a reason.
Generating small, performant bundles and making consumer bundlers use the right bundles shouldn't have to be difficult.
yarn add --dev preconstruct
yarn preconstruct init
yarn preconstruct buildpreconstruct generates bundles for NPM packages with different module formats and enforces that consumer bundlers like webpack and Node can import those packages.
preconstruct generates development and production CommonJS bundles so that you can have process.env.NODE_ENV checks to have helpful warnings and errors without slowing down production along with building browser specific bundles when you use typeof window so that you can have Node.js specific code which is dead code eliminated from browser bundles.
preconstruct strictly enforces that the relevant fields in your package.json which tell Node and bundlers like webpack where to look for your bundles are correct, so you don't have to worry about them, and you'll always be publishing working versions of your packages. preconstruct can also fix most validation problems automatically by running preconstruct fix
Rather than having to configure everything, preconstruct infers how to build your project. For example, if you have a module field in your package.json, preconstruct creates an ESM bundle, but if you don't have a module field in your package.json, preconstruct won't create an ESM bundle.
preconstruct is built with monorepo's in mind, rather than having to manage and build packages on an individual level, preconstruct lets you manage and build all of your packages together as a single project.
preconstruct lets you create entrypoints beyond the standard entrypoint of package-name so a single package can have other entrypoints like package-name/some-cool-thing and my-package/another-cool-thing. These entrypoints still have bundles in multiple module formats, so Node and consumer bundlers can still get the right module format for them.
preconstruct exports aliases which you can use with tools like Jest and webpack so you can import from entrypoints like consumers do but rather than importing a dist file, you'll be importing the source file.
yarn add --dev preconstructpreconstruct initpreconstruct init asks questions about your project and how it should be built and creates the appropriate config in your package.json.
preconstruct buildpreconstruct build creates the bundles for your project.
preconstruct watchpreconstruct watch is similar to preconstruct build except instead of doing a single build, it starts a watcher and rebuilds the project whenever there are changes.
preconstruct dev creates files and symlinks in the dist folders of entrypoints which map to the appropriate source file so that the package can be imported from Node and in bundlers and the source file will be imported.
It's best to use the dev command in a postinstall hook so that people don't have to worry about running it.
{
"scripts": {
"postinstall": "preconstruct dev"
}
}Note:
preconstruct devalso needs to be run whenever the project's packages or entrypoints change.
preconstruct fixpreconstruct fix fixes invalid configuration in a project. It also throws an error when there is an issue which cannot be resolved automatically.
preconstruct validatepreconstruct validate checks that the project, packages and entrypoints are all valid.
Note: A build can still fail even if validate doesn't fail because there are some checks which can only happen during
preconstruct accepts configuration at three different configuration points; projects, packages and entrypoints. These configuration points can be represented by one package.json or by 20 package.jsons, it depends on the requirements of a specific project. For example, in a single package repo with one entrypoint, it would be represented by a single package.json.
Projects roughly map 1:1 with a version control repository. They specify global configuration that applies to all builds.
Array<string>
packages is an array of globs which specify which packages should be built with preconstruct.
Note: this is the default value, if it's what you want, you don't need to specify it.
{
"preconstruct": {
"packages": ["."]
}
}{
"preconstruct": {
"packages": ["packages/*"]
}
}{ [packageName: string]: (umdName: string) }
globals specifies the UMD names of peerDependencies since peerDependencies aren't bundled in UMD builds. You shouldn't specify this option manually, preconstruct will prompt you for the UMD name of a package when it's necessary.
Note: this is the default value, if it's what you want, you don't need to specify it.
{
"preconstruct": {
"globals": {}
}
}{
"preconstruct": {
"globals": {
"react": "React",
"react-dom": "ReactDOM"
}
}
}Packages map 1:1 with npm packages. Along with specifying the entrypoints option described below, packages are also responsible for specifying dependencies which is necessary for bundling UMD bundles and ensuring that packages will have all of their required dependencies when installed through npm.
Array<string>
entrypoints is an array of globs which specify the entrypoints which consumers of your package should be able to import.
Note: this is the default value, if it's what you want, you don't need to specify it.
{
"preconstruct": {
"entrypoints": ["."]
}
}{
"preconstruct": {
"entrypoints": [".", "other-entrypoint"]
}
}Entrypoints are the lowest level configuration point and describe a set of bundles for a particular entrypoint.
string
sources specifies the source file to use for a given entrypoint. It's resolved relative to the package.json where it's specified.
Note: this is the default value, if it's what you want, you don't need to specify it.
{
"preconstruct": {
"source": "src/index"
}
}{
"preconstruct": {
"source": "modules/index"
}
}Build types specify what types of bundles preconstruct should build. They are specified via the package.json fields which Node and bundlers like webpack look at to find bundles. It's important to note that all of the entrypoints in a package must have the same build types, this is necessary to ensure that common dependencies between entrypoints aren't duplicated.
The main field specifies a CommonJS build. It is the only build type which is required. This bundle will work in Node and can work in bundlers like webpack but a ES Module build is recommended for bundlers like webpack.
Example:
{
"main": "dist/my-package.cjs.js"
}The module field specifies an ES Module build. This bundle is what bundlers like webpack will use.
Example:
{
"module": "dist/my-package.esm.js"
}The umd:main field specifies a UMD build. This bundle can be used directly in a browser with a <script> tag.
Example:
{
"umd:main": "dist/my-package.umd.min.js"
}Balancing between having minimal to no configuration and being able to support everyone's use case is a really hard problem. preconstruct tries to have good defaults to support lots of common use cases but it won't support everything. We want to make preconstruct work really well for most use cases rather than being okay for all use cases.
- microbundle was a huge inspiration for this! ❤️
- rollup - rollup has done the really hard stuff that makes preconstruct possible!
- bolt - lots of utils and things in this project were inspired by things in bolt
- all the people who wrote all the dependencies for this project!