aberlaas

2.5.0 • Public • Published

aberlaas

Scaffold your JavaScript projects with consistent config for tests, lint, release and CI.

aberlaas is a wrapper around Jest, ESLint, Prettier, etc and their plugins so you only install one package in your devDependencies instead of dozens.

I created this module because I got tired of copy-pasting the same configuration files from project to project. With one meta module to handle all the tooling, I could get started on a new project in minutes instead of hours.

Using aberlaas on every project ensured my linting rules and release process is consistent across my projects. Of course, if you don't like the defaults it's shipped with, you can override them as all configuration files are exposed.

Installation

yarn add --dev aberlaas

yarn run aberlaas init

This will add aberlaas to your devDependencies and bootstrap your project. Config files for all the tools will be created (.eslintrc.js, jest.config.js, etc) and new yarn run scripts will be added for the most common tasks (lint, test, release, etc).

At that point, you should probably commit all the changes.

(Optional) Setup the external services

yarn run aberlaas setup

This will configure third party services like GitHub and CircleCI to work better with aberlaas.


The following table lists all the scripts added:

Script Description
yarn run test Run tests using Jest
yarn run test:watch Run tests using Jest in watch mode
yarn run ci Run testing and linting in CI
yarn run release Release the module on npm

Testing (with Jest)

aberlaas test to run all the Jest tests in ./lib. You can alter the behavior with the following options:

CLI Argument Default value Description
[...] ./lib Files and directories to test.
--config jest.config.js Jest config file to use
--watch false If enabled, will listen for changes on files and rerun tests
--failFast false If enabled, will stop as soon as one test fails

Note that you can also pass any other command-line flag and they will be passed directly to Jest under the hood.

Jest is loaded with jest-extended allowing you to use new matchers like .toBeString(), .toStartWith(), etc.

New global variables

testName is available in all tests and contains the name of the current it/test block.

captureOutput allows to swallow any stdout/stderr output for later inspection. Output is stripped of any trailing newlines and ANSI characters.

const actual = await captureOutput(async () => {
  console.log('foo');
});
// actual.stdout = ['foo']

dedent is included in all tests, so you can write multiline strings without having to break your indentation.

describe('moduleName', () => {
  describe('methodName', () => {
    it('should test a multiline string', () => {
      const input = dedent`
        Leading and trailing lines will be trimmed, so you can write something like
        this and have it work as you expect:

          * how convenient it is
          * that I can use an indented list
             - and still have it do the right thing`;
      // …
    });
  });

Precommit hooks

aberlaas uses lint-staged to make sure all committed code follows your coding standard.

All css, js, json and yml files will be checked for parsing errors (using aberlaas lint internally), and if errors are found it will attempt to automatically fix them. If errors persist, it will prevent the commit and let you know which file contains errors so you can fix them before committing again.

Whenever you commit a .js file that has a test attached (or a test file directly), aberlaas test will be run on those files. If the tests don't pass, your commit will be rejected.

Those two measures ensure that you'll never "break the build", by committing invalid files or code that does not pass the test. If you want to ignore this behavior, you can always add the -n option to your git commit command to skip the hooks.

Releasing

aberlaas release will release the package to npm. It will update the version in package.json as well as creating the related git tag.

When called without arguments, it will prompt you for the next version to package. If called with an argument, this will be used as the next version number (for example, yarn run release 1.1.3). You can also use SemVer increments (for example, yarn run release minor).

Use --dry-run to start a dry-run. It will simulate a release but won't actually push anything to GitHub or npm.

Continuous Integration

aberlaas ci is triggered by CI Servers (currently only CircleCI is supported), and won't do anything when run locally.

When on a CI server it will first display the current node and yarn version, and then test and lint scripts in that order. It will fail whenever one of them fails, or succeed if they all succeed.

The node and yarn version used both locally and on the CI server will be the same. A .nvmrc file is created when running yarn run aberlaas init that will force local users to use the specified version. The same version is also specified in the Docker image pulled by CircleCI. As for Yarn, a local copy of the whole yarn program is added to the repository when first initializing it, so both locals and CI servers will use it.

By default, tests running on the CI will be parallelized on two CPUs (this is what most free CI tier offer). If you have access to higher end machines, you can update this value by passing the --cpu-count=X flag to your aberlaas ci call.

File structure

./lib/configs contain the default configuration for all the tools. They are exported by the package and thus can be required in userland.

./templates contains default configurations files copied to userland. Each extends the configuration exported in the previous files. Copying files to userland allows user to change the files if they want to change the behavior.

.eslintrc.js, stylelint.config.js and vite.config.js are local configuration files for aberlaas itself. They eat their own dog food by referencing the same configs as above.

Tools used and their future

ESLint

ESLint doesn't yet support ESM config files. We'll upgrade to the latest ESLint when it does. This should also help fix the issue with Yarn PnP (see below).

Yarn

tl;dr; We'll move to Yarn PnP once ESLint has support for Flat Configs, and default yarn run doesn't add a ~1s delay overhead

PnP

Aberlaas is using Yarn Berry (v2+) as its main package management tool.

Yarn Berry comes with a Plug And Play (PnP) feature that replaces the usage of node_modules in favor of a .pnp.cjs file. Instead of having a very large node_modules folder, a unique copy of each dependency is stored in the user home folder and the .pnp.cjs file only keeps references to those folder. This makes installing dependencies faster as it needs way less I/O.

By ditching the whole node_modules principle, it also removes concepts like hoisting of dependencies in a monorepo. This, unfortunately, breaks ESLint.

ESLint expect all its plugins to be defined as peerDependencies at the root of a mono-repo, as it will always try to include them from there. It works more or less correctly at the best of times, and aberlaas already has some hacks (including resolvePluginsRelativeTo) to work around that.

But with PnP, there is no way to make it work correctly, so I will need to wait for a better compatibility between ESLint and Yarn 2 before using it.

Sources:

Calling binaries from the host

In yarn v1, if you install aberlaas in your project, all of aberlaas dependencies (including binaries) were hoisted to the root. This was a design flaw, as if several dependencies defined binaries by the same name, they would fight in a race condition to take the slot in node_modules/.bin.

This has been fixed in Yarn Berry, but it also means it's no longer possible to call yarn run eslint from a repository that includes aberlaas, because eslint is not a direct dependency of the repo.

It might be possible to define proxy binaries inside of aberlaas, but those will incur performance issues as they will need to spawn one more yarn context (see below).

Calling binaries from aberlaas

In Yarn V1, I was calling binaries that aberlaas depends on (eslint, vitest, etc) by calling yarn run. This was adding some overhead but was acceptable. Yarn Berry adds even more overhead and it is becoming noticeable.

Now, my prefered way it to use the NodeJS API of the dependencies instead of their CLI, to limit the overhead. I managed to move most tools to this new approach, but sometimes I still need to use the CLI (vitest for example has awesome live watching and display reloading that I don't think I can easily replicate through code).

I ran some performance tests to see what would be the fastest way to call vitest from aberlaas

hyperfine \
    "zsh -i -c 'yarn bin vitest && /home/tim/local/www/projects/aberlaas/node_modules/vitest/vitest.mjs --version'" \
    "zsh -i -c 'yarn run vitest --version'" \
    "/home/tim/local/www/projects/aberlaas/node_modules/vitest/vitest.mjs --version" \
    "/home/tim/local/www/projects/aberlaas/node_modules/.bin/vitest --version" 
Benchmark 1: zsh -i -c 'yarn run vitest --version'
  Time (mean ± σ):      1.945 s ±  0.051 s    [User: 1.986 s, System: 0.850 s]
  Range (min … max):    1.859 s …  2.018 s    10 runs
 
Benchmark 2: zsh -i -c 'yarn bin vitest && /home/tim/local/www/projects/aberlaas/node_modules/vitest/vitest.mjs --version'
  Time (mean ± σ):      2.108 s ±  0.150 s    [User: 2.108 s, System: 0.843 s]
  Range (min … max):    1.930 s …  2.289 s    10 runs
 
Benchmark 3: /home/tim/local/www/projects/aberlaas/node_modules/vitest/vitest.m
js --version
  Time (mean ± σ):     482.5 ms ±  40.9 ms    [User: 448.4 ms, System: 327.2 ms]
  Range (min … max):   442.1 ms … 553.3 ms    10 runs
 
Benchmark 4: /home/tim/local/www/projects/aberlaas/node_modules/.bin/vitest --version
  Time (mean ± σ):     491.9 ms ±  29.6 ms    [User: 454.1 ms, System: 331.2 ms]
  Range (min … max):   453.8 ms … 535.4 ms    10 runs

Finding the binary through yarn bin then calling it is the slowest, but yarn run isn't much faster. Directly calling the binary is the fastest, but it's path can't be easily guessed (apart from reading and parsing a package.json). But using the symlinks in node_modules/.bin is consistent, barely slower than calling the binary directly and much faster than using yarn.

This is what I'll be using. Of course, this will break when I'll move to PnP as the node_modules folder won't be there, but hopefully Yarn will be faster by then and I can use yarn run reliably.

Speed

With Yarn 2+, calling yarn run seem to add ~1s of overhead each time. This is a known issue (due to the fact Yarn 2 needs to spawn yarn 1, node and a few other layers).

1s is not much in itself, but grows quickly when you have nested yarn run aberlaas calls that call yarn run eslint, and even more when you need to deal with monorepos and multiple packages that each have their own yarn scope.

There are open issues on the topic, but nothing merged yet:

  • #3732, where it is discussed to make yarn run aware that it's running yarn run and keep the same state without spawning new yarns
  • #2575, which is the main issue about Yarn performance, with benchmarks against npm/npx/yarn v1.

Where does the name Aberlaas come from?

Aberlaas is the base camp from which all great expedition start in the La Horde du Contrevent book. I felt it's a great name for a bootstrapping kit for modules.

For your convenience, aberlass and aberlas are added as aliases by default.

Documentation

The complete documentation can be found on https://projects.pixelastic.com/aberlaas/

Readme

Keywords

none

Package Sidebar

Install

npm i aberlaas

Weekly Downloads

12

Version

2.5.0

License

MIT

Unpacked Size

93 kB

Total Files

69

Last publish

Collaborators

  • pixelastic