Skip to content

zaripych/repka

Repository files navigation

Turnip or Repka

Features

  • 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.

Documentation

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 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/ts@1.0.0-beta.10

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.

Repository structure

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 of repka 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 the bin field (more on that below). repka will use tsx where required to run TypeScript files.

  • The type is module, while we could support cjs output, it is really not a good idea.

  • repka lint will run eslint and tsc and run them in parallel

  • repka test will run jest

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.

What Makes repka Awesome

No Need To Build for Development

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.

No Config Linting

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.

No Config Testing

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

No Config Bundling

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.

No Config Declarations

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

Advanced Configuration

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
  }
);

Modern Transforms

repka uses esbuild for jest and repka build:node.

Attributions

Forked version of the DTS Bundle Generator is used to generate .d.ts files

Turnip icons created by Ridho Imam Prayogi - Flaticon

Final note

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.