Skip to content

a collection of packages to compile Tailwind CSS like shorthand syntax into CSS at runtime

License

Notifications You must be signed in to change notification settings

kenoxa/beamwind

Repository files navigation

Beamwind

a collection of packages to compile Tailwind CSS like shorthand syntax into CSS at runtime

MIT License Latest Release Bundle Size Github Typescript CI Coverage Status PRs Welcome

Read the docs | API | Change Log | ⚡️ Demo


This library takes inspiration from Tailwind CSS (see differences), Oceanwind (see differences) and Otion to provide means of efficiently generating mostly atomic styles from shorthand syntax and appending them to the DOM at runtime.

beamwind uses the tailwind default theme (@beamwind/preset-tailwind) and an opinionated set of base styles for modern browsers based on Tailwind Preflight (@beamwind/preflight):

import { bw } from 'beamwind'

document.body.className = bw`h-full bg-pink-700 rotate-3 scale-95`

⚡️ Check out the live and interactive demo

As an alternative @beamwind/system is a good start if you prefer a semantic naming scheme in your design system: ⚡️ Demo

For rapid prototyping @beamwind/play is the right choice. It combines @beamwind/preset-tailwind and @beamwind/preset-system with auto-conversion of unknown theme values: ⚡️ Demo


Table Of Contents (click to expand)

Key Features

Beamwind: a wind blowing against a vessel from a direction at right angles to its keel for optimal speed

Backstory

Design systems embrace a component-oriented mindset. Inspired by Tailwind CSS, utility classes provide reusable styles with no unwanted side-effects. However, they have to be generated upfront.

Atomicity generalizes the former concept by instantiating style rules on demand. Serving as a solid foundation for constraint-based layouts, atomic CSS-in-JS has come to fluorish at scale.

Usage

The following code examples will use beamwind. But the API is the same for all packages.

To use the library, first import the module then invoke the bw export using tagged template syntax (or one of the many other ways):

import { bw } from 'beamwind'

document.body.className = bw`h-full bg-purple-500 rotate-3 scale-95`

Running the above code will result in the following happening:

  1. Parse directives (h-full, bg-purple-500, rotate-3, scale-95)
  2. Translate directives into CSS rules using plugins (e.g. h-full -> { height: 100vh }).
  3. Inject each CSS rule with a unique class name into a library-managed style sheet
  4. Return a space-separated string of class names

If you are unfamiliar with the Tailwind CSS shorthand syntax please read the Tailwind documentation about Utility-First, Responsive Design and Hover, Focus, & Other States.

Variant Grouping

Directives with the same variants can be grouped using parenthesis. Beamwind will expand the nested directives; applying the variant to each directive in the group before translation. For example:

Notice any directives within tagged template literals can span multiple lines

bw`
  sm:hover:(
    bg-black
    text-white
  )
  md:(bg-white hover:text-black)
`
// => sm:hover:bg-black sm:hover:text-white md:bg-white md:hover:text-black

It is possible to nest groupings too, for example:

bw`
  sm:(
    bg-black
    text-white
    hover:(bg-white text-black)
  )
`
// => sm:hover:bg-black sm:hover:text-white sm:hover:bg-white sm:hover:text-black

Object values which are String, Array or Object start a new variant group:

bw({
  sm: {
    'bg-black': true,
    'text-white': true,
    hover: 'bg-white text-black',
  },
})
// => sm:hover:bg-black sm:hover:text-white sm:hover:bg-white sm:hover:text-black

Two things to note here is that the outermost variant should always be a responsive variant (just like in tailwind hover:sm: is not supported) and that nesting responsive variants doesn't make sense either, for example sm:md: is not supported.

Directive Grouping

Directives with the same prefix can be grouped using parenthesis. Beamwind will expand the nested directives; applying the prefix to each directive in the group before translation. For example:

bw`text(center bold gray-500)`)
// => text-center text-bold text-gray-500

bw`divide(y-2 blue-500 opacity(75 md:50))`
// => divide-y-2 divide-blue-500 divide-opacity-75 md:divide-opacity-50

bw`w(1/2 sm:1/3 lg:1/6) p-2`
// => w-1/2 sm:w-1/3 lg:w-1/6 p-2

Some directives like ring need to be applied as is. For that case you can use the special & directive which is replaced with the current prefix:

bw`ring(& ping-700 offset(4 ping-200))`)
// => ring ring-ping-700 ring-offset-4 ring-offset-on-ping-200

Negated values can be used within the braces and will be applied to the directive:

bw`rotate(-3 hover:6 md:(3 hover:-6))`
// => -rotate-3 hover:rotate-6 md:rotate-3 md:hover:-rotate-6"

Function Signature

It is possible to invoke beamwind in a multitude of different ways. The bw function can take any number of arguments, each of which can be an Object, Array, Boolean, Number, String or inline plugins. This feature is based on clsx.

Important: Any falsey values are discarded! Standalone Boolean and Number values are discarded as well.

For example:

// Tag Template Literal (falsey interpolations will be omitted)
bw`bg-gray-200 rounded`
//=> 'bg-gray-200 rounded'

bw`bg-gray-200 ${false && 'rounded'}`
//=> 'bg-gray-200'

bw`bg-gray-200 ${[false && 'rounded', 'block']}`
//=> 'bg-gray-200 block'

bw`bg-gray-200 ${{ rounded: false, underline: isTrue() }}`
//=> 'bg-gray-200 underline'

// Strings (variadic)
bw('bg-gray-200', true && 'rounded', 'underline')
//=> 'bg-gray-200 rounded underline'

// Objects (keys with falsey values will be omitted)
bw({ 'bg-gray-200': true, rounded: false, underline: isTrue() })
//=> 'bg-gray-200 underline'

// Objects (variadic)
bw({ 'bg-gray-200': true }, { rounded: false }, null, { underline: true })
//=> 'bg-gray-200 underline'

// Arrays (falsey items will be omitted)
bw(['bg-gray-200', 0, false, 'rounded'])
//=> 'bg-gray-200 rounded'

// Arrays (variadic)
bw(['bg-gray-200'], ['', 0, false, 'rounded'], [['underline', [['text-lg'], 'block']]])
//=> 'bg-gray-200 rounded underline text-lg block'

// Kitchen sink (with nest
bw(
  'bg-gray-200',
  [1 && 'rounded', { underline: false, 'text-secondary': null }, ['text-lg', ['shadow-lg']]],
  'uppercase',
)
//=> 'bg-gray-200 rounded text-lg shadow-lg uppercase'

For advanced use cases bw additionally accepts inline plugins.

Directive Factories

Often you will find yourself in a position to need an abstraction to simplify the creation of directives. A best practice is to create a function that returns the required directives:

const btn = (color) => {
  if (color) {
    return `bg-${color} hover:bg-${color}-hover active:bg-${color}-active`
  }

  return 'inline-block font-bold py-2 px-4 rounded'
}

bw(btn())
// => inline-block font-bold py-2 px-4 rounded

bw([btn(), btn('primary')])
// => inline-block font-bold py-2 px-4 rounded bg-primary hover:bg-primary-hover active:bg-primary-active

This can be converted into a component plugin.

Extending the default theme

The Theming section provides detailed insight into the theming options.

Importing and invoking bw directly will cause it to use the default theme of that package. To customize the theme, use the setup export. This will change the theme used by the bw export.

import { bw, setup } from 'beamwind'

setup({
  theme: {
    extend: {
      colors: {
        'red-500': '#DC2626',
      },
    },
  },
})

bw`bg-red-500` // will result in a #DC2626 background-color

setup can be called multiple times where each call extends the existing configuration.

Alternatively you can create a own instance:

import { createInstance } from 'beamwind'

const cx = createInstance({
  theme: {
    extend: {
      colors: {
        'red-500': '#DC2626',
      },
    },
  },
})

cx`bg-red-500` // will result in a #DC2626 background-color

Packages

ready to use instances

These packages all use @beamwind/core under the hood which only has a minimal theme intended to be used as base to be further configured.

The Instance Creation section show how to implement your own packages.

Presets

reusable configuration presets to create your own instance

Base Styles

opinionated base styles that are designed to smooth over cross-browser inconsistencies and make it easier to work within the constraints of your design system.

Helpers

Examples

Theming

Customizing the default theme for your project.

The theme property of the setup options follows Tailwinds Theme Configuration It contains keys for screens, colors, and spacing, as well as a key for each customizable core plugin. The core theme is bare bones as most projects have their own colors, sizes, ... and naming system. A tailwind like theme is available via @beamwind/preset-tailwind. For a full featured design system take a look at @beamwind/preset-system.

API / Themes provides an overview of all possible values and differences are highlighted here.

To customize the theme call setup with a theme property:

import { setup } from 'beamwind'

setup({
  theme: {
    /* ... */
  },
})

If the new theme needs to access the existing theme a function can be used:

setup({
  theme: (theme) => {
    return {
      extend: {
        colors: {
          important: theme('colors', 'critical'),
        },
      },
    }
  },
})

Referencing other values

If you need to reference another value in your theme, you can do so by providing a closure instead of a static value. The closure will receive a theme() function that you can use to look up other values in your theme.

setup({
  theme: {
    colors: {
      important: (theme) => theme('colors', 'critical', 'red' /* Fallback */),
    },
    fill: (theme) => theme('colors'),
  },
})

Colors

The colors key allows you to customize the global color palette for your project.

By default, these colors are inherited by all color-related core plugins, like borderColor, divideColor, placeholderColor and others.

Please note the following guidelines:

  1. This is a flat object ({ 'gray-50': '#f9fafb' }) not a nested on like in tailwind ({ 'gray': { 50: '#f9fafb' } }) uses.
  2. Colors should be in #-hexadecimal notation (like #RRGGBB or #RGB) to work best with opacity plugins like text-opacitiy or bg-opacitiy.

"On" colors

provide accessible contrast to their base color - on-primary or on-surface

Whenever elements, such as text or icons, appear in front of surfaces, those elements should use colors designed to be clear and legible against the colors behind them. When a color appears "on" top of a primary color, it’s called an "on primary color". They are labelled using the original color category (such as primary color) with the prefix on-.

"On" colors are primarily applied to text, iconography, and strokes. Sometimes, they are applied to surfaces.

The bg-<color> directive adds a default color CSS declaration if a corresponding on-* color is found. The reverse works as well: bg-on-<color> adds bg-<color> as color. The color set in a way it can be overridden using text-<color> (see Selector Ordering).

Dark Mode

Now that dark mode is a first-class feature of many operating systems, it's becoming more and more common to design a dark version of your website to go along with the default design.

To make this as easy as possible, beamwind includes a dark variant that lets you style your site differently when dark mode is enabled:

bw`
  bg-white text-black
  dark:(bg-gray-800 text-white)`

It's important to note that the dark mode variant is always enabled and available for all directives.

Now whenever dark mode is enabled on the user's operating system, dark:{directive} classes will take precedence over unprefixed classes. The media strategy uses the prefers-color-scheme media feature under the hood, but if you'd like to support toggling dark mode manually, you can also use the class strategy for more control:

setup({
  darkMode: 'class', // default is 'media'
  darkModeClass: 'dark-mode', // optional, default is 'dark'
})

For an example how to toggle dark mode manually read the Tailwind Guide.

Plugins

Conceptual there are two kind of plugins:

  1. Utilities: these may return a CSS rule object to be injected into the page (eq: { 'text-color': 'red' })
  2. Components: these return directives which are resolved by utilities to their final class name (eq: 'bg-white text-red')

New plugins can be provided using the setup method. setup can be called multiple times where each call extends the existing configuration.

Plugins are searched for by name using the longest prefix before a dash ("-"'). The name and the remaining parts (splitted by a dash) are provided as first argument to the plugin function. For example if the directive is bg-gradient-to-t the following order applies:

Plugin Parts
bg-gradient-to-t ["bg-gradient-to-t"]
bg-gradient-to ["bg-gradient-to", "t"]
bg-gradient ["bg-gradient", "to", "t"]
bg ["bg", "gradient", "to", "t"]

Adding New Utilities

Although beamwind provides a pretty comprehensive set of utility classes out of the box, you may run into situations where you need to add a few of your own.

Static utilities can be defined as CSS declaration objects:

setup({
  plugins: {
    'text-important': { color: 'red' },
  },
})

Sometimes a pseudo class or child selector maybe required. In these case use an array where the first value is appended to the generated class name in the selector and the second value is CSS declaration object. The next example shows how this can be use to implement a Stretched link:

setup({
  plugins: {
    'stretched-link': [
      '::after',
      {
        position: 'absolute',
        top: '0',
        right: '0',
        bottom: '0',
        left: '0',
        'z-index': '1',
        content: '""',
      },
    ],
  },
})

Most utilities have some logic based on the directive value or need access to the current theme. For these case you can define a function which needs to return a CSS rule object, a decorator / rule tuple or a falsey value if it couldn't handle the provided directive parts.

Dynamic plugins should be side-effect free and produce the same output given the same parameters.

Lets define a basic scroll-snap utility. It should support these directives:

  • scroll-snap-none
  • scroll-snap-x
  • scroll-snap-y
setup({
  plugins: {
    // The directive splitted by '-' with the plugin name as first value: ['scroll-snap', ...]
    'scroll-snap': (parts) => {
      return { 'scroll-snap-type': parts[1] }
    },
  },
})

As we are passing the second part through this additionally supports other values like block, inline, both, ... Note: This may lead to invalid css.

What this currently does not support is something like scroll-snap-both-mandatory. Lets fix that:

// ... same as before
return { 'scroll-snap-type': parts.slice(1).join(' ') }

As this is quite common beamwind provides two helper (join and tail) for this:

import { join, tail } from 'beamwind'

// ... same as before
return { 'scroll-snap-type': join(tail(parts), ' ') }

As a second parameter the plugin function receives a theme value accessor. We can use that to access configured theme values. This allows to provide aliases for directive parts. Lets allow these directives:

  • scroll-snap - using a default value
  • scroll-snap-proximity - using a theme value
setup({
  // Add a new theme section; Not needed if you are re-using existing theme sections
  theme: {
    scroll: {
      // eg: 'scroll-snap' => { scroll-snap-type: both; }
      DEFAULT: 'both',
      // eg: 'scroll-snap-proximity' => { scroll-snap-type: both proximity; }
      proximity: 'both proximity',
    }
  }

  plugins: {
    'scroll-snap': (parts, theme) => {
      // Omit the first element which is 'scroll-snap'
      return { 'scroll-snap-type': theme('scroll', tail(parts)) }
    }
  }
})

This will fail for unknown theme values. To support the previous directives like scroll-snap-x we need to mark the value resolution as optional and provide a fallback value:

return {
  'scroll-snap-type': theme('scroll', tail(parts), true /* Optional */) || join(tail(parts), ' '),
}

Responsive and state variants (like hover, focus, ...) are automatically applied. Opposed to tailwind there is no need to define which one should be applied.

Optional Typescript does not recognize this new section. Using module augmentation we can announce the new property:

declare module '@beamwind/types' {
  interface Theme {
    scroll: ThemeSection
  }
}

Adding New Components

Components define a set of utility directives that should be applied if the component directive is used.

Static components that are only a collection of utilities can be defined as strings. The following example allows to use card as a directive (bw('card')) which will be expanded to max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-2xl:

setup({
  plugins: {
    card: 'max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-2xl',
  },
})

Dynamic components can be implemented as function which should return a string of directives to be applied.

setup({
  plugins: {
    btn(parts) {
      if (parts[1]) {
        return `bg-${parts[1]} hover:bg-${parts[1]}-hover active:bg-${parts[1]}-active`
      }

      return 'font-bold py-2 px-4 rounded'
    },
  },
})

Some dynamic components depend on additional logic and like to use the familiar bw API. For these cases beamwind provides the apply helper, which has the same API signature as bw.

import { setup, apply } from 'beamwind'

setup({
  plugins: {
    btn(parts, theme) {
      if (parts[1]) {
        return apply`bg-${parts[1]} ${{ rounded: parts[1] === 'rounded }}`
      }

      return 'font-bold py-2 px-4 rounded'
    },
  },
})

This is inspired by @apply directive from Tailwind.

Inline Plugins

A global plugin registry (per beamwind instance) has it's downsides as each key/name must be unique. beamwind allows to define inline plugins (API doc) which are just like normal plugins without the first parameter.

Please take a look at Plugin API documentation for further details about what a plugin can do.

const card = (theme) => ({
  display: block
  border: `1px solid ${theme('colors', 'primary')}`,
  'text-align': 'center',
})

bw`font-bold ${card} text-primary`
// => font-bold __card_1 text-primary

bw('font-bold', card, 'text-primary')
// => font-bold __card_1 text-primary

// You can use variants
bw`sm:${card} text-primary`
// => font-bold sm:__card_1 text-primary

bw({ sm: card }, 'text-primary')
// => font-bold sm:__card_1 text-primary

Note: This should be a last resort as it comes with a small performance penalty.

Plugin Helper Functions

beamwind provides a small set of helper functions to write your own plugins:

Global Styles

Sometimes global CSS styles are required. beamwind support injection of CSS during the initialization using init:

setup({
  init(insert, theme) {
    insert(`body{margin:${theme.spacing.xl}}`)
  },
})

Catching Errors

By default warnings about missing translations will be written to the console.

It is possible to make beamwind throw an error rather just warn by opting into strict mode:

import { strict } from 'beamwind'

setup({ mode: strict })

To fully customize the error handling you can provide a warn function:

import { mode } from 'beamwind'

setup({
  mode: mode((message) => {
    /* ... */
  }),
})

The exports warn, strict and mode exports are re-exported by all packages.

For advanced use case you can implement your own mode:

import { mode } from 'beamwind'

setup({
  mode: {
    unknown(section, keypath, optional, theme) {
      /* ... */
    },
    warn(message, directive) {
      /* ... */
    },
  },
})

For an example see @beamwind/preset-play.

Autoprefix

A custom auto-prefixer method may be used as a replacement for the built-in tiny-css-prefixer:

import { prefix as stylisPrefix } from 'stylis' // v4

setup({
  // A custom solution which weighs more than the default
  prefix: (property, value) => stylisPrefix(`${property}:${value};`, property.length).slice(0, -1),
})

Hashed Class Names

beamwind uses hashed class names (like _1m4vcp0) by default. During development or testing it maybe useful to use readable class names:

setup({ hash: false })

Alternatively a custom hash function can be provided:

import hash from '@emotion/hash'

setup({ hash: (string) => '_' + hash(string) })

Injector

An injector is responsible for adding a CSS rule to its underlying target.

During testing or server-side rendering the virtualInjector should be used:

import { virtualInjector } from 'beamwind'

const injector = virtualInjector()
setup({ injector })

injector.target // Array with sorted CSS rulesText

Instance creation

There are some case where you need to create separate instance of beamwind:

  • for reduced bundle size by using a own preset
  • IE 11 support using reset

Basically you need to do the following:

// Choose either reset or preflight
import preflight from '@beamwind/preflight'
import { createInstance } from '@beamwind/core'

import yourTheme from './your-theme'

export const { bw, setup, theme } = createInstance([preflight(), yourTheme])

You can use one of the existing packages as a template.

Multiple Browsing Contexts

Creating a own beamwind instance is required if you want to manage styles of multiple browsing contexts (e.g. an <iframe> besides the main document).

import { createInstance, cssomInjector } from 'beamwind'

const iframeDocument = document.getElementsByTagName('iframe')[0].contentDocument

export const { bw } = createInstance({
  injector: cssomInjector({
    // Make sure this node exists or create it on the fly if necessary
    target: iframeDocument.getElementById('__beamwind'),
  }),
})

This option should be used along with a custom target for injection.

Server-side rendering (SSR)

Beamwind supports SSR. Consider the following example:

import { h } from 'preact'
import render from 'preact-render-to-string'
import htm from 'htm'
import { createInstance, virtualInjector } from 'beamwind'

const injector = virtualInjector()
const { bw } = createInstance({ injector })

const html = htm.bind(h)
const style = {
  main: bw`clearfix`,
}

const app = html`<main className=${style.main}>hello beamwind</main>`
const appHtml = render(app)
const styleTag = `<style id="__beamwind">${injector.target.join('\n')}</style>`

// Inject styleTag to your HTML now.

We are planning to implement the style tag rendering via getStyleTag(injector) in a @beamwind/ssr package. The idea is that @beamwind/ssr would optimize the styles instead of simply joining them.

nonce

In order to prevent harmful code injection on the web, a Content Security Policy (CSP) may be put in place. During server-side rendering, a cryptographic nonce (number used once) may be embedded when generating a page on demand:

import { virtualInjector } from 'beamwind'

// Usage with webpack: https://webpack.js.org/guides/csp/
const injector = virtualInjector({ nonce: __webpack_nonce__ })

The same nonce parameter should be supplied to the client-side:

import { setup } from 'beamwind'

setup({ nonce: __webpack_nonce__ })

target

Changes the destination of the injected rules. By default, a <style id="__beamwind"> element in the ` during runtime, which gets created if unavailable.

Selector Ordering

This section describes the internal ordering of the generated CSS rules. beamwind takes care of this

beamwind creates, except for a few exceptions, one CSS rule with a single class as selector per directive. This means thy have all the same specificity.

If two declarations have the same weight, origin, and specificity, the latter specified wins.

Some directives depend on each other. For example the via-<color> must be declared after the from-<color> directive. As a result beamwind has to ensure that all CSS rules are in a specific order.

The following rules apply in deterministically sort the injected CSS rules:

  • media queries are sorted in a mobile first manner using the min-width value
  • other at-rules - based on -:;,#( counting
  • pseudo classes and variants are sorted in the following order: first, last, odd, even, link, visited, empty, checked, group-hover, group-focus, focus-within, hover, focus, focus-visible, active, disabled, others - meaning that active overrides focus and hover for example (see When do the :hover, :focus, and :active pseudo-classes apply?
  • number of declarations (descending) - this allows single declaration styles to overwrite styles from multi declaration styles
  • greatest precedence of properties (ignoring vendor prefixed and custom properties) based on - counting - shorthand properties are inserted first eg longhand properties override shorthands.
  • greatest precedence of values based on counting -:;,#(

Roadmap

TODO see TODO.md

Tailwind Differences

Beamwind aims to support with Tailwind v2 and v1 (see IE 11 Compatibilty). Some features are not yet implemented.

  • beamwind supports variant and directive grouping to reduce the overwhelming maze Tailwind sometimes creates
  • beamwind theme has a slightly different format:
  • beamwind supports "on" colors
  • beamwind automatically infers negated values - they do not need to be in the theme config

Missing Features

The following Tailwind v2 features are not yet available in beamwind:

Did we miss a feature? Please open a an issue or contribute a pull request.

Additional Features

  • d-*: allows to set the display property (from bootstrap)

  • appearance-*: supports all values

  • bg-<color>: if a "on" color) (on-<color>) is found it is used as default CSS color; to change to a different color use text-<color> (tailwind style)

  • bg-gradient-to-* is built-in, no need to configure these

  • text-underline, text-no-underline, text-line-through, text-uppercase, text-lowercase and text-capitalize: this allows grouping of text directives like text(lg red-500 capitalize underline)

  • font-italic and font-no-italic: this allows grouping of font directives like font(sans italic bold)

  • border and divide allow to combine positions (top, rrigh, left, bottom)

    • tr - top & right
    • brl - bottom, right and left

    Note x and y can not be combined.

  • rotate, scale , skew and translate provide a fallback for IE 11

    transform rotate-45 works but when using transform rotate-45 scale-150 only one of both is applied.

IE 11 compatibility

Some new tailwind features use CSS Variables (Custom Properties) and are therefore not compatible with IE 11.

Beamwind includes fallbacks for the following directives which mimic Tailwind v1 behavior:

Some directive only work with CSS Variables and are not supported in IE 11:

Oceanwind Differences

All Tailwind Differences apply as well.

This library is heavily inspired and based on oceanwind. Without the work of Luke Jackson this would not have been possible! He wrote a great blog post about the development of oceanwind.

Some notable differences are:

Size Comparison

  oceanwind beamwind
minified Oceanwind Minified Beamwind Minified
gzipped Oceanwind Gzipped Beamwind Gzipped

Browser Support

All versions of Node.js are supported.

All browsers that support Array.isArray and Object.keys are supported (IE9+).

Some directives use fallbacks for IE 11.

Acknowledgements

Without these libraries and their authors this would not have been possible:

Contribute

Thanks for being willing to contribute!

This project is free and open-source, so if you think this project can help you or anyone else, you may star it on GitHub. Feel free to open an issue if you have any idea, question, or you've found a bug.

Working on your first Pull Request? You can learn how from this free series How to Contribute to an Open Source Project on GitHub

We are following the Conventional Commits convention.

Develop

Clone the repository and cd into the project directory.

Run yarn install && yarn build.

Cd into the package that you'd like to make progress on.

  • yarn test: Run test suite including linting
  • yarn format: Ensure consistent code style
  • yarn build: Build all packages
  • yarn publish: To publish all changed packages

Sponsors

Kenoxa GmbH Kenoxa GmbH

License

MIT © Kenoxa GmbH