Skip to content

babel-utils/babel-plugin-tester

master
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Code

Latest commit

 

Git stats

Files

Permalink
Failed to load latest commit information.
Type
Name
Latest commit message
Commit time
src
November 5, 2023 17:45
November 5, 2023 17:42
January 6, 2023 14:07
January 24, 2023 20:24

babel-plugin-tester

Note
This documentation is for babel-plugin-tester@β‰₯11. Documentation for earlier versions can be found here.

Utilities for testing babel plugins and presets.


Code Coverage Version Downloads MIT License

All Contributors

PRs Welcome Code of Conduct

The Problem

You are writing a babel plugin or preset and want to write tests for it too.

This Solution

This is a fairly simple abstraction to help you write tests for your babel plugin or preset. It was built to work with Jest, but most of the functionality will work with Mocha, Jasmine, node:test, Vitest, and any other test runner that defines standard describe and it globals with async support (see appendix).

This package is tested on both Windows and nix (Ubuntu) environments.

Installation

This module is distributed via npm which is bundled with node and should be installed as one of your project's devDependencies:

npm install --save-dev babel-plugin-tester

Usage

Import

ESM:

import { pluginTester } from 'babel-plugin-tester';

CJS:

const { pluginTester } = require('babel-plugin-tester');

Default Export Compatibility

For backwards compatibility reasons, a default export is also available.

These default exports are considered deprecated and should be avoided, especially in TypeScript code. They will be removed in the next major release. Prefer proper import syntax.

import pluginTester from 'babel-plugin-tester';

const pluginTester = require('babel-plugin-tester');

const pluginTester = require('babel-plugin-tester').default;

const { default: pluginTester } = require('babel-plugin-tester');

Invoke

/* file: test/unit.test.js */

import { pluginTester } from 'babel-plugin-tester';
import yourPlugin from '../src/your-plugin';

pluginTester({
  plugin: yourPlugin,
  tests: {
    /* Your test objects */
  }
});

Note how pluginTester does not appear inside any test/it block nor within any hook functions. For advanced use cases, pluginTester may appear within one or more describe blocks, though this is discouraged.

Options

This section lists the options you can pass to babel-plugin-tester. They are all optional with respect to the following:

plugin

This is used to provide the babel plugin under test. For example:

/* file: test/unit.test.js */

import { pluginTester } from 'babel-plugin-tester';
import identifierReversePlugin from '../src/identifier-reverse-plugin';

pluginTester({
  plugin: identifierReversePlugin,
  tests: {
    /* Your test objects */
  }
});

/* file: src/identifier-reverse-plugin.js */

// Normally you would import this from your plugin module
function identifierReversePlugin() {
  return {
    name: 'identifier reverse',
    visitor: {
      Identifier(idPath) {
        idPath.node.name = idPath.node.name.split('').reverse().join('');
      }
    }
  };
}

pluginName

This is used as the describe block name and in your tests' names. If pluginName can be inferred from the plugin's name, then it will be and you do not need to provide this option. If it cannot be inferred for whatever reason, pluginName defaults to "unknown plugin".

Note that there is a small caveat when relying on pluginName inference.

pluginOptions

This is used to pass options into your plugin at transform time. If provided, the object will be lodash.mergeWith'd with each test object's pluginOptions/fixture's pluginOptions, with the latter taking precedence. Note that arrays will be concatenated and explicitly undefined values will unset previously defined values during merging.

preset

This is used to provide the babel preset under test. For example:

/* file: cool-new-babel-preset.test.js */

import path from 'node:path';
import { pluginTester } from 'babel-plugin-tester';
import coolNewBabelPreset from './cool-new-babel-preset.js';

pluginTester({
  preset: coolNewBabelPreset,
  // A path to a directory containing your test fixtures
  fixtures: path.join(__dirname, 'fixtures')
});

/* file: cool-new-babel-preset.js */

function identifierReversePlugin() {
  return {
    name: 'identifier reverse',
    visitor: {
      Identifier(idPath) {
        idPath.node.name = idPath.node.name.split('').reverse().join('');
      }
    }
  };
}

function identifierAppendPlugin() {
  return {
    name: 'identifier append',
    visitor: {
      Identifier(idPath) {
        idPath.node.name = `${idPath.node.name}_appended`;
      }
    }
  };
}

export function coolNewBabelPreset() {
  return { plugins: [identifierReversePlugin, identifierAppendPlugin] };
}

presetName

This is used as the describe block name and in your tests' names. Defaults to "unknown preset".

presetOptions

This is used to pass options into your preset at transform time. If provided, the object will be lodash.mergeWith'd with each test object's presetOptions/fixture's presetOptions, with the latter taking precedence. Note that arrays will be concatenated and explicitly undefined values will unset previously defined values during merging.

babel

This is used to provide your own implementation of babel. This is particularly useful if you want to use a different version of babel than what's required by this package.

babelOptions

This is used to configure babel. If provided, the object will be lodash.mergeWith'd with the defaults and each test object's babelOptions/fixture's babelOptions, with the latter taking precedence. Note that arrays will be concatenated and explicitly undefined values will unset previously defined values during merging.

Also note that babelOptions.babelrc and babelOptions.configFile are set to false by default, which disables automatic babel configuration loading. This can be re-enabled if desired.

To simply reuse your project's babel.config.js or some other configuration file, set babelOptions like so:

import path from 'node:path';
import { pluginTester } from 'babel-plugin-tester';

pluginTester({
  plugin: yourPlugin,
  // ...
  babelOptions: require(path.join('..', 'babel.config.js')),
  // ...
  tests: {
    /* Your test objects */
  }
});
Custom Plugin and Preset Run Order

By default, when you include a custom list of plugins or presets in babelOptions, the plugin or preset under test will always be the final plugin or preset to run.

For example, consider the myPlugin plugin:

import { pluginTester } from 'babel-plugin-tester';

pluginTester({
  plugin: myPlugin,
  pluginName: 'my-plugin',
  babelOptions: {
    plugins: [
      ['@babel/plugin-syntax-decorators', { legacy: true }],
      ['@babel/plugin-proposal-class-properties', { loose: true }]
    ]
  }
});

By default, myPlugin will be invoked after @babel/plugin-syntax-decorators and @babel/plugin-proposal-class-properties (i.e. myPlugin is appended by default).

It is possible to specify a custom ordering using the exported runPluginUnderTestHere symbol. For instance, to run myPlugin after @babel/plugin-syntax-decorators but before @babel/plugin-proposal-class-properties:

import { pluginTester, runPluginUnderTestHere } from 'babel-plugin-tester';

pluginTester({
  plugin: myPlugin,
  pluginName: 'my-plugin',
  babelOptions: {
    plugins: [
      ['@babel/plugin-syntax-decorators', { legacy: true }],
      runPluginUnderTestHere,
      ['@babel/plugin-proposal-class-properties', { loose: true }]
    ]
  }
});

Or to run myPlugin before both @babel/plugin-syntax-decorators and @babel/plugin-proposal-class-properties:

import { pluginTester, runPluginUnderTestHere } from 'babel-plugin-tester';

pluginTester({
  plugin: myPlugin,
  pluginName: 'my-plugin',
  babelOptions: {
    plugins: [
      runPluginUnderTestHere,
      ['@babel/plugin-syntax-decorators', { legacy: true }],
      ['@babel/plugin-proposal-class-properties', { loose: true }]
    ]
  }
});

The same can be done when testing presets. Note that myPreset is normally prepended by default since, unlike plugins, presets are run in reverse order:

import { pluginTester, runPresetUnderTestHere } from 'babel-plugin-tester';

pluginTester({
  preset: myPreset,
  presetName: 'my-preset',
  babelOptions: {
    presets: [
      '@babel/preset-typescript',
      ['@babel/preset-react', { pragma: 'dom' }],
      runPresetUnderTestHere
    ]
  }
});

In this example, myPreset will run first instead of last.

title

This is used to specify a custom title for the two top-level describe blocks, the first enclosing all tests (i.e. describe(title, ...)) and the second enclosing all fixtures (i.e. describe(`${title} fixtures`, ...)).

Explicitly setting this option will override any defaults or inferred values. Set to false to prevent the creation of these enclosing describe blocks. Otherwise, the title defaults to using pluginName/presetName.

filepath

This is used to resolve relative paths provided by the fixtures option; the test object properties codeFixture, outputFixture, and execFixture; and during configuration resolution for prettier. That is: if the aforesaid properties are not absolute paths, they will be path.join'd with the directory name of filepath.

filepath is also passed to formatResult if a more specific path is not available, and it is used as the default value for babelOptions.filename in test objects.

This option defaults to the absolute path of the file that invoked the pluginTester function.

For backwards compatibility reasons, filepath is synonymous with filename. They can be used interchangeably, though care must be taken not to confuse the babel-plugin-tester option filename with babelOptions.filename. They are NOT the same!

endOfLine

This is used to control which line endings both the actual output from babel and the expected output will be converted to. Defaults to "lf".

Options Description
"lf" Use Unix-style line endings
"crlf" Use Windows-style line endings
"auto" Use the system default line endings
"preserve" Use the line endings from the input
false Disable line ending conversion entirely

When disabling line ending conversion, note that Babel will always output LF even if the input is CRLF.

setup

This function will be run before every test runs, including fixtures. It can return a function which will be treated as a teardown function. It can also return a promise. If that promise resolves to a function, that will be treated as a teardown function.

See here for the complete run order.

teardown

This function will be run after every test runs, including fixtures. You can define this via teardown or you can return it from the setup function. This can likewise return a promise if it is asynchronous.

This function, if provided, will be run after any teardown function returned by setup. See here for the complete run order.

formatResult

This function is used to format all babel outputs, and defaults to a function that invokes prettier. If a prettier configuration file is found, then that will be used. Otherwise, prettier will use its own default configuration.

You can also override or entirely disable formatting.

snapshot

Equivalent to snapshot but applied globally across all test objects.

fixtureOutputName

Equivalent to fixtureOutputName but applied globally across all fixtures.

fixtureOutputExt

Equivalent to fixtureOutputExt but applied globally across all fixtures.

titleNumbering

Determines which test titles are prefixed with a number when registering test blocks (e.g. `1. ${title}`, `2. ${title}`, etc). Defaults to "all".

Options Description
"all" All test object and fixtures tests will be numbered
"tests-only" Only test object tests will be numbered
"fixtures-only" Only fixtures tests will be numbered
false Disable automatic numbering in titles entirely

restartTitleNumbering

Normally, multiple invocations of babel-plugin-tester in the same test file will share the same test title numbering. For example:

/* file: test/unit.test.js */

import { pluginTester } from 'babel-plugin-tester';
import yourPlugin from '../src/your-plugin';

pluginTester({
  plugin: yourPlugin,
  tests: { 'test one': testOne, 'test two': testTwo }
});

pluginTester({
  plugin: yourPlugin,
  tests: { 'test one': testOne, 'test x': testTwo }
});

pluginTester({
  plugin: yourPlugin,
  tests: { 'test five': testOne }
});

Will result in test blocks with names like:

1. Test one
2. Test two
3. Test one
4. Test x
5. Test five

However, setting this option to true will restart the numbering:

/* file: test/unit.test.js */

import { pluginTester } from 'babel-plugin-tester';
import yourPlugin from '../src/your-plugin';

pluginTester({
  plugin: yourPlugin,
  tests: { 'test one': testOne, 'test two': testTwo }
});

pluginTester({
  plugin: yourPlugin,
  restartTitleNumbering: true,
  tests: { 'test one': testOne, 'test x': testTwo }
});

pluginTester({
  plugin: yourPlugin,
  tests: { 'test five': testOne }
});

Which will result in test blocks with names like:

1. Test one
2. Test two
1. Test one
2. Test x
3. Test five

This option is false by default.

fixtures

There are two ways to create tests: using the tests option to provide one or more test objects or using the fixtures option described here. Both can be used simultaneously.

The fixtures option must be a path to a directory with a structure similar to the following:

fixtures
β”œβ”€β”€ first-test         # test title will be: "1. first test"
β”‚Β Β  β”œβ”€β”€ code.js        # required
β”‚Β Β  └── output.js      # required (unless using the `throws` option)
β”œβ”€β”€ second-test        # test title will be: "2. second test"
β”‚   β”œβ”€β”€ .babelrc.js    # optional
β”‚   β”œβ”€β”€ options.json   # optional
β”‚   β”œβ”€β”€ code.ts        # required (other file extensions are allowed too)
β”‚   └── output.js      # required (unless using the `throws` option)
└── nested
    β”œβ”€β”€ options.json   # optional
    β”œβ”€β”€ third-test     # test title will be: "3. nested > third test"
    β”‚   β”œβ”€β”€ code.mjs   # required (other file extensions are allowed too)
    β”‚   β”œβ”€β”€ output.js  # required (unless using the `throws` option)
    β”‚   └── options.js # optional (overrides props in nested/options.json)
    └── x-fourth-test  # test title will be: "4. nested > x fourth test"
        └── exec.js    # required (alternative to code/output structure)

Assuming the fixtures directory is in the same directory as your test file, you could use it with the following configuration:

pluginTester({
  plugin,
  fixtures: path.join(__dirname, 'fixtures')
});

If fixtures is not an absolute path, it will be path.join'd with the directory name of filepath.

.babelrc, .babelrc.json, .babelrc.js, .babelrc.cjs, and .babelrc.mjs config files in fixture directories are technically supported out-of-the-box, though .mjs config files will cause a segfault in Jest until this issue with V8/Chromium is resolved.

And it would run four tests, one for each directory in fixtures containing a file starting with "code" or "exec".

code.js

This file's contents will be used as the source code input into babel at transform time. Any file extension can be used, even a multi-part extension (e.g. .test.js in code.test.js) as long as the file name starts with code.; the expected output file will have the same file extension suffix (i.e. .js in code.test.js) as this file unless changed with the fixtureOutputExt option.

After being transformed by babel, the resulting output will have whitespace trimmed, line endings converted, and then get formatted by prettier.

Note that this file cannot appear in the same directory as exec.js. If more than one code.* file exists in a directory, the first one will be used and the rest will be silently ignored.

output.js

This file, if provided, will have its contents compared with babel's output, which is code.js transformed by babel and formatted with prettier. If this file is missing and neither throws nor exec.js are being used, this file will be automatically generated from babel's output. Additionally, the name and extension of this file can be changed with the fixtureOutputName and fixtureOutputExt options.

Before being compared to babel's output, this file's contents will have whitespace trimmed and line endings converted.

Note that this file cannot appear in the same directory as exec.js.

exec.js

This file's contents will be used as the input into babel at transform time just like the code.js file, except the output will be evaluated in the same CJS context as the test runner itself, meaning it supports features like a/sync IIFEs, debugging breakpoints (!), and has access to mocked modules, expect, require, __dirname and __filename (derived from this file's path), and other globals/features provided by your test framework. However, the context does not support import, top-level await, or any other ESM syntax. Hence, while any file extension can be used (e.g. .ts, .vue, .jsx), this file will always be evaluated as CJS.

The test will always pass unless an exception is thrown (e.g. when an expect() fails).

Use this to make advanced assertions on the output. For example, to test that babel-plugin-proposal-throw-expressions actually throws, your exec.js file might contain:

expect(() => throw new Error('throw expression')).toThrow('throw expression');

Keep in mind that, despite sharing a global context, execution will occur in a separate realm, which means native/intrinsic types will be different. This can lead to unexpectedly failing tests. For example:

expect(require(`${__dirname}/imported-file.json`)).toStrictEqual({
  data: 'imported'
});

This may fail in some test frameworks with the message "serializes to the same string". This is because the former object's Object prototype comes from a different realm than the second object's Object prototype, meaning the two objects are not technically strictly equal. However, something like the following, which creates two objects in the same realm, will pass:

expect(
  Object.fromEntries(
    Object.entries(require(`${__dirname}/imported-file.json`))
  )
).toStrictEqual({ data: 'imported' });

Or:

expect(JSON.stringify(require(`${__dirname}/imported-file.json`))).toBe(
  JSON.stringify({ data: 'imported' })
);

Or even:

expect(require(`${__dirname}/imported-file.json`)).toEqual({
  data: 'imported'
});

After being transformed by babel but before being evaluated, the babel output will have whitespace trimmed, line endings converted, and then get formatted by prettier.

Note that this file cannot appear in the same directory as code.js or output.js. If more than one exec.* file exists in a directory, the first one will be used and the rest will be silently ignored.

options.json (Or options.js)

For each fixture, the contents of the entirely optional options.json file are lodash.mergeWith'd with the options provided to babel-plugin-tester, with the former taking precedence. Note that arrays will be concatenated and explicitly undefined values will unset previously defined values during merging.

For added flexibility, options.json can be specified as options.js instead so long as a JSON object is exported via module.exports. If both files exist in the same directory, options.js will take precedence and options.json will be ignored entirely.

Fixtures support deeply nested directory structures as well as shared or "root" options.json files. For example, placing an options.json file in the fixtures/nested directory would make its contents the "global configuration" for all fixtures under fixtures/nested. That is: each fixture would lodash.mergeWith the options provided to babel-plugin-tester, fixtures/nested/options.json, and the contents of their local options.json file as described above.

What follows are the properties you may use if you provide an options file, all of which are optional:

babelOptions

This is used to configure babel. Properties specified here override (lodash.mergeWith) those from the babelOptions option provided to babel-plugin-tester. Note that arrays will be concatenated and explicitly undefined values will unset previously defined values during merging.

pluginOptions

This is used to pass options into your plugin at transform time. Properties specified here override (lodash.mergeWith) those from the pluginOptions option provided to babel-plugin-tester. Note that arrays will be concatenated and explicitly undefined values will unset previously defined values during merging.

Unlike with babel-plugin-tester's options, you can safely mix plugin-specific properties (like pluginOptions) with preset-specific properties (like presetOptions) in your options files.

presetOptions

This is used to pass options into your preset at transform time. Properties specified here override (lodash.mergeWith) those from the presetOptions option provided to babel-plugin-tester. Note that arrays will be concatenated and explicitly undefined values will unset previously defined values during merging.

Unlike with babel-plugin-tester's options, you can safely mix plugin-specific properties (like pluginOptions) with preset-specific properties (like presetOptions) in your options files.

title

If provided, this will be used as the title of the test. Otherwise, the directory name will be used as the title by default (with spaces replacing dashes).

only

Use this to run only the specified fixture. Useful while developing to help focus on a small number of fixtures. Can be used in multiple options.json files.

Requires Jest, an equivalent interface (like Vitest), or a manually-defined it object exposing an appropriate only method.

skip

Use this to skip running the specified fixture. Useful for when you are working on a feature that is not yet supported. Can be used in multiple options.json files.

Requires Jest, an equivalent interface (like Vitest), or a manually-defined it object exposing an appropriate skip method.

throws

When using certain values, this property must be used in options.js instead of options.json.

Use this to assert that a particular code.js file should cause babel to throw an error during transformation. For example:

{
  // ...
  throws: true,
  throws: 'should have this exact message',
  throws: /should pass this regex/,
  throws: SyntaxError, // Should be an instance of this class
  throws: err => {
    if (err instanceof SyntaxError && /message/.test(err.message)) {
      return true; // Test will fail if this function's return value !== true
    }
  },
}

Be careful using instanceof across realms as it can lead to strange behavior with frontend frames/windows and with tools that rely on Node's VM module (like Jest). Prefer name checks and utilities like isNativeError, Array.isArray, and overriding Symbol.hasInstance instead.

If the value of throws is a class, that class must be a subtype of Error or the behavior of babel-plugin-tester is undefined.

For backwards compatibility reasons, throws is synonymous with error. They can be used interchangeably, with throws taking precedence.

Note that this property cannot be present when using an exec.js or output.js file.

setup

As it requires a function value, this property must be used in options.js instead of options.json.

This function will be run before a particular fixture's tests are run. It can return a function which will be treated as a teardown function. It can also return a promise. If that promise resolves to a function, that will be treated as a teardown function.

This function, if provided, will run after any setup function provided as a babel-plugin-tester option. See here for the complete run order.

teardown

As it requires a function value, this property must be used in options.js instead of options.json.

This function will be run after a fixture's tests finish running. You can define this via teardown or you can return it from the setup function. This can likewise return a promise if it is asynchronous.

This function, if provided, will be run after any teardown function returned by the setup property, both of which will run before any teardown function provided as a babel-plugin-tester option. See here for the complete run order.

formatResult

As it requires a function value, this property must be used in options.js instead of options.json.

This function is used to format all babel outputs, and defaults to a function that invokes prettier. If a prettier configuration file is found, then that will be used. Otherwise, prettier will use its own default configuration.

You can also entirely disable formatting.

This will override the formatResult function provided to babel-plugin-tester.

fixtureOutputName

Use this to provide your own fixture output file name. Defaults to "output".

fixtureOutputExt

Use this to provide your own fixture output file extension. Including the leading period is optional; that is: if you want output.jsx, fixtureOutputExt can be set to either "jsx" or ".jsx". If omitted, the input fixture's file extension will be used instead.

This is particularly useful if you are testing TypeScript input.

tests

There are two ways to create tests: using the fixtures option that leverages the filesystem or using the tests option described here. Both can be used simultaneously.

Using the tests option, you can provide test objects describing your expected transformations. You can provide tests as an object of test objects or an array of test objects. If you provide an object, the object's keys will be used as the default title of each test. If you provide an array, each test's default title will be derived from its index and pluginName/presetName.

See the example for more details.

Test Objects

A minimal test object can be:

  1. A string representing code.
  2. An object with a code property.

What follows are the properties you may use if you provide an object, most of which are optional:

babelOptions

This is used to configure babel. Properties specified here override (lodash.mergeWith) those from the babelOptions option provided to babel-plugin-tester. Note that arrays will be concatenated and explicitly undefined values will unset previously defined values during merging.

pluginOptions

This is used to pass options into your plugin at transform time. Properties specified here override (lodash.mergeWith) those from the pluginOptions option provided to babel-plugin-tester. Note that arrays will be concatenated and explicitly undefined values will unset previously defined values during merging.

Unlike with babel-plugin-tester's options, you can safely mix plugin-specific properties (like pluginOptions) with preset-specific properties (like presetOptions) in your test objects.

presetOptions

This is used to pass options into your preset at transform time. Properties specified here override (lodash.mergeWith) those from the presetOptions option provided to babel-plugin-tester. Note that arrays will be concatenated and explicitly undefined values will unset previously defined values during merging.

Unlike with babel-plugin-tester's options, you can safely mix plugin-specific properties (like pluginOptions) with preset-specific properties (like presetOptions) in your test objects.

title

If provided, this will be used as the title of the test. Otherwise, the title will be determined from test object by default.

only

Use this to run only the specified test. Useful while developing to help focus on a small number of tests. Can be used on multiple tests.

Requires Jest, an equivalent interface (like Vitest), or a manually-defined it object exposing an appropriate only method.

skip

Use this to skip running the specified test. Useful for when you are working on a feature that is not yet supported. Can be used on multiple tests.

Requires Jest, an equivalent interface (like Vitest), or a manually-defined it object exposing an appropriate skip method.

throws

Use this to assert that a particular test object should cause babel to throw an error during transformation. For example:

{
  // ...
  throws: true,
  throws: 'should have this exact message',
  throws: /should pass this regex/,
  throws: SyntaxError, // Should be an instance of this class
  throws: err => {
    if (err instanceof SyntaxError && /message/.test(err.message)) {
      return true; // Test will fail if this function's return value !== true
    }
  },
}

Be careful using instanceof across realms as it can lead to strange behavior with frontend frames/windows and with tools that rely on Node's VM module (like Jest). Prefer name checks and utilities like isNativeError, Array.isArray, and overriding Symbol.hasInstance instead.

If the value of throws is a class, that class must be a subtype of Error or the behavior of babel-plugin-tester is undefined.

For backwards compatibility reasons, throws is synonymous with error. They can be used interchangeably, with throws taking precedence.

Note that this property cannot be present when using the output, outputFixture, exec, execFixture, or snapshot properties.

setup

This function will be run before a particular test is run. It can return a function which will be treated as a teardown function. It can also return a promise. If that promise resolves to a function, that will be treated as a teardown function.

This function, if provided, will run after any setup function provided as a babel-plugin-tester option. See here for the complete run order.

teardown

This function will be run after a test finishes running. You can define this via teardown or you can return it from the setup function. This can likewise return a promise if it is asynchronous.

This function, if provided, will be run after any teardown function returned by the setup property, both of which will run before any teardown function provided as a babel-plugin-tester option. See here for the complete run order.

formatResult

This function is used to format all babel outputs, and defaults to a function that invokes prettier. If a prettier configuration file is found, then that will be used. Otherwise, prettier will use its own default configuration.

You can also entirely disable formatting.

This will override the formatResult function provided to babel-plugin-tester.

snapshot

If you would prefer to take a snapshot of babel's output rather than compare it to something you provide manually, specify snapshot: true. This will cause babel-plugin-tester to generate a snapshot containing both the source code and babel's output.

Defaults to false.

Note that this property cannot appear in the same test object as the output, outputFixture, exec, execFixture, or throws properties.

Requires Jest, an appropriate shim or equivalent interface (like Vitest), or a manually-defined expect object exposing an appropriate toMatchSnapshot method.

code

The code that you want babel to transform using your plugin or preset. This must be provided unless you are using the codeFixture or exec properties instead. If you do not provide the output or outputFixture properties and snapshot is not truthy, then the assertion is that this code is unchanged by the transformation.

Before being transformed by babel, any indentation will be stripped as a convenience for template literals. After being transformed, the resulting output will have whitespace trimmed, line endings converted, and then get formatted by prettier.

Note that this property cannot appear in the same test object as the codeFixture, exec, or execFixture properties.

output

The value of this property will be compared with babel's output.

Before being compared to babel's output, this value will have whitespace trimmed, line endings converted, and any indentation stripped as a convenience for template literals.

Note that this property cannot appear in the same test object as the outputFixture, exec, execFixture, throws, or snapshot properties.

exec

The provided source will be transformed just like the code property, except the output will be evaluated in the same CJS context as the test runner itself, meaning it supports features like a/sync IIFEs, debugging breakpoints (!), and has access to mocked modules, expect, require, __dirname and __filename (derived from available path info and falling back on filepath), and other globals/features provided by your test framework. However, the context does not support import, top-level await, or any other ESM syntax. Hence, while any file extension can be used (e.g. .ts, .vue, .jsx), this file will always be evaluated as CJS.

The test will always pass unless an exception is thrown (e.g. when an expect() fails).

Use this to make advanced assertions on the output. For example, you can test that babel-plugin-proposal-throw-expressions actually throws using the following:

{
  // ...
  exec: `
    expect(() => throw new Error('throw expression')).toThrow('throw expression');
  `;
}

Keep in mind that, despite sharing a global context, execution will occur in a separate realm, which means native/intrinsic types will be different. This can lead to unexpectedly failing tests. For example:

expect(require(`${__dirname}/imported-file.json`)).toStrictEqual({
  data: 'imported'
});

This may fail in some test frameworks with the message "serializes to the same string". This is because the former object's Object prototype comes from a different realm than the second object's Object prototype, meaning the two objects are not technically strictly equal. However, something like the following, which creates two objects in the same realm, will pass:

expect(
  Object.fromEntries(
    Object.entries(require(`${__dirname}/imported-file.json`))
  )
).toStrictEqual({ data: 'imported' });

Or:

expect(JSON.stringify(require(`${__dirname}/imported-file.json`))).toBe(
  JSON.stringify({ data: 'imported' })
);

Or even:

expect(require(`${__dirname}/imported-file.json`)).toEqual({
  data: 'imported'
});

After being transformed by babel but before being evaluated, the babel output will have whitespace trimmed, line endings converted, and then get formatted by prettier.

Note that this property cannot appear in the same test object as the execFixture, code, codeFixture, output, outputFixture, throws, or snapshot properties.

codeFixture

If you would rather put your code in a separate file, you can specify a file path here instead. If it is an absolute path, then that's the file that will be loaded. Otherwise, codeFixture will be path.join'd with the directory name of filepath.

After being transformed by babel, the resulting output will have whitespace trimmed, line endings converted, and then get formatted by prettier.

Like code, this property cannot appear in the same test object as the exec or execFixture properties, nor the code property (obviously).

If you find you are using this property more than a couple of times, consider using fixtures instead.

For backwards compatibility reasons, codeFixture is synonymous with fixture. They can be used interchangeably, though care must be taken not to confuse the test object property fixture with the babel-plugin-tester option fixtures, the latter being plural.

outputFixture

If you would rather put your output in a separate file, you can specify a file path here instead. If it is an absolute path, then that's the file that will be loaded. Otherwise, outputFixture will be path.join'd with the directory name of filepath.

Before being compared to babel's output, this file's contents will have whitespace trimmed and line endings converted.

Like output, this property cannot appear in the same test object as the exec, execFixture, throws, or snapshot properties, nor the output property (obviously).

If you find you are using this property more than a couple of times, consider using fixtures instead.

execFixture

If you would rather put your exec in a separate file, you can specify a file path here instead. If it is an absolute path, then that's the file that will be loaded. Otherwise, execFixture will be path.join'd with the directory name of filepath.

After being transformed by babel but before being evaluated, the babel output will have whitespace trimmed, line endings converted, and then get formatted by prettier.

Like exec, this property cannot appear in the same test object as the code, codeFixture, output, outputFixture, throws, or snapshot properties, nor the exec property (obviously).

If you find you are using this property more than a couple of times, consider using fixtures instead.

Examples

Simple Example

import { pluginTester } from 'babel-plugin-tester';
import identifierReversePlugin from '../identifier-reverse-plugin';

// NOTE: you can use beforeAll, afterAll, beforeEach, and afterEach as usual,
// but initial configuration tasks, like loading content from fixture files,
// will complete *at the point the pluginTester function is called* which means
// BEFORE beforeAll and other Jest hooks are run.

pluginTester({
  plugin: identifierReversePlugin,
  // Defaults to false, but with this line we set the default to true across
  // *all* tests.
  snapshot: true,
  tests: [
    {
      code: "'hello';"
      // Snapshot should show that prettier has changed the single quotes to
      // double quotes (using prettier's default configuration).
    },
    {
      // This test will pass if and only if code has not changed.
      code: '"hello";'
      // To prevent false negatives (like with reckless use of `npx jest -u`),
      // snapshots of code that does not change are forbidden. Snapshots
      // succeed only when babel output !== code input.
      snapshot: false;
    },
    {
      code: 'var hello = "hi";',
      output: 'var olleh = "hi";',
      // You can't take a snapshot and also manually specify an output string.
      // It's either one or the other.
      snapshot: false
    },
    // A valid test can be a test object or a simple string.
    `
      function sayHi(person) {
        return 'Hello ' + person + '!'
      }
      console.log(sayHi('Jenny'))
    `
  ]
});

Full Example

import path from 'node:path';
import { pluginTester } from 'babel-plugin-tester';
import identifierReversePlugin from '../identifier-reverse-plugin';

pluginTester({
  // One (and ONLY ONE) of the two following lines MUST be included.
  plugin: identifierReversePlugin,
  //preset: coolNewBabelPreset,

  // Usually unnecessary if it is returned by the plugin. This will default to
  // 'unknown plugin' if a name cannot otherwise be inferred.
  pluginName: 'identifier reverse',
  // Unlike with pluginName, there is no presetName inference. This will default
  // to 'unknown preset' if a name is not provided.
  //presetName: 'cool-new-babel-preset',

  // Used to test specific plugin options.
  pluginOptions: {
    optionA: true
  },
  //presetOptions: {
  //  optionB: false,
  //}

  // Defaults to the plugin name.
  title: 'describe block title',

  // Only useful if you are using fixtures, codeFixture, outputFixture, or
  // execFixture options. Defaults to the absolute path of the file the
  // pluginTester function was invoked from, which in this case  is equivalent
  // to the following line:
  filepath: __filename,

  // These are the defaults that will be lodash.mergeWith'd with the provided
  // babelOptions option.
  babelOptions: {
    parserOpts: {},
    generatorOpts: {},
    babelrc: false,
    configFile: false
  },

  // Defaults to false but we're being explicit here: do not use snapshots
  // across all tests. Note that snapshots are only guaranteed to work with
  // Jest.
  snapshot: false,

  // Defaults to a function that formats with prettier.
  formatResult: customFormatFunction,

  // You can provide tests as an object:
  tests: {
    // The key is the title. The value is the code that is unchanged (because
    // snapshot == false across all tests). Test title will be: "1. does not
    // change code with no identifiers".
    'does not change code with no identifiers': '"hello";',

    // Test title will be: "2. changes this code".
    'changes this code': {
      // Input to the plugin.
      code: 'var hello = "hi";',
      // Expected output.
      output: 'var olleh = "hi";'
    }
  },

  // Alternatively, you can provide tests as an array:
  tests: [
    // Should be unchanged by the plugin (because snapshot == false across all
    // tests). Test title will be: "1. identifier reverse".
    '"hello";',
    {
      // Test title will be: "2. identifier reverse".
      code: 'var hello = "hi";',
      output: 'var olleh = "hi";'
    },
    {
      // Test title will be: "3. unchanged code".
      title: 'unchanged code',
      // Because this is an absolute path, the filepath option above will not
      // be used to resolve this path.
      codeFixture: path.join(
        __dirname,
        '..',
        'fixtures',
        'codeFixture-unchanging.js'
      )
      // No output, outputFixture, or snapshot, so the assertion will be that
      // the plugin does not change this code.
    },
    {
      // Because these are not absolute paths, they will be joined with the
      // directory of the filepath option provided above.
      codeFixture: path.join('..', 'fixtures', 'codeFixture.js'),
      // Because outputFixture is provided, the assertion will be that the
      // plugin will change the contents of "codeFixture.js" to the contents of
      // "outputFixture.js".
      outputFixture: path.join('..', 'fixtures', 'outputFixture.js')
    },
    {
      // As a convenience, this will have the indentation striped and it will
      // be trimmed.
      code: `
        function sayHi(person) {
          return 'Hello ' + person + '!';
        }
      `,
      // This will take a Jest snapshot, overwriting the default/global
      // settings (set above). The snapshot will contain both source code and
      // the transformed output, making the snapshot file easier to understand.
      snapshot: true
    },
    {
      code: 'var hello = "hi";',
      output: 'var olleh = "hi";',
      // This can be used to overwrite pluginOptions (set above).
      pluginOptions: {
        optionA: false
      }
      // This can be used to overwrite presetOptions (set above).
      //presetOptions: {
      //  optionB: true
      //}
    },
    {
      title: 'unchanged code',
      code: '"no change";',
      setup() {
        // Runs before this test.
        return function teardown() {
          // Runs after this tests.
        };
        // Can also return a promise.
      },
      teardown() {
        // Runs after this test.
        // Can return a promise.
      }
    },
    {
      // This source will be transformed just like the code property, except the
      // produced code will be evaluated in the same CJS context as the test
      // runner. This lets us make more advanced assertions on the output.
      exec: `
        const hello = "hi";
        // The plugin will reverse ALL identifiers, even globals like "expect"!
        tcepxe(hello)['toBe']("hi");
      `
    }
  ]
});

Fixtures Examples

See fixtures for an example directory layout or check out the use of babel-plugin-tester fixtures in some of these other projects:

Appendix

Testing Framework Compatibility

This package was originally tested on and built to work with Jest, but it is also tested against Vitest, Mocha, Jasmine, and node:test. See below for details.

Jest

All babel-plugin-tester features work with Jest. No further action is necessary πŸš€

Vitest

All babel-plugin-tester features work with Vitest, though Vitest does not provide global APIs by default. You can either supply some interoperability code (see Jasmine or node:test below for an example) or run Vitest with the --globals CLI option.

Mocha

Most babel-plugin-tester features work with Mocha, except Mocha does not natively support snapshots.

Jasmine

Most babel-plugin-tester features work with Jasmine if you define the appropriate globals:

import { pluginTester } from 'babel-plugin-tester';

globalThis.it.skip = globalThis.xit;
globalThis.it.only = globalThis.fit;

pluginTester(...);

However, Jasmine does not natively support snapshots.

node:test

Most babel-plugin-tester features work with node:test if you define the appropriate globals:

import { describe, it } from 'node:test';
import { pluginTester } from 'babel-plugin-tester';

globalThis.describe = describe;
globalThis.it = it;
// globalThis.it.skip = ... (weirdly, this is already defined)
globalThis.it.only = (...args) => it(args[0], { only: true }, args[1]);

pluginTester(...);

However, node:test does not natively support snapshots.

Other Frameworks

Other testing frameworks and test runners should also work so long as they define standard describe and it globals with async support, or appropriate interoperability code is used like in the above Jasmine and node:test examples.

Using Babel for Configuration Loading

babelOptions.babelrc and babelOptions.configFile are set to false by default. This way, you can manually import (or provide an object literal) the exact configuration you want to apply rather than relying on babel's somewhat complex configuration loading rules. However, if your plugin, preset, or project relies on a complicated external setup to do its work, and you do not mind the default run order, you can leverage babel's automatic configuration loading via the babelOptions.babelrc and/or babelOptions.configFile options.

Fixtures provided via the fixtures option do not need to provide a separate babelOptions.filename since it will be set automatically. This section only applies to test objects.

When relying on babelOptions.babelrc, you must also provide a babelOptions.filename for each test object that does not include a codeFixture or execFixture property. For example:

pluginTester({
  plugin,
  tests: [
    {
      code: '"blah"',
      // This configuration is set at the test level
      babelOptions: {
        babelrc: true,
        filename: path.join(__dirname, 'some-file.js')
      }
    },
    {
      code: '"hi"',
      // This configuration is set at the test level
      babelOptions: {
        babelrc: true,
        filename: path.join(__dirname, 'some-other-file.js')
      }
    },
    {
      // babelOptions.filename will be set to the value of codeFixture for you
      // unless you set it manually here at the test level
      codeFixture: path.join(__dirname, 'fixtures', 'my-file.js')
    },
    {
      // babelOptions.filename will be set to the value of execFixture for you
      // unless you set it manually here at the test level
      execFixture: path.join(__dirname, 'fixtures', 'my-script.js')
    }
  ]
});

This file does not actually have to exist either, so you can use whatever value you want for filename as long as the .babelrc file is resolved properly. Hence, the above example could be simplified further:

pluginTester({
  plugin,
  // This configuration is global: it applies to *all* tests by default!
  babelOptions: {
    babelrc: true,
    // The value of filename does not have to point to a file that exists
    filename: __filename
  },
  tests: [
    '"blah"',
    '"hi"',
    {
      // babelOptions.filename will be set to the value of codeFixture for you
      // unless you set it manually here at the test level
      codeFixture: path.join(__dirname, 'fixtures', 'my-file.js')
    },
    {
      // babelOptions.filename will be set to the value of execFixture for you
      // unless you set it manually here at the test level
      execFixture: path.join(__dirname, 'fixtures', 'my-script.js')
    }
  ]
});

pluginName Inference Caveat

Inferring pluginName during testing requires invoking the plugin at least twice: once outside of babel to check for the plugin's name and then again when run by babel. This is irrelevant to babel-plugin-tester (even if your plugin crashes when run outside of babel) and to the overwhelming majority of babel plugins in existence. This only becomes a problem if your plugin is aggressively stateful, which is against the babel handbook on plugin design.

For example, the following plugin which replaces an import specifier using a regular expression will exhibit strange behavior due to being invoked twice:

/*  -*-*-  BAD CODE DO NOT USE  -*-*-  */

let source;
// vvv When first invoked outside of babel, all passed arguments are mocks vvv
function badNotGoodPlugin({ assertVersion, types: t }) {
  // ^^^ Which means assertVersion is mocked and t is undefined ^^^
  assertVersion(7);

  // vvv So don't memoize `t` here (which among other things is poor design) vvv
  if (!source) {
    source = (value, original, replacement) => {
      return t.stringLiteral(value.replace(original, replacement));
    };
  }

  return {
    name: 'bad-bad-not-good',
    visitor: {
      ImportDeclaration(path, state) {
        path.node.source = source(
          path.node.source.value,
          state.opts.originalRegExp,
          state.opts.replacementString
        );
      }
    }
  };
}

pluginTester({
  plugin: badNotGoodPlugin,
  pluginOptions: { originalRegExp: /^y$/, replacementString: 'z' },
  tests: [{ code: 'import { x } from "y";', output: 'import { x } from "z";' }]
});

// Result: error!
// TypeError: Cannot read properties of undefined (reading 'stringLiteral')

If you still want to use global state despite the handbook's advice, either initialize global state within your visitor:

let source;
function okayPlugin({ assertVersion, types: t }) {
  assertVersion(7);

  return {
    name: 'okay',
    visitor: {
      Program: {
        enter() {
          // vvv Initialize global state in a safe place vvv
          if (!source) {
            source = (value, original, replacement) => {
              return t.stringLiteral(value.replace(original, replacement));
            };
          }
        }
      },
      ImportDeclaration(path, state) {
        path.node.source = source(
          path.node.source.value,
          state.opts.originalRegExp,
          state.opts.replacementString
        );
      }
    }
  };
}

pluginTester({
  plugin: okayPlugin,
  pluginOptions: { originalRegExp: /^y$/, replacementString: 'z' },
  tests: [{ code: 'import { x } from "y";', output: 'import { x } from "z";' }]
});

// Result: works!

Or do things the proper way and just use local state instead:

function betterPlugin({ assertVersion, types: t }) {
  assertVersion(7);

  // vvv Use local state instead so t is memoized properly vvv
  const source = (value, original, replacement) => {
    return t.stringLiteral(value.replace(original, replacement));
  };

  return {
    name: 'better',
    visitor: {
      ImportDeclaration(path, state) {
        path.node.source = source(
          path.node.source.value,
          state.opts.originalRegExp,
          state.opts.replacementString
        );
      }
    }
  };
}

pluginTester({
  plugin: betterPlugin,
  pluginOptions: { originalRegExp: /^y$/, replacementString: 'z' },
  tests: [{ code: 'import { x } from "y";', output: 'import { x } from "z";' }]
});

// Result: works!

Custom Snapshot Serialization

If you are using Jest and snapshots, then the snapshot output could have a bunch of bothersome \" to escape quotes. This is because, when Jest serializes a string, it will wrap everything in double quotes. This is not a huge deal, but it makes the snapshots harder to read, so we automatically add a snapshot serializer for you to remove those. Note that this serializer is added globally and thus will affect all snapshots taken in the test file, even those outside of babel-plugin-tester.

If you would like to disable this feature, then use the "pure" import (also disables formatting of babel output with prettier):

- import { pluginTester } from 'babel-plugin-tester'
+ import { pluginTester } from 'babel-plugin-tester/pure'

It seems recent versions of Jest already ship with easier-to-read snapshots, making this serializer redundant. Therefore, the built-in custom serializer will likely be removed entirely in a future version of babel-plugin-tester.

Formatting Output with Prettier

By default, a formatter is used which formats all babel output with prettier. It will look for a prettier configuration file relative to the file that's being tested or the current working directory. If it cannot find one, then it uses the default configuration for prettier.

This makes your snapshots easier to read and your expectations easier to write, but if you would like to disable this feature, you can either use the pure import to disable automatic formatting (along with snapshot serialization) or you can override the formatResult option manually like so:

pluginTester({
  // ...
  formatResult: (r) => r
  // ...
});

Built-In Debugging Support

This package uses debug under the hood. To view all possible debugging output, including the results of all babel transformations, set the DEBUG='babel-plugin-tester,babel-plugin-tester:*' environment variable when running your tests.

For example:

# Those using Windows (but not WSL) have to set environment variable differently
NODE_ENV='test' DEBUG='babel-plugin-tester,babel-plugin-tester:*' DEBUG_DEPTH='1' npx jest

Available Debug Namespaces

The following debug namespaces are available for activation:

  • babel-plugin-tester:index
  • babel-plugin-tester:formatter
  • babel-plugin-tester:serializer
  • babel-plugin-tester:tester
    • babel-plugin-tester:tester:resolve-base
    • babel-plugin-tester:tester:resolve-env
    • babel-plugin-tester:tester:normalize
      • babel-plugin-tester:tester:normalize:create-desc
      • babel-plugin-tester:tester:normalize:create-fix
      • babel-plugin-tester:tester:normalize:create-obj
    • babel-plugin-tester:tester:register
    • babel-plugin-tester:tester:wrapper
    • babel-plugin-tester:tester:test
    • babel-plugin-tester:tester:validate
    • babel-plugin-tester:tester:read-opts
    • babel-plugin-tester:tester:read-code
    • babel-plugin-tester:tester:eol
    • babel-plugin-tester:tester:finalize

The babel-plugin-tester:tester namespace and its sub-namespaces each have an additional verbose sub-namespace that can be activated or deactivated at will, e.g. babel-plugin-tester:tester:verbose and babel-plugin-tester:tester:wrapper:verbose.

For example, to view all debug output except verbose output:

# Those using Windows (but not WSL) have to set environment variable differently
NODE_ENV='test' DEBUG='babel-plugin-tester,babel-plugin-tester:*,-*:verbose' npx jest

TEST_ONLY/TEST_NUM_ONLY and TEST_SKIP/TEST_NUM_SKIP Environment Variables

The optional TEST_ONLY and TEST_SKIP environment variables are recognized by babel-plugin-tester, allowing you to control which tests are run in an adhoc fashion without having to modify your test configuration code.

The values of these variables will be transformed into regular expressions via RegExp(value, 'u') and matched against each test/fixture title (not including automatically assigned numbers prefixed to titles). Tests with titles that match TEST_ONLY will be run while all others are skipped. On the other hand, tests with titles that match TEST_SKIP will be skipped while others are run.

For example, to skip the test titled "this is the name of a failing unit test":

TEST_SKIP='name of a failing' npx jest

Given both TEST_ONLY and TEST_SKIP, tests matched by TEST_SKIP will always be skipped, even if they are also matched by TEST_ONLY. These environment variables also override both the fixture-specific skip/only and test object skip/only properties.

In addition to TEST_ONLY and TEST_SKIP, you can also target tests specifically by their automatically assigned number using TEST_NUM_ONLY and TEST_NUM_SKIP. These environment variables function identically to their counterparts except they accept one or more numbers separated by commas (spaces around commas are ignored) instead of regular expressions. Inclusive ranges (e.g. 4-9) are also supported.

For example, the following will skip tests numbered 1, 3, 5, and 6-10 (inclusive):

# Spaces around commas don't matter and sequential/final commas are ignored
TEST_NUM_SKIP='5,1, 6-10,,  3,' npx jest

TEST_NUM_ONLY and TEST_NUM_SKIP are meaningless if titleNumbering is false or your tests are otherwise unnumbered, and may match multiple tests if automatic numbering is restarted.

setup and teardown Run Order

For each test object and fixture test, setup and teardown functions are run in the following order:

  1. Base setup.
  2. Test object setup / fixture setup.
  3. Test object / fixture test is run.
  4. Any function returned by test object setup / fixture setup.
  5. Test object teardown / fixture teardown.
  6. Any function returned by base setup.
  7. Base teardown.

Inspiration

The API was inspired by:

Issues

Looking to contribute? Look for the Good First Issue label.

πŸ› Bugs

Please file an issue for bugs, missing documentation, or unexpected behavior.

See Bugs

πŸ’‘ Feature Requests

Please file an issue to suggest new features. Vote on feature requests by adding a πŸ‘. This helps maintainers prioritize what to work on.

See Feature Requests

Contributors ✨

Thanks goes to these people (emoji key):

Kent C. Dodds
Kent C. Dodds

πŸ’» πŸ“– πŸš‡ ⚠️
james kyle
james kyle

πŸ’» πŸ“– πŸ‘€ ⚠️
Brad Bohen
Brad Bohen

πŸ›
Kyle Welch
Kyle Welch

πŸ’» πŸ“– ⚠️
kontrollanten
kontrollanten

πŸ’»
RubΓ©n Norte
RubΓ©n Norte

πŸ’» ⚠️
AndrΓ© Neves
AndrΓ© Neves

πŸ’» ⚠️
Kristoffer K.
Kristoffer K.

πŸ’» ⚠️
Alex Kanunnikov
Alex Kanunnikov

πŸ’» ⚠️
Sebastian Silbermann
Sebastian Silbermann

πŸ’»
Andrey Los
Andrey Los

πŸ›
Charles Bodman
Charles Bodman

πŸ“–
MichaΓ«l De Boey
MichaΓ«l De Boey

πŸ’»
yuyaryshev
yuyaryshev

πŸ’»
Marek Buchar
Marek Buchar

πŸ’» ⚠️ πŸ“–
Jay Phelps
Jay Phelps

πŸ‘€
Mathias
Mathias

πŸ“–
joe moon
joe moon

πŸ’» ⚠️
Bernard
Bernard

πŸ’» ⚠️ πŸ“– πŸš‡ πŸ‘€ 🚧
Oleksandr Fediashov
Oleksandr Fediashov

πŸ›
Mateusz BurzyΕ„ski
Mateusz BurzyΕ„ski

πŸ’»
Steven Hargrove
Steven Hargrove

πŸ’» πŸ“–
Alex R Hogue
Alex R Hogue

πŸ’»
Arthur Zahorski
Arthur Zahorski

πŸ’»
EGOIST
EGOIST

πŸš‡
Ifiok Jr.
Ifiok Jr.

πŸ“¦
Jan BuschtΓΆns
Jan BuschtΓΆns

πŸ’»
Miguel Albernaz
Miguel Albernaz

πŸ’» πŸš‡
peterlebrun
peterlebrun

πŸ“–
Simen Bekkhus
Simen Bekkhus

πŸ›

This project follows the all-contributors specification. Contributions of any kind welcome!

License

MIT