Publish a codebase as multiple packages.
When should I use this?
You should consolidate your packages into a monorepo when you have multiple related packages which:
- Are interdependent (eg. a component library where each component is a package and should always depend on the latest version of other component packages you maintain)
- Have shared tooling (eg. all packages use the same TypeScript config, ESLint config, Jest config)
There are many existing monorepo tools which help you do this (eg. Lerna, Bolt, Rush). The unique benefits that Package Splitter provides are:
- Zero boilerplate (eg. you don't need to define a full
package.json
for each package, never repeat yourself) - No need for custom tooling (eg. you don't need to maintain project references in TypeScript or run tools multiple times for different packages, it's one regular project so you use tools like normal, things like VSCode "jump to reference" work naturally over cross-package boundaries)
- Can easily be added to existing projects (eg. if you're splitting up a monolith, just add a few
package.json
files at package boundaries and you're done) - Super easy to setup (provide the path to a few directories and it Just Works
™️ )
In most cases a monorepo using Package Splitter is the easiest solution, however there are times when you may need to go with a traditional monorepo tool, such as when:
- You need different build tooling for different packages (eg. you have backend packages which are compiled by TypeScript and frontend packages which are compiled by Webpack and Babel)
In these cases you should probably use a tool like Lerna but keep in mind you can mix and match approaches depending on your needs (eg. have Lerna maintain a frontend package and a backend package, each with their own build tooling, then have these packages both use Package Splitter to further split them into many modules).
Usage
- Build a standard single project repository with the code for your packages all somewhere within the source directory.
- Create a
package.json
file for each subpackage and place them in each subpackage's source directory. Thesepackage.json
files can contain as much or as little details as you'd like. Nothing is required; anything not specified will be inferred according to the package inference rules. - Install Package Splitter with:
npm i package-splitter
oryarn add package-splitter
- To publish modules use:
package-splitter publish -s INPUT_PATH_FOR_SOURCE_FILES -b INPUT_PATH_FOR_BUILT_FILES -p OUTPUT_PATH_FOR_PUBLISH_FILES
All packages have now:
- Had their versions bumped (type of bump based on your Git commit message)
- Been published to npm
Default Configuration
For convenience there are a few behaviors configured by default. Read more about them here:
package.json
fields are inferred- The release type (patch, minor or major) of the version bump when publishing is determined by commit messages
Package Inference Rules
Below are the default values for package.json
files:
Field | Default Value | Example |
---|---|---|
name |
Path from the src directory to the package's directory. Name is converted to snake-case. |
If the package.json is at src/esky/lid/package.json , then the inferred package name will be esky-lid . |
version |
The latest version published on npm, or 0.0.1 if it is not published. |
|
dependencies |
Packages which are imported in the code are added to the list of dependencies. | If the package has a source file containing import x from 'some-module'; then some-module will be added to dependencies . |
engines |
The engines field of the parent package.json . |
If the root package.json has "engines": { "node": "12" } then a package will inherit the same engines value. |
type |
The type field of the parent package.json . |
If the root package.json has "type": "module" then a package will inherit the same type value. |
Dependencies
The dependencies
, peerDependencies
and optionalDependencies
fields require some extra explanation.
- All source files (
.js
,.jsx
,.ts
,.tsx
) within the package are scanned for imported packages (throughimport
statements, dynamicimport
statements andrequire
calls where the argument is a string literal) - Imported packages are added to the
dependencies
field whether or not thedependencies
field already exists in thepackage.json
- The versions of imported packages are retrieved from the nearest parent
package.json
file which contains the dependency in adependencies
,peerDependencies
oroptionalDependencies
field - If an imported dependency has been explicitly declared in
dependencies
,peerDependencies
oroptionalDependencies
, it will not be added todependencies
- If the version of a dependency is explicitly declared in
dependencies
,peerDependencies
oroptionalDependencies
asnull
(eg."dependencies": { "some-module": null }
) then the version will be replaced with the version from the nearest parentpackage.json
file
Versioning
When publishing a package, the following things happen when there is no version
field in its package.json
:
- The latest version number of the package is looked up on npm
- This is used as the inferred
version
of the package - The Git commit messages since the last publish for files within the package are collected
- If there have been no commits within the package since the last publish, the process ends here and the package is not published
- Collected messages are each assigned a release type depending on what they begin with:
-
release: MAJOR
- major -
feat:
- minor - Other - patch
-
- The inferred version of the package is bumped according to the greatest release type of the collected commit messages
- The package is published with this bumped inferred version