Library bundler
- Supports multiple entrypoints
- Supports Typescript, Javascript, React, etc
- Builds a version for apps in dev mode, and another one with apps in prod mode.
- Builds an ESModules version for better tree-shaking
- Supports the new JSX transform (and correctly switches between
react-jsx
andreact-jsxdev
)
-
Install it with your favourite package manager:
pnpm add -D @alduino/pkg-lib
-
Specify what your package’s entrypoints are if you haven’t already, in your package.json:
{ main: "dist/index.js", module: "dist/index.mjs", typings: "dist/index.d.ts" }
-
Add
/.js
and/.mjs
to your gitignore, to ignore bundled entrypoints -
Run
pkg-lib build
If you are using Typescript, there is some additional steps before you can run the build command:
-
Add
/.d.ts
to your gitignore -
Set
compilerOptions.isolatedModules
totrue
in your tsconfig.json -
Add
**/node_modules
and**/.*/
to theexclude
option in your tsconfig.json -
Double check that the
include
option includes all entrypoints and source code (you will get weird errors if it doesn’t) -
Run
pkg-lib build
-c, --config
: The path to the configuration file. Defaults to.pkglibrc
.--no-dev
: Disables__DEV__
replacement--no-invariant
: Disablesinvariant
optimisation--no-warning
: Disableswarning
optimisation--entrypoints
: Glob file paths, or explicitly named entrypoint, separated by,
(no whitespace). For explicit entrypoint names, use[name]=[path]
.
Other than this, you can use all the configuration values in the CLI too. This will override all other configuration.
See pkg-lib build --help
for more info.
Watches the source files for changes, and recompiles the minimum needed to get the output up-to-date.
Takes the same parameters as build
.
If you are using Typescript, you will notice the Typescript features build stage can be very slow. pkg-lib attempts to work around this by using incremental builds, which reduces subsequent .d.ts builds from 5s to 1s in the example project. API Extractor should also have a speedup from this, but from tests with the example project it seems to not make much difference.
Basically, this is because tsc
, the compiler that pkg-lib uses to build your .d.ts files, is really slow. We’re
watching a project, stc - an attempt to rewrite tsc in Rust, which will hopefully
speed it up a lot.
If you have any ideas how to make tsc
any faster, please tell me
in this issue.
For now, the actual code is emitted before tsc runs so you should still be able to work quickly.
pkg-lib reads its config from a .pkglibrc
JSON file. Here’s the big list of every configuration option.
entrypoint
: The package’s root, will be compiled as an entrypoint named bymainEntryOut
. Set tofalse
to disable automatic detection. Defaults asrc/index
that is ajs
,ts
,cjs
,mjs
,ejs
, oresm
file.entrypoints
: A list of named entrypoints. See here for the format of the value. Defaults to any files with the above extensions in./entry
or the root project directory.typings
: Output for Typescript typings. Defaults to[entrypoint].d.ts
.mainEntry
: The output file of the main entrypoint, with no extension. Files will be generated as defined in the various entrypoint output options. Defaults toindex
.cjsOut
: The output file for library consumer entry. Defaults to[entrypoint].js
.cjsDevOut
: The output file for a development build. Defaults todist/[entrypoint].dev.js
.cjsProdOut
: The output file for a production build. Defaults todist/[entrypoint].prod.min.js
.esmOut
: The output file for an ESModule build. Defaults to[entrypoint].mjs
.platform
: The target platform, one ofneutral
,browser
ornode
. Defaults toneutral
.target
: The JS syntax and std libs to target (e.g.node12
,es2019
). Defaults toes6
.dev
: Enable__DEV__
andprocess.env.NODE_ENV
. Defaults totrue
.invariant
: Disables invariant replacing when false, or changes the function name. Use an array of identifiers for multiple invariant functions. Defaults toinvariant
.warning
: Disables warning replacing when false, or changes the function name. Use an array of identifiers for multiple warning functions. Defaults towarning
.docsDir
: Output directory for documentation files. They will be put in[docsDir]/[unscopedPackageName].md
. Disabled by default.
You can also set some of these in your package.json
:
source
sets `entrypointmain
setsmainEntry
to be a matching entrypoint name forcjsOut
module
setsmainEntry
ifmain
hasn’t already, to be a matching entrypoint name foresmOut
typings
setsmainEntry
if neithermain
normodule
have already, to be a matching entrypoint name fortypings
docs
setsdocsDir
Run pkg-lib build
to run the build. You can set a script for this in your package.json
:
{
"scripts": {
"build": "pkg-lib build"
}
}
pkg-lib has support for multiple entrypoints. Note that this is separate from the exports
field in the package.json
,
as that field is only supported by ESModules (which Typescript does not support at the moment).
If you don’t specify any custom entrypoints, pkg-lib will automatically add multiple entrypoints for any js
, ts
, cjs
, mjs
, ejs
, or esm
in the entry
directory, and any files with those extensions that can’t match one of
the build result paths in the root. In fact, no matter what glob or paths you specify, pkg-lib will refuse to resolve
them as entrypoints if they could match a build result, as otherwise the file would get overwritten when you build your
code. Please note that this check may not be perfect, so you shouldn’t rely on it (
see the known issues).
This means, by default, .js
and .mjs
files in the root directory would not be recognised as entrypoints, as they
would match the default cjsOut
setting of [entrypoint].js
and esmOut
setting of [entrypoint].mjs
. You can either
put these in the entry
directory, or change the cjsOut
and/or esmOut
options to not match them (e.g. change
cjsOut
to use the .cjs
extension).
Here’s an example directory structure:
(project root)
├── src
│ └── index.ts
├── utils.ts
├── server.ts
└── package.json
The entrypoints would be src/index.ts
(the package root), utils.ts
, and server.ts
. These would build to individual
bundles, so then you could import them as follows:
import pkg from "package"; // imports /src/index.ts
import utils from "package/utils"; // imports /utils.ts
import server from "package/server"; // imports /server.ts
You can customise the entrypoints or where they are searched for, by using the entrypoints
key in the config. This can
have a few types of values:
- a glob string, matching any file to be used as an entrypoint, e.g.
"./entrypoints/*.ts"
. The file’s name (without the extension) will be used as the name of the entrypoint. - an array of file paths, globs (see above), and/or entrypoint objects (see below). For any files in this list, the file’s name (without the extension) will be used as the name of the entrypoint.
- an object, where each key is the name of an entrypoint, and the value is the path to the entrypoint source file.
(see fast-glob's documentation for more info on glob syntax)
For example, with a config of ["./entrypoints/*.ts", "./server.ts", {utils: "./utils/index.ts"}]
, the entrypoints
could be:
test => ./entrypoints/test.ts
foo => ./entrypoints/foo.ts
server => ./server.ts
utils => ./utils/index.ts
Two entrypoints must not have the same name. If they do, an error will be thrown.
- The check does not include subdirectories (as this would prevent any
.js
files from being an entrypoint, by default). This means that, if an entrypoint name is explicitly specified to have a subdirectory (i.e. the name has a slash, e.g.foo/bar
) and that directory points to the same location as the source file, that file might get overwritten. No entrypoint names can have subdirectories without explicitly specifying them (as the file’s basename is used, which doesn’t include its directory), so normally this should be a nonissue.
At the moment we use API Extractor to bundle typings into a single .d.ts
file for each
entrypoint. API Extractor currently doesn’t support multiple entrypoints, so we have to run it multiple times - once for
each entrypoint.
Because each .d.ts
file has completely separate typings (even for types that should be shared), if you have any
user-accessible classes with private properties, Typescript will not let the user use instances across multiple
endpoints. For now, make sure none of your exported classes have private fields, or create a wrapper type for them that
doesn’t. See
The Typescript stage of the builds may also get significantly slower, as it has to run the whole Typescript compiler for every entrypoint.
See these issues for more information.
pkg-lib can generate documentation for you if you are using Typescript, based on your tsdoc comments. By default it generates a simple markdown file listing each export and their properties, arguments, return types, and the summary and remarks you give them.
Documentation generation is disabled by default, to enable it set docs
in your package.json
or docsDir
in the
config to an output directory where the documentation will be generated. If you use the built-in documentation
generator, a file will be created in this directory named after your library’s name, without a scope.
You can override the built-in documentation generator with a custom one by creating a file in your project directory
called pkglib.documenter.js
(it can also be a .mjs
or .ts
file). This file is bundled into a temporary file before
it is run using the same settings as the normal bundling (as a development build), so you can use features
like invariant
, although Typescript types will not be checked.
The custom documentation generation API is very simple, and has only two concepts: hooks and context. To use these,
import their functions from @alduino/pkg-lib/docgen
. This file has Typescript typings too.
Each hook is called as a new child process of pkg-lib
, so you can’t save values to be used across hooks like you
normally would (as a general rule of thumb, don’t use any global variables inside this file).
Instead, use the getContext()
and setContext()
functions. If you call setContext(someValue)
, the next time you
call getContext()
(even in another hook) that value will be returned. You can use this to store any serialisable
data (functions and symbols are not supported), e.g. to make a table of contents.
-
The default value returned from
getContext()
when you haven’t calledsetContext()
yet isnull
. -
Setting the context to
undefined
will actually set it tonull
. -
Don’t edit the object passed into
setContext
or returned fromgetContext
without callingsetContext
with it again. Due to the implementation of these functions, these edits will change the value in the current hook, but it will not persist to others.
To run your code on a hook, use the hook(name, callback)
function:
const {hook} = require("@alduino/pkg-lib/docgen");
hook("name", arg => {
console.log("Called on the `name` hook with some data:", arg);
});
There is currently three hooks supplied:
This hook is called for each documentation file (currently only once). You need to write the output file yourself.
For an example implementation, see here and here.
It is passed some values as an object in the first parameter:
fileName: string
: The name that the file you create should be called, without an extensionoutputDirectory: string
: The directory that the file should be put insource: ApiPackage
: Information about each export. See @microsoft/api-extractor-model.
This hook is called before all other hooks. It is not passed any information.
This hook is called after all other hooks. It is not passed any information.
If you are going to generate a separate table of contents file, this is the place to do it - you can save information
about each file in their doc
hooks, then read it here and create the file.
In your code, you can call invariant
to make an assertion about some state:
invariant(5 + 5 > 10, "Maths has stopped working");
pkg-lib converts calls to invariant
to not have a message in the production output, to reduce code size. Think of it
like this:
if (process.env.NODE_ENV === "production") {
invariant(5 + 5 > 10);
} else {
invariant(5 + 5 > 10, "Maths has stopped working");
}
There are some significant differences however, that could change how your code works:
- The check is done before calling
invariant
, so that it’s only called if it needs to be. This looks more likeif (!(5 + 5 > 10)) invariant(false)
. - The result of the check will be returned as the result of the expression. With
foo = invariant("test")
, foo will be set to"test"
if it is truthy. To combat this, pkg-lib will prevent you from callinginvariant
as a part of an expression. If you want to disable this (which is not recommended), setrecommendedExprCheck
in the config tofalse
.
If you do not want to use invariant
, disable it by setting invariant
in the config to false
.
You can change what names pkg-lib uses for invariant
functions by setting the invariant
config value to the name of
the function you want to use. If there are multiple, you can set it to an array too.
Build output
5 + 5 > 10 || invariant(false, "Maths has stopped existing!");
5 + 5 > 10 || t(!1);
5 + 5 > 10 ||
(process.env.NODE_ENV !== "production"
? invariant(false, "Maths has stopped existing!")
: invariant(false));
pkg-lib doesn’t supply an invariant
function, you need to create or import one yourself. Make sure it has the
signature invariant(check: boolean, message?: string): void
.
We recommend tiny-invariant
.
In your code, you can call warning
to log a warning message to the console if a check fails:
warning(5 + 5 > 10, "The universe is about to collapse");
pkg-lib removes calls to warning
in production builds:
if (process.env.NODE_ENV !== "production") {
warning(5 + 5 > 10, "The universe is about to collapse");
}
There are some significant differences however, that could change how your code works:
- The check is done before calling
warning
, so that it’s only called if it needs to be. This looks more likeif (!(5 + 5 > 10)) warning(false, "The universe is about to collapse")
. - The result of the check will be returned as the result of the expression. With
foo = warning("test")
, foo will be set to"test"
if it is truthy. To combat this, pkg-lib will prevent you from callingwarning
as a part of an expression. If you want to disable this (which is not recommended), setrecommendedExprCheck
in the config tofalse
.
If you do not want to use warning
, disable it by setting warning
in the config to false
.
You can change what names pkg-lib uses for warning
functions by setting the warning
config value to the name of the
function you want to use. If there are multiple, you can set it to an array too.
Build output
5 + 5 > 10 || warning(false, "The universe is about to collapse");
No code is generated for production builds.
(5 + 5 > 10 && process.env.NODE_ENV !== "production") || warning(false, "The universe is about to collapse");
pkg-lib doesn’t supply a warning
function, you need to create or import one yourself. Make sure it has the
signature warning(check: boolean, message: string): void
.
We recommend tiny-warning
.