- single dependency linting, bundling, testing and packaging for TypeScript projects
- supports both monorepo with multiple packages and single package repos
- minimum configuration required, driven by
package.json
- advanced configuration via TypeScript scripts with auto-completion
- ESM by default
Have a look at example packages in the playground.
Repka allows you to quickly setup a TypeScript project with linting, bundling,
testing and packaging. All you need is a package.json
file and a single
dependency @repka-kit/ts
.
If you are setting up a project from scratch:
npx --package @repka-kit/[email protected] repka init
If you are adding repka
to an existing project, you can use the same command,
or just add it as a dependency and follow your instincts:
npm add -D @repka-kit/[email protected]
After that following dependencies become available:
tsc
tsx
eslint
jest
dts-bundle-generator
prettier
repka
repka
encapsulates configurations for all the tools and automatically
initializes them based on the package.json
file in the project root and your
workspaces configuration.
Main benefit is that we can then update essential dev dependencies in this repository without having to change much in the packages that use this repository.
Another benefit is consistency across repositories.
Repka supports both monorepo and single package repositories. It is designed in a way that you can start with a single package repository and then easily migrate to a monorepo, if needed.
When building a repository, you can put all source code for every package in the
src
directory and have a package.json
, tsconfig.json
and possibly other
config files (if we want to override default settings or eslint rules) next to
it:
src/
index.ts
tsconfig.json
.eslintrc.js
package.json
The contents of the tsconfig.json
will be generated by the init
command and
do not need to change if all of the source files are within the src
directory.
{
"extends": "@repka-kit/ts/configs/tsconfig.base.json",
"compilerOptions": {
"outDir": ".tsc-out",
"tsBuildInfoFile": ".tsc-out/.tsbuildinfo"
},
"include": ["src"]
}
The contents of the package.json
for an app package can also be quite typical:
{
"name": "@playground/todo-list-store",
"version": "0.0.0-development",
"private": true,
"type": "module",
"exports": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"build": "repka build:node",
"lint": "repka lint",
"test": "repka test"
},
"dependencies": {},
"devDependencies": {
"@repka-kit/ts": "workspace:*"
}
}
There are a few things to note here:
-
@repka-kit/ts
is a dev dependency, it is recommended to put it as a workspace dependency in a monorepo - this way all packages will use the same version ofrepka
and it will be easier to update it. -
The
exports
field only points to TypeScript files. There is literally no need for any JavaScript files in the repository. This includes any files declared in thebin
field (more on that below).repka
will usetsx
where required to run TypeScript files. -
The
type
ismodule
, while we could supportcjs
output, it is really not a good idea. -
repka lint
will runeslint
andtsc
and run them in parallel -
repka test
will runjest
In case, when we have multiple packages can be like so:
packages/
package1/
src/
index.ts
tsconfig.json
package.json
package2/
src/
index.ts
tsconfig.json
package.json
package.json
tsconfig.json
.eslintrc.js
Depending on the package manager the way workspaces are configured will be
different. For npm
it will be:
{
"workspaces": ["packages/*"]
}
For pnpm
:
# ./pnpm-workspace.yaml
packages:
- 'packages/*'
Refer to the documentation of your package manager for more details.
repka
encourages you to reference TypeScript files directly, so there is no
need to build your packages. You can reference one package in another package
via package.json
dependencies
field, install
the repo to symlink packages
using your package manager of choice (pnpm
recommended) and finally, start
using it. It just works.
The fact that you don't need to build your packages while developing them is quite awesome. It allows you to iterate faster and not worry about building the right dependencies before testing them.
How is this possible?
Well, we actually have two package.json
files. One is the original file that
you use for development, another is generated by repka
before you publish the
package. This allows you to use TypeScript directly in your packages during
development and not worry about what happens to those entries before you
publish.
Then, tsc
kind of just works with TypeScript files. It is able to load
TypeScript files on the fly while type-checking. Because repka
uses
"moduleResolution": "bundler"
there is no need to specify any extensions when
importing files.
For cases when you actually need to run code, repka
encourages you to use
tsx
, for example if you have a "bin"
field in your package.json
:
{
"bin": {
"cli": "./src/bin/cli.ts"
}
}
You will be asked to add a shebang to the file:
#!/usr/bin/env tsx
And then you can run it directly:
pnpm run cli
Another benefit of having separate package.json
for publishing is that we can
optimize list of dependencies and exclude dependencies which we already bundled
into the package.
Now imagine that we have a monorepo with a lot of packages or a repository with a single package. We can run exactly same command to lint every package at the root of the repo:
pnpm eslint
Which will run eslint
on entire repository.
To also check for TypeScript issues we can use:
pnpm repka lint
Which will run tsc
and eslint
in parallel.
Or we can parallelize it via pnpm -r
:
pnpm -r lint
While the above still requires lint
script to be present in every package, we
don't have to worry about creating a eslint
config for every package or
maintaining a list of all the plugins in package.json
dependencies.
In repka
we only enforce eslint
rules that are absolutely necessary and lead
to bugs, when violated. Rules which can lead to false positives are not used.
This is to ensure that the code is not riddled with eslint-disable
comments.
The more you disable - the more it becomes useless.
eslint
is paired with prettier
and it is expected that developers use format
on save along with eslint --fix
on save. This way we can ensure that the code
is consistent, formatted and linted at all times.
eslint
rules still can be overridden standard eslint
way.
Another case is running tests:
pnpm jest
Use the above command to run all tests in the monorepo.
Alternatively, pass a glob pattern to run tests for a given set of files:
pnpm jest packages/playground/todo-list-store
There is no need to create a jest.config.js
file for every package. It's all
encapsulated by repka
and automatically discovered when needed.
What if we want to segregate integration tests from unit tests? It's done via
"convention" in repka
:
src
__integration__
test-helpers.ts
example.test.ts
file.ts
file.test.ts
Just put your integration tests into __integration__
directory and they can be
executed via:
pnpm jest --integration
Bundling is done via repka build:node
command. This is the command that
generates the ./dist/package.json
for publishing and bundles the entry points.
side note: As monorepo can contain applications of different types there is a plan to add a command to build web apps as well. But for now it's just node. The plan was to just use rollup, vite or webpack, but it's not done yet.
The bundling is based off package.json
exports
field. If you want to have
multiple entry points, just add them to the exports
field:
{
"exports": {
".": "./src/index.ts",
"./helpers": "./src/helpers.ts"
}
}
You can use repka build:node --watch
to watch for changes and rebuild the code
as you code.
In addition to that, naturally, globs are also supported:
{
"exports": {
".": "./src/index.ts",
"./features/*": "./src/features/*"
}
}
Every TypeScript file in features/
directory is going to become its own entry
point.
After bundling, if you mean to publish a package which can be consumed as a
library it is recommended to generate declarations for it. This is done via
repka declarations
command. It will generate .d.ts
files for all the entry
points.
It just works. No need to configure anything.
However, the dts-bundle-generator
which is used under the hood has a few
limitations: https://github.com/timocov/dts-bundle-generator#known-limitations
repka
is designed to be as simple as possible, but it also allows you to
configure bundling via code. Create a build.ts
file at the root of the package
that needs configuring.
Here is an example snippet:
import { buildForNode, pipeline } from './src';
import { addCjsBundle } from './src/build/cjsBuildHelpers';
await pipeline(
buildForNode({
extraRollupConfigs: (opts) => [
/**
* Add custom rollup config to support cjs bundles
* or whatever else we want
*/
addCjsBundle(opts),
],
copy: [
{
/**
* Copy files we don't want bundled but as is
*/
include: ['configs/**/*'],
destination: './dist/',
},
],
}),
async () => {
// do something else during the build
}
);
repka
uses esbuild
for jest
and repka build:node
.
Forked version of the DTS Bundle Generator is used to generate .d.ts files
Turnip icons created by Ridho Imam Prayogi - Flaticon
Now, I don't expect that a lot of people will use this as the project is quite opinionated. In addition to that - I'm just a single person and might not have the resources to help people out in case they have issues with it.
If you find the solution useful and want to help support the project - contributions are welcome.