3.0.0 • Published 4 months ago

flooent v3.0.0

Weekly downloads
21
License
MIT
Repository
github
Last release
4 months ago

flooent

npm bundle size latest version Coverage Status

Fluent interface to provide an expressive syntax for common manipulations. Rather than enforcing a different paradigm, flooent builds upon and extends the native capabilities of various JavaScript objects.

Given you have logical, procedural, "hard to visualize" code like this:

// given we have const path = 'App/Controllers/user.js'
const filename = path.substring(path.lastIndexOf('/') + 1)
let name = filename.substring(0, filename.lastIndexOf('.'))
if (!name.endsWith('Controller')) name+= 'Controller'
return name.substring(0, 1).toUpperCase() + name.substring(1)

refactor it into plain English

// given we have const path = 'App/Controllers/user.js'
given.string(path)
  .afterLast('/')
  .beforeLast('.')
  .endWith('Controller')
  .capitalize()
  .valueOf()

Try it out online!

Index

Get Started

Installation

npm install flooent

given

Use given to create either a flooent Array, Map or String.

import { given } from 'flooent'

given.string('hello') // instance of Stringable
given.array([1, 2]) // instance of Arrayable
given.map(new Map) // or given.map([['key', 'value']]) | instance of Mappable
given.map.fromObject({ key: 'value' }) // instance of Mappable

Flooent objects only extend the native functionality, so you can still execute any native method like given.string('hello').includes('h').

To turn flooent objects back into their respective primitive form, use the valueOf() method.

given.string('hello').valueOf()

Best Practices

After transforming your data, convert the object back to its primitive form before passing it to another function or returning it.

This is to avoid naming collisions with possibly new native methods:

import { given } from 'flooent'
import { arrayToCsv } from 'some-csv-lib'

// Avoid this
const sortedItems = given.array(items).sortAsc('id')
arrayToCsv(sortedItems)

// instead, do this
const sortedItems = given.array(items).sortAsc('id').valueOf()
arrayToCsv(sortedItems)

Constraints

Back to top

The contraints that apply to flooent strings are the same that apply to when you new up a native string using new (new String('')) and is just how JavaScript works.

For one, the type will be object instead of string.

typeof given.string('') // object
typeof '' // string

Flooent strings are immutable. You can still do things like this:

given.string('?') + '!' // '?!'

which will return a primitive (not an instance of flooent).

However you can not mutate flooent objects like this:

given.string('') += '!' // ERROR

There are various fluent alternatives available.

Functional API

If you only need to do a single thing, you can also import most functions individually. The result of these functions will not be turned into a flooent object.

import { afterLast } from 'flooent/string'
afterLast('www.example.com', '.') // 'com'

import { move } from 'flooent/array'
move(['music', 'tech', 'sports'], 0, 'after', 1) // ['tech', 'music', 'sports']

import { rename } from 'flooent/map'
rename(new Map([['item_id', 1]]), 'item_id', 'itemId') // Map { itemId → 1 }

import { rename } from 'flooent/object'
rename({ item_id: 1 }), 'item_id', 'itemId') // { itemId: 1 }

Strings

Back to top

You have access to everything from the native String object.

Fluent methods

pipe

Executes the callback and transforms the result back into a flooent string if it is a string. Useful for creating reusable functions for specific method combinations, or for continuing the chain when using non-flooent functions.

const append = str => str.append('!').prepend('!!')

given.string('').pipe(append) // String { '!' }

after

Returns the remaining text after the first occurrence of the given value. If the value does not exist in the string, the entire string is returned unchanged.

given.string('sub.domain.com').after('.') // String { 'domain.com' }

afterLast

Returns the remaining text after the last occurrence of the given value. If the value does not exist in the string, the entire string is returned unchanged.

given.string('sub.domain.com').afterLast('.') // String { 'com' }

before

Returns the text before the first occurrence of the given value. If the value does not exist in the string, the entire string is returned unchanged.

given.string('sub.domain.com').before('.') // String { 'sub' }

beforeLast

Returns the text before the last occurrence of the given value. If the value does not exist in the string, the entire string is returned unchanged.

given.string('sub.domain.com').beforeLast('.') // String { 'sub.domain' }

append

Alias for concat. Appends the given value to string.

given.string('hello').append(' world') // String { 'hello world' }

prepend

Prepends the given value to string.

given.string('world').prepend('hello ') // String { 'hello world' }

endWith

Appends the given value only if string doesn't already end with it.

given.string('hello').endWith(' world') // String { 'hello world' }
given.string('hello world').endWith(' world') // String { 'hello world' }

startWith

Prepends the given value only if string doesn't already start with it.

given.string('world').startWith('hello ') // String { 'hello world' }
given.string('hello world').startWith('hello ') // String { 'hello world' }

limit

Truncates text to given length and appends second argument if string got truncated.

given.string('The quick brown fox jumps over the lazy dog').limit(9) // The quick...
given.string('The quick brown fox jumps over the lazy dog').limit(9, ' (Read more)') // The quick (Read more)
given.string('Hello').limit(10) // Hello

tap

Tap into the chain without modifying the string.

given.string('')
  .append('!')
  .tap(str => console.log(str))
  .append('!')
  // ...

when

Executes the callback if first given value evaluates to true. Result will get transformed back into a flooent string if it is a raw string.

// can be a boolean
given.string('').when(true, str => str.append('!')) // String { '!' }
given.string('').when(false, str => str.append('!')) // String { '' }

// or a method
given.string('hello').when(str => str.endsWith('hello'), str => str.append(' world')) // String { 'hello world' }
given.string('hi').when(str => str.endsWith('hello'), str => str.append(' world')) // String { 'hello' }

whenEmpty

Executes the callback if string is empty. Result will get transformed back into a flooent string if it is a raw string.

given.string('').whenEmpty(str => str.append('!')) // String { '!' }
given.string('hello').whenEmpty(str => str.append('!')) // String { 'hello' }

wrap

Wraps a string with the given value.

given.string('others').wrap('***') // String { '***others***' }
given.string('oldschool').wrap('<blink>', '</blink>') // String { '<blink>oldschool</blink>' }

unwrap

Unwraps a string with the given value.

given.string('***others***').unwrap('***') // String { 'others' }
given.string('<blink>oldschool</blink>').unwrap('<blink>', '</blink>') // String { 'oldschool' }

camel

Turns the string into camel case.

given('foo bar').camel() // String { 'fooBar' }

title

Turns the string into title case.

given.string('foo bar').title() // String { 'Foo Bar' }

studly

Turns the string into studly case.

given('foo bar').studly() // String { 'FooBar' }

capitalize

Capitalizes the first character.

given.string('foo bar').capitalize() // String { 'Foo bar' }

kebab

Turns the string into kebab case.

given('foo bar').kebab() // String { 'foo-bar' }

snake

Turns the string into snake case.

given('foo bar').snake() // String { 'foo_bar' }

slug

Turns the string into URI conform slug.

given.string('Foo Bar ♥').slug() // String { 'foo-bar' }
given.string('foo bär').slug('+') // String { 'foo+bar' }

Arrays

Back to top

You have access to everything from the native Array object.

Non-Fluent methods

sum

Returns the sum of the array.

given.array([2, 2, 1]).sum() // 5

See usage for arrays of objects.

toMap

Turns an array in the structure of [ ['key', 'value'] ] into a flooent map.

const entries = [['key', 'value']]
given.array(entries).toMap()// FlooentMap { itemId → 1 }

sized

Creates a flooent array of the specified length and populates it using the callback function.

given.array.sized(i => i) // [0, 1, 2]

Fluent methods

pipe

Executes callback and transforms result back into a flooent array if the result is an array. Useful for creating reusable functions for specific method combinations, or for continuing the chain when using non-flooent functions.

const reusableFunction = array => array.append(1)

given.array([]).pipe(reusableFunction) // [1]

mutate

Mutates the original array with the return value of the given callback. This is an escape hatch for when you need it and usually not recommended.

const numbers = given.array(1, 2, 3)

numbers.mutate(n => n.append(4)) // [1, 2, 3, 4]
numbers  // [1, 2, 3, 4]

when

Executes callback if first given value evaluates to true. Result will get transformed back into a flooent array if it is an array.

// can be a boolean
given.array([]).when(true, str => str.append(1)) // [1]
given.array([]).when(false, str => str.append(1)) // []

// or a method
given.array([]).when(array => array.length === 0), array => array.append('called!')) // ['called']
given.array([]).when(array => array.length === 1, array => array.append('called!')) // []

where

Filters array by given key / value pair.

const numbers = [1, 1, 2, 3]

given.array(numbers).where(1) // [1, 1]

See usage for arrays of objects.

whereIn

Filters array by given values.

const numbers = [1, 1, 2, 3]

given.array(numbers).whereIn([1, 3]) // [1, 1, 3]

See usage for arrays of objects.

whereNot

Removes given value from array.

const numbers = [1, 1, 2, 3]

given.array(numbers).whereNot(1) // [2, 3]

See usage for arrays of objects.

whereNotIn

Removes given values from array.

const numbers = [1, 1, 2, 3]

given.array(numbers).whereNotIn([2, 3]) // [1, 1]

See usage for arrays of objects.

reject

Return all items that don't pass the given truth test. Inverse of Array.filter.

given.array([{ id: 1, disabled: true }]).reject(item => item.disabled) // []

until

Returns the items until either the given value is found, or the given callback returns true.

given.array(['a', 'b', 'c']).until('c') // ['a', 'b']
given.array(['a', 'b', 'c']).until(item => item === 'c') // ['a', 'b']

shuffle

Shuffles the array.

given.array([1, 2, 3]).shuffle() // ?, maybe: [1, 3, 2]

unique

Returns array of unique values.

given.array([1, 1, 2]).unique() // [1, 2]

See usage for arrays of objects.

chunk

Breaks the array into multiple, smaller arrays of a given size.

given.array([1, 2, 3, 4, 5]).chunk(3) // [[1, 2, 3], [4, 5]]

forPage

Returns the items for the given page and size.

given.array(['a', 'b', 'c', 'd', 'e', 'f', 'g']).forPage(1, 3) // ['a', 'b', 'c']
given.array(['a', 'b', 'c', 'd', 'e', 'f', 'g']).forPage(2, 3) // ['d', 'e', 'f']
given.array(['a', 'b', 'c', 'd', 'e', 'f', 'g']).forPage(3, 3) // ['g']
given.array(['a', 'b', 'c', 'd', 'e', 'f', 'g']).forPage(4, 3) // []

pad

Fills up the array with the given value.

given.array([1, 2, 3]).pad(5, 0) // [1, 2, 3, 0, 0]

filled

Only returns items which are not empty.

given.array([0, '', null, undefined, 1, 2]).filled() // [1, 2]

See usage for arrays of objects.

partition

Returns a tuple separating the items that pass the given truth test.

const users = given.array([{ id: 1, active: false }, { id: 2, active: false }, { id: 3, active: true }])

const [activeUsers, inactiveUsers] = users.partition(user => user.active)

prepend

Prepends the given items to the array. Unlike unshift, it is immutable and returns a new array.

const numbers = given.array([2, 3])
numbers.prepend(0, 1) // [0, 1, 2, 3]

To prepend items at a specific index, check out the Pointer API.

append

Appends the given items to the array. Unlike push, it is immutable and returns a new array.

const numbers = given.array([0, 1])
numbers.append(2, 3) // [0, 1, 2, 3]

To append items at a specific index, check out the Pointer API.

sortAsc / sortDesc

Sorts an array in their respective order and returns a new array.

given.array([3, 1, 2]).sortAsc() // [1, 2, 3]
given.array([3, 1, 2]).sortDesc() // [3, 2, 1]

See usage for arrays of objects.

tap

Tap into the chain without modifying the array.

given.array([])
  .append(1)
  .tap(array => console.log(array))
  .append(2)
  // ...

Pointer API

Points to a specific index inside the array to do further actions on it.

given.array(['music', 'video', 'tech']).point(1) // returns pointer pointing to 'video'
given.array(['music', 'video', 'tech']).point(-1) // returns pointer pointing to 'tech'
given.array(['music', 'video', 'tech']).point(item => item === 'music') // returns pointer pointing to 'music'

append

Appends given value to array in between the currently pointed item and its next item and returns a new array.

given.array(['music', 'tech']).point(0).append('video') // ['music', 'video', 'tech']

prepend

Prepends given value to array in between the currently pointed item and its previous item and returns a new array.

given.array(['music', 'tech']).point(1).prepend('video') // ['music', 'video', 'tech']

set

Sets the value at the current index and returns a new array.

given.array(['music', 'tec']).point(1).set(item => item + 'h') // ['music', 'tech']

remove

Removes the current index and returns a new array.

given.array(['music', 'tech']).point(1).remove() // ['music']

split

Splits the array at the current index

given.array(['a', 'is', 'c']).point(1).split() // [['a'], ['c']]

value

Returns the value for current pointer position.

given.array(['music', 'tech']).point(1).value() // ['music', 'tech']

step

Steps forward or backwards given the number of steps.

given.array(['music', 'tec']).point(1).step(-1).value() // ['music']

move

Moves an item in the array using the given source index to either "before" or "after" the given target.

given.array(['b', 'a', 'c']).move(0, 'after', 1) // ['a', 'b', 'c']
given.array(['b', 'a', 'c']).move(0, 'before', 2) // ['a', 'b', 'c']
given.array(['b', 'a', 'c']).move(1, 'before', 0) // ['a', 'b', 'c']

Instead of the index, you can also specify "first" or "last":

given.array(['c', 'a', 'b']).move('first', 'after', 'last') // ['a', 'b', 'c']
given.array(['b', 'c', 'a']).move('last', 'before', 'first') // ['a', 'b', 'c']

Methods for arrays of objects

sum

Returns the sum of the given field/result of callback in the array.

  const users = [{ id: 1, points: 10 }, { id: 2, points: 10 }, { id: 3, points: 10 }]

  given.array(users).sum('points') // 30
  given.array(users).sum(user => user.points * 10) // 300

sortAsc / sortDesc

Sorts an array in their respective order and returns a new array.

const numbers = [{ val: 3 }, { val: 1 }, { val: 2 }]
given.array(numbers).sortAsc('val') // [{ val: 1 }, { val: 2 }, { val: 3 }]
given.array(numbers).sortDesc('val') // [{ val: 3 }, { val: 2 }, { val: 1 }]

Also works by passing the index (useful when working with array entries).

given.array([[0], [2], [1]]).sortAsc(0)) // [[0], [1], [2]])

Alternatively, pass in a map function of which its result will become the key instead.

const numbers = [{ val: 3 }, { val: 1 }, { val: 2 }]
given.array(numbers).sortAsc(item => item.val) // [{ val: 1 }, { val: 2 }, { val: 3 }]
given.array(numbers).sortDesc(item => item.val) // [{ val: 3 }, { val: 2 }, { val: 1 }]

pluck

Pluck the given field out of each object in the array.

const cities = [
  { id: 1, name: 'Munich' },
  { id: 2, name: 'Naha' },
]

given.array(cities).pluck('name') // ['Munich', 'Naha']

where

Filters array by given key / value pair.

const cities = [
  { id: 1, name: 'Munich' },
  { id: 2, name: 'Naha' },
  { id: 3, name: 'Naha' },
]

given.array(cities).where('name', 'Munich') // [{ id: 1, name: 'Munich' }]

whereNot

Removes items from array by the given key / value pair.

const cities = [
  { id: 1, name: 'Munich' },
  { id: 2, name: 'Naha' },
  { id: 3, name: 'Naha' },
]

given.array(cities).whereNot('name', 'Naha') // [{ id: 1, name: 'Munich' }]

whereIn

Filters array by given key and values.

const cities = [
  { id: 1, name: 'Munich' },
  { id: 2, name: 'Naha' },
  { id: 3, name: 'Yoron' },
]

given.array(cities).whereIn('name', ['Munich', 'Yoron']) // [{ id: 1, name: 'Munich' }, { id: 3, name: 'Yoron' }]

whereNotIn

Removes items from array by the given key and values.

const cities = [
  { id: 1, name: 'Munich' },
  { id: 2, name: 'Naha' },
  { id: 3, name: 'Yoron' },
]

given.array(cities).whereNotIn('name', ['Naha', 'Yoron']) // [{ id: 1, name: 'Munich' }]

omit

Omits given keys from all objects in the array.

const people = [
  { id: 1, age: 24, initials: 'mz' },
  { id: 2, age: 2, initials: 'lz' }
]

given.array(people).omit(['initials', 'age']) // [ { id: 1 }, { id: 2 } ])

unique

Returns array of unique values comparing the given key.

const items = [{ id: 1, name: 'music' }, { id: 2, name: 'movie' }, { id: 1, name: 'music' }]
given.array(items).unique('id') // [{ id: 1, name: 'music' }, { id: 2, name: 'movie' }]

Alternatively, pass in a function of which its result will become the key instead.

const items = [{ id: 1, name: 'music' }, { id: 2, name: 'movie' }, { id: 3, name: 'MUSIC' }]
given.array(items).unique(item => item.name.toLowerCase()) // [{ id: 1, name: 'music' }, { id: 2, name: 'movie' }]

filled

Only returns items which are not empty.

const items = [{ id: 1, name: 'music' }, { id: 2, name: 'movie' }, { id: 3, name: '' }]
given.array(items).filled('name') // [{ id: 1, name: 'music' }, { id: 2, name: 'movie' }]

groupBy

Groups an array by the given key and returns a flooent map.

const items = [{ id: 1, name: 'music' }, { id: 2, name: 'movie' }, { id: 3, name: 'music' }]
given.array(items).groupBy('name').toObject() // result is:
/*
{
  music: [{ id: 1, name: 'music' }, { id: 3, name: 'music' }],
  movie: [{ id: 2, name: 'movie' }]
}
*/

Alternatively, pass in a function of which its result will become the key instead.

const items = [{ id: 1, name: 'Music' }, { id: 2, name: 'movie' }, { id: 3, name: 'music' }]
given.array(items).groupBy(item => item.name.toUpperCase()).toObject() // result is:
/*
{
  MUSIC: [{ id: 1, name: 'music' }, { id: 3, name: 'music' }],
  MOVIE: [{ id: 2, name: 'movie' }]
}
*/

There is no standalone function for "groupBy". Instead, use the native "Map.groupBy" or "Object.groupBy" (they only support a callback as the argument).

keyBy

Keys the collection by the given key and returns a flooent map. If multiple items have the same key, only the last one will appear in the new collection.

const items = [{ id: 1, name: 'music' }, { id: 2, name: 'movie' }, { id: 3, name: 'music' }]
given.array(items).keyBy('name').toObject() // result is:
/*
{
  music: { id: 3, name: 'music' },
  movie: { id: 2, name: 'movie' }
}
*/

Alternatively, pass in a function of which its result will become the key instead:

const items = [{ id: 1, name: 'music' }, { id: 2, name: 'movie' }, { id: 3, name: 'music' }]
given.array(items).keyBy(item => item.name).toObject()

toKeyedMap

Turns the given array into a flooent map with each element becoming a key in the map.

const genres = ['music', 'tech', 'games']
const map = given.array(genres).toKeyedMap(null).toObject() // result is:
/*
{
  music: null,
  tech: null,
  games: null
}
*/

Alternatively, pass in a callback to specify the default value for each item individually:

const genres = ['music', 'tech', 'games']
const map = given.array(genres).toKeyedMap(genre => genre.toUpperCase()).toObject() // result is:
/*
{
  music: 'MUSIC',
  tech: 'TECH',
  games: 'GAMES'
}
*/

Maps

Back to top

You have access to everything from the native Map object.

You construct a flooent map the same way as a native map:

// using an array in the format of entries
given.map([['key', 'value']])

// using an existing map
given.map(new Map())

Additionally, since normal objects don't have a fluent API in general, you can turn your objects into a map, perform any manipulations and turn them back into an object instead:

given.map
  .fromObject({ key: 'value' })
  .rename('key', 'id')
  .toObject()

For nested data structures, only the first layer gets transformed into a map

Using standalone functions, there are two variants:

import { mapKeys } from 'flooent/map' // -> working with maps
import { mapKeys } from 'flooent/object' // -> working with objects

toEntries, toKeys, toValues

flooent variants of the native methods entries, keys, and values. Instead of a MapIterator, these return a flooent array instead.

const map = given.map.fromObject({ key: 'value' })
map.toKeys() // ['key']
map.toValues() // ['value']
map.toEntries() // [['key', 'value']]

pipe

Executes the callback and transforms the result back into a flooent map if it is a map. Useful for creating reusable functions for specific method combinations, or for continuing the chain when using non-flooent functions.

const extractAreas = map => map.only(['area1', 'area2', 'area3'])

given.map(cityMap).pipe(extractAreas) // String { '!' }

when

Executes the callback if first given value evaluates to true. Result will get transformed back into a flooent map if it is a raw map.

// can be a boolean
given.map(cityMap).when(true, map => map.set('id', genId()))

// or a method
given.map(cityMap).when(map => map.has('isNew'), map => map.set('id', genId()))

pull

Returns the value for the given key and deletes the key value pair from the map (mutation).

const map = given.map.fromObject({ key: 'value' })
map.pull('key') // 'value'
map.has('key') // false

mapKeys

Iterates the entries through the given callback and assigns each result as the key.

const map = given.map.fromObject({ a: 1 }).mapKeys((value, key, index) => key + value)

map.get('a1') // 1

mapValues

Iterates the entries through the given callback and assigns each result as the value.

const map = given.map.fromObject({ a: '1' }).mapValues((value, key, index) => key + value)

map.get('a') // a1

only

Returns a new map with only the given keys.

  given.map.fromObject({ one: 1, two: 2, three: 3 }).only(['one', 'two']) // Map { "one" → 1, "two" → 2 }

except

Inverse of only. Returns a new map with all keys except for the given keys.

  given.map.fromObject({ one: 1, two: 2, three: 3 }).except(['one', 'two']) // Map { "three" → 3 }

arrange

Rearranges the map to the given keys. Any unmentioned keys will be appended to the end.

given.map.fromObject({ strings: 2, numbers: 1, functions: 4 })
  .arrange('numbers', 'functions')
  .toKeys() // ['numbers', 'functions', 'strings']

rename

Renames the given key with the new key if found, keeping the original insertion order.

given.map.fromObject({ one: 1, to: 2, three: 3 })
  .rename('to', 'two')
  .toKeys() // ['one', 'two', 'three']

Macros (Extending flooent)

Back to top

Extend flooent with your own custom methods using macro.

import { given } from 'flooent'

given.string.macro('scream', function() {
  return this.toUpperCase()
})

given.string('hello').scream() // String { 'HELLO' }

Define macros at a central place before your business logic. E.g. entry point or service provider

TypeScript

For TypeScript support, you need to additionally declare the module.

declare module 'flooent' {
  interface Stringable { // Stringable | Arrayable | Mappable
    scream(): Stringable;
  }
}

More examples

These methods, while convenient, are not in the core since they are not all too common yet quadruply the bundle size among other reasons.

import { given } from 'flooent'
import isequal from 'lodash.isequal' // npm install lodash.isequal

given.array.macro('is', function(compareWith) {
  return isequal(this, compareWith)
})

Then, use it like this:

const users = [{ id: 1 }]
given.array(users).is([{ id: 1 }]) // true
import { given } from 'flooent'
import clonedeep from 'lodash.clonedeep' // npm install lodash.clonedeep

given.array.macro('clone', function() {
  // lodash does array.constructor(length) which doesn't work on subclassed arrays
  const clone = clonedeep([...this])
  return this.constructor.from(clone)
})

given.map.macro('clone', function() {
  return this.toEntries().clone().toMap()
})

Then, use it like this:

given.array([['key', 'value']]).clone()
given.map([['key', 'value']]).clone()
import { given } from 'flooent'
import pluralize from 'pluralize' // npm install pluralize

given.string.macro('plural', function(count) {
  const plural = pluralize(this, count, false)
  return new this.constructor(plural) // new up again because pluralize returns raw string.
})

given.string.macro('singular', function() {
  return new this.constructor(pluralize.singular(this))
})

Then, use it like this:

given.string('child').plural() // String { 'children' }
given.string('child').plural(3) // String { 'children' }
given.string('child').plural(1) // String { 'child' }

given.string('children').singular() // String { 'child' }
given.string('child').singular() // String { 'child' }
3.0.0

4 months ago

2.5.2

5 months ago

2.5.1

12 months ago

2.5.0

2 years ago

2.4.2

2 years ago

2.4.1

3 years ago

2.3.2

3 years ago

2.3.1

4 years ago

2.3.0

4 years ago

2.2.0

4 years ago

2.1.0

5 years ago

2.0.1

5 years ago

2.0.0

5 years ago

2.0.0-beta.6

5 years ago

2.0.0-beta.5

5 years ago

2.0.0-beta.2

5 years ago

2.0.0-beta.1

5 years ago

2.0.0-beta.4

5 years ago

1.4.1

5 years ago

1.4.0-beta.5

5 years ago

1.4.0-beta.4

5 years ago

1.4.0-beta.3

5 years ago

1.4.0-beta.2

5 years ago

1.4.0-beta.1

5 years ago

1.4.0-beta.0

5 years ago

1.4.0

5 years ago

1.3.0

5 years ago

1.2.0

5 years ago

1.1.0

5 years ago

1.0.0

5 years ago

0.14.0

5 years ago

0.13.1

5 years ago

0.13.0

5 years ago

0.12.1

5 years ago

0.12.0

5 years ago

0.11.1

5 years ago

0.11.0

5 years ago

0.10.0

5 years ago

0.9.0

5 years ago

0.8.1

5 years ago

0.8.0

5 years ago

0.7.0

5 years ago

0.6.0

5 years ago

0.5.0

5 years ago

0.4.0

5 years ago

0.3.1

5 years ago

0.3.0

5 years ago

0.1.0

5 years ago

0.0.1

5 years ago