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');
});
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.
Auto-Releasing
As an optional feature, you can have aberlaas automatically release a new
version of your module from the CI environment when relevant.
The CI will then check all the commits since the last release. If any commit is
a feat()
it will release a new minor version; it any commit is a fix()
it
will release a new patch version. For major release, you'll have to do it
manually.
This option is not enabled by default. If you need it, you need to follow those
steps:
- Run
aberlaas setup --auto-release
. It will setup the required ENV
variables
and ssh keys - Update your
aberlaas ci
script to aberlaas ci --auto-release
- Uncomment the
add_ssh_keys
in your .circleci.yml
file
File structure
./lib/configs
contain the default configuration for all the tools. They are
exported by the package and thus can be require
d 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
, .stylelintrc.js
and jest.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/