npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@xoid/feature

v0.0.6

Published

A plugin system to compose JavaScript classes

Downloads

40

Readme

@xoid/feature

@xoid/feature is a tiny plugin system oriented in JavaScript classes. It provides a type-safe dependency injection mechanism for the classes that refer to each other's instance variables. It especially improves the developer experience for TypeScript users, by reducing the types and the runtime code that needs to be written (demonstrated below). It provides a neat pattern for tree-shakable plugins.

It has 2 exports: Feature and compose .

Classes that extend from Feature, has the following properties by default:

  • There's no need for .constructor.
  • .from can be used to share data between sibling instances. (dependency injection part)
  • .options can be used anywhere.
  • .getOptions can be used to merge external options onto the default options.
  • .main will run when all the instances are ready.

Example:

import { Feature, compose } from '@xoid/feature'

class Alpha extends Feature<{ alpha?: number }> {
  options = this.getOptions({ alpha: 3 })
  alpha = this.options.alpha
}

class Beta extends Feature<{ beta: number }> {
  beta = this.options.beta
  main() {
    console.log('alpha:', this.from(Alpha).alpha)
  }
}

class Gamma extends Feature<{ message: string }> {
  getSum() {
    return `${this.options.message}${this.from(Alpha).alpha + this.from(Beta).beta}`
  }
}

const init = compose([Alpha, Beta, Gamma], (from) => from(Gamma).getSum())

const result = init({ beta: 2, message: 'The answer is: ' })

console.log(result)

Console output:

alpha: 3
The answer is: 5

Without @xoid/feature, the same functionality would require:

class Alpha {
  alpha: number
  static defaultOptions = { alpha: 3 }
  constructor(options: { alpha?: number }) {
    this.alpha = options.alpha ?? Alpha.defaultOptions.alpha
  }
}

class Beta {
  instances!: { Alpha: Alpha }
  beta: number
  constructor(options: { beta: number }) {
    this.beta = options.beta
  }
  main(instances: { Alpha: Alpha }) {
    this.instances = instances
    console.log('alpha:', this.instances.Alpha.alpha)
  }
}

class Gamma {
  instances!: { Alpha: Alpha; Beta: Beta }
  options: { message: string }
  constructor(options: { message: string }) {
    this.options = options
  }
  main(instances: { Alpha: Alpha; Beta: Beta }) {
    this.instances = instances
  }
  getResult() {
    return `${this.options.message}${this.instances.Alpha.alpha + this.instances.Beta.beta}`
  }
}

const init = (
  options: ConstructorParameters<typeof Alpha>[0] &
    ConstructorParameters<typeof Beta>[0] &
    ConstructorParameters<typeof Gamma>[0]
) => {
  const context = {} as Record<string, { main?: Function }>
  [Alpha, Beta, Gamma].forEach((item) => {
    context[item.name] = new item(options)
  })
  Object.keys(context).forEach((key) => context[key]?.main(context))
  return context.Gamma.getSum()
}

const result = init({ beta: 2, message: 'foo' })

console.log(result)

Tree-shaking methodology

This feature is useful if you are a library-author who wants to build a tree-shakable library with a lot of composable classes.

Create the following file:

<root>/ids/Alpha.tsx (identity module)

export const id = Symbol()
export const type = {} as import('../Alpha').default

Modify the following lines in existing files:

<root>/Alpha.tsx (real module)

import { Feature } from '@xoid/feature'
import { id } from './ids/Alpha'

export default class Alpha extends Feature<{ alpha?: number }> {
+ id = id 
  options = this.getOptions({ alpha: 3 })
  alpha = this.options.alpha
}

<root>/Beta.tsx

- import Alpha from './Alpha'
+ import * as Alpha from './ids/Alpha'

class Beta extends Feature() {
  main() {
    console.log(this.from(Alpha).alpha) // This used to work, and still works after the diff
  }
}

Augmentation technique

There's a neat type-safety helper built-in to @xoid/feature. The callback argument of compose has a second agrument called types. types should not be used in the runtime, it only serves as a type aggregator. types merges the types of all the classes that are composed. In the following example, draggableProps key is used multiple times in different features, so that each feature "augments" the same interface. Finally types is fed as a parameter type to useDraggable

import { Feature, compose } from '@xoid/feature'

class DragHelper extends Feature() {
  draggableProps!: {
    onDragStart: () => void
    onDragEnd: () => void
  }
}

class DropHelper extends Feature() {
  draggableProps!: {
    onDrop: () => void
  }
}

const createDraggable = compose(
  [DragHelper, DropHelper], 
  (from, types) => {
    const useDraggable = (props: typeof types.draggableProps) => {
      // TODO: implement helper
    }
    return { useDraggable }
  }
)

const { useDraggable } = createDraggable({})

// This whole thing would be typesafe thanks to the `types`
useDraggable({ onDragStart, onDragEnd, onDrop })

Note

@xoid/feature doesn't depend on xoid. It can be used standalone, however using it with xoid is synergetically amazing.