- 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/ts@1.0.0-beta.10 repka initIf 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/ts@1.0.0-beta.10After that following dependencies become available:
tsctsxeslintjestdts-bundle-generatorprettierrepka
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/tsis 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 ofrepkaand it will be easier to update it. -
The
exportsfield only points to TypeScript files. There is literally no need for any JavaScript files in the repository. This includes any files declared in thebinfield (more on that below).repkawill usetsxwhere required to run TypeScript files. -
The
typeismodule, while we could supportcjsoutput, it is really not a good idea. -
repka lintwill runeslintandtscand run them in parallel -
repka testwill 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 tsxAnd then you can run it directly:
pnpm run cliAnother 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 eslintWhich will run eslint on entire repository.
To also check for TypeScript issues we can use:
pnpm repka lintWhich will run tsc and eslint in parallel.
Or we can parallelize it via pnpm -r:
pnpm -r lintWhile 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 jestUse 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-storeThere 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.
