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

@passionio/feature-plugin

v0.0.12

Published

This library is the core engine which loads a set of Passion frontend *features* according to the provided configuration, and enables the app to render Pages which consist of *views* and *widgets* belonging to those features.

Downloads

17

Readme

Description

This library is the core engine which loads a set of Passion frontend features according to the provided configuration, and enables the app to render Pages which consist of views and widgets belonging to those features.

  +-------------------------App-------------------------------+
  |                                                           |
  |  +-----------------FeatureSet----------------------+      |
  |  |                                                 |      |
  |  |    +-----Feature1---+    +---Feature2----+      |      |
  |  |    |  <Service>     |    | <Service>     |      |      |
  |  |    | ▲<views>       |    | <views> ◄-+   |      |      |
  |  |    | |<widgets      |    | <widgets> |   |      |      |
  |  |    +-|-------▲------+    +▲----------|---+      |      |
  |  +------|--------\----------/-----------|----------+      |
  |         |         \        /            |                 |
  |         |          \      /             |                 |
  |         |           \    /              |                 |
  |     +---|Page1+   +--Page2--+   +--Page3|--+              |
  |     |   |     |   | Widgets |   |       |  |              |
  |     |  View   |   |         |   |   View   |              |
  |     |         |   |         |   |          |              |
  |     |         |   |         |   |          |              |
  |     |         |   |         |   |          |              |
  |     |         |   |         |   |          |              |
  |     +---------+   +---------+   +----------+              |
  |                                                           |
  +-----------------------------------------------------------+
      

Table of Contents

Usage

Install:

yarn add @passionio/feature-plugin

Use:

  import { Page, FeatureSet } from '@passionio/feature-plugin'
  
  export const App = () => (
    <FeatureSet featureConfig={[{ featureName: 'HelloWorld' }]} importFeature={featureName => features[featureName]}>
       <Page data={{
         type: 'FeatureView',
         featureName: 'HelloWorld',
         viewName: 'Main',
       }} />
    </FeatureSet>
  )

Page

Pages are the main building blocks of a Passion.io app. It can be of one of two types:

  • FeatureView - the page consists of a single View which belongs to a single Feature plugin, and occupies the whole page. To create a Page of this type one needs to specify the name of the feature from which to pull the view, and the name of the view itself (see examples below).
  • Widgetized - consists of one or more widgets laid out in a vertical column, each belonging to a potentially different feature. For each widget the name of the parent feature and the name of the widget needs to be specified.

Example 1. A widgetized Page consisting of a widget AcademyCourse defined in the Courses feature, and a Testimonials widget belonging to a different feature called Marketing:

  {
    type: 'Widgetized',
    widgets: [
      {
        id: 1,
        featureName: 'Courses',
        widgetName: 'AcademyCourse',
      },
      {
        id: 2,
        featureName: 'Marketing',
        widgetName: 'Testimonials',
      },
    ]
  }

Example 2. A feature view Page which consists of a single view Main pulled from the Courses feature:

  {
    type: 'FeatureView',
    featureName: 'Courses',
    viewName: 'Main',
  }

To render a Page in your app, you need to nest it as a child within the main FeatureSet component. This way the View and Widget components inside the page gain access to the loaded features and their interfaces:

  import { Page, FeatureSet } from '@passionio/feature-plugin'
  
  const App = () => (
    <FeatureSet {...}>
       <Page data={{
         type: 'FeatureView',
         featureName: 'Courses',
         viewName: 'Main',
       }} />
    </FeatureSet>
  )

FeatureSet

This is the main component of this library, which loads the configured libraries and provides the whole internal mechanism to any Pages rendered within it, i.e. to the views and widgets within those pages.

When rendering a <FeatureSet> in your app, you must provide two props:

  • featureConfig - an array of feature descriptors in the form:
      {
        featureName: "Feature1"
        version: "^1.0.0"
      }
  • importFeature - an async function which loads a feature by name, e.g.
      const importFeature = featureName => {
        const featureModule = await import(`./${featureName}.js`)
        return featureModule.default
      }

Example:

const App = () => (
  <FeatureSet
    featureConfig={[
      {
        featureName: 'Courses',
        version: '0.0.1'
      },
      {
        featureName: 'Community',
        version: '0.0.2'
      },
    ]}
    importFeature={
      featureName => {
        if (featureName === 'Courses') {
          return require('./Courses.js')
        }
        if (featureName === 'Community') {
          return require('./Community.js')
        }
        throw new Error(`Unknown feature: ${featureName}`)
      }
    }
  >
    <Page {...} />
    <Page {...} />
  </FeatureSet>
)

Feature

A Feature in this frontend layer represents the frontend side of a whole conceptual "feature" that Passion offers to its users, such as Courses, Community, Apps, Users, Creator Payments etc. Having in mind we see all such features as bounded contexts (in the terminology of Domain-Driven Design), each needs its backend service support as well as its frontend counterparts. A mobile app can be thus seen as an aggregation of a number of such features or feature plugins. Each such feature frontend plugin consists of 3 major parts - views, widgets and a frontend service - and is expected to take the following form:

  {
    getWidget(name: string): ReactComponent,
    getView(name: string): ReactComponent,
    getName(): string,
    getService(): ReactComponent,
    async initialize({ useOwnRestrictedInterface: ReactHook }): void
  }

Service

In this context a Service represents the frontend "brain" of the Feature which should determine its full lifecycle. Examples:

  • The Service component of the Users Feature could immediately (on load) reach out to the local storage and determine if the user is already logged in and provide this information through its open interface to other feature plugins.
  • The Service component of the Courses Feature could consume the open interface of the Users Feature Service and load/precache the available courses
  • The Community Feature Service could initialize the websockets or any polling mechanism to start receiving messages from the server

The Service is expected to be a React component because this way it gains access to the entire React ecosystem through the use of hooks (e.g. it can use useEffect for onload events, useQuery to communicate to the server, useSelector to consume the Redux state, useOpenInterface to consume another Feature Service, etc). It should however return null because its purpose is not to render anything (see views and widgets for that below)

Services can have interfaces through which they publish information to the rest of the system. Interfaces can be open (for any other Feature to consume) or restricted (only available to the views and widgets of its own Feature). Therefore the Service is provided with the handles for publishing those two types of interfaces as props. Example:

  const UsersService = ({ publishRestrictedInterface, publishOpenInterface }) => {
    useEffect(() => {
      const username = localStorage.get('username')
      if (username) {
        // The username is considered the public end-result of this Feature available to all consumers
        publishOpenInterface({
          username
        })
    }, [])
    
    const attemptLogin = useCallback(() => { /* ... */ }, [])
    useEffect(() => {
      // An action to login is considered private to this Feature so only its own widgets and views can trigger it correctly
      publishRestrictedInterface({
        attemptLogin
      })
    }, [attemptLogin])
  }

Once published, both interfaces are available to the corresponding other parties through the use of the corresponding hooks.

To consume an open interface, one can simply import the useOpenInterface hook from the library and specify which feature's open interface it wants:

  import { useOpenInterface } from '@passionio/feature-plugin'
  
  const CommunityService = () => {
    const { username } = useOpenInterface('Users')
    
    useEffect(() => {
      if (!username) {
        return
      }
      setupMessageListeners(username)
      
      return () => {
        destroyMessageListeners(username)
      }
    }, [username])
  }

Only a view or widget of the same Feature can consume the restricted interface of the belonging Service component. To achieve this, the library provides this private hook called useOwnRestrictedInterface only to the Feature itself through its initialize function (see signature above). The feature can use this event to get the handle of the hook and use it to its own convenience.

Example 1. The Users Feature generates a LoginForm Widget class which uses useOwnRestrictedInterface from closure:

  const createLoginFormWidget = ({ useOwnRestrictedInterface }) => {
    const LoginForm = () => {
      const { attemptLogin } = useOwnRestrictedInterface()
      return (
        <input {...} />
        <input {...} />
        <button onClick={attemptLogin} />
      }
    }
    return LoginForm
  }
  
  const widgets = {}
  
  export default {
    getWidget(widgetName) {
      return widgets[widgetName]
    }
    
    initialize({ useOwnRestrictedInterface }) {
      widgets.LoginForm = createLoginFormWidget({ useOwnRestrictedInterface })
    }
    
    // ...
  }

Instead of closure, the Feature authors / team may decide to use a different mechanism, e.g. store useOwnRestrictedInterface to a globally accessible variable from where all its subcomponents can use it or something similar.

Views and widgets

The views and widgets are React components that a Feature offers to an App (or any other consumer of the Feature) as something it can render.

Views

Views are also reusable mini-applications but are supposed to occupy the entire screen

  • The Courses Feature could designate a MainView which is what we know as the "courses tab" today
  • The Users Feature could offer a view Login with all authentication options, as well as a Profile view with all user's editable information

Widgets

Widgets are reusable mini-applications that can be laid out as the building blocks of a Widgetized Pages (see #Page above). Examples:

  • The Courses Feature could offer widgets like MostPopularCourse or UnfinishedLesson, which the App may render on the widgetized Discover page
  • The Community Feature could offer such widgets as HottestThread or LatestMessages that could also be placed on the widgetized Discover page
  • The Users Feature could have a Profile widget that could be placed in the widgetized Settings page

args

Both widgets and views support the args prop which can be used to customize the behavior of that component. This way the Creator can specify additional parameters to a widget or view when they put it into a Page, the parameters get saved as the Page config in the database and later when the Page is rendered the args will be passed to the widget / view. Technically speaking, when the args are specified in the data parameter of a widget or view config, they will be automatically passed to the component:

  <Page data={{
    type: 'FeatureView',
    featureName: 'HelloWorld',
    viewName: 'World',
    args: {
      population: '8b'
    },
  }} />
  
  // ...will automatically be available to:
  
  const HelloWorldView = ({ args: { population } }) => (
    <div>Population: {population}</div>
  )
  
  // Same for widgets:
  
  <Page data={{
    type: 'Widgetized',
    widgets: [
      {
        featureName: 'ManyWidgets',
        widgetName: 'SingleWidget',
        args: {
          color: 'blue'
        },
      },
    },
  }} />
  
  // ...will automatically be available to:
  
  const SingleWidget = ({ args: { color } }) => (
    <div style={{ color }}>Colored text</div>
  )

UI Components

To share UI components from the parent app or shell, you can pass them through context using the guiElements prop and access them using the useGuiElements hook.

import { guiElements } from 'src/guiElements'; // e.g. { ProgressBar: () => null, Button: ... }
import { FeatureSet } from '@passionio/feature-plugin';

<FeatureSet
  ...
  guiElements={guiElements}
>
import { useGuiElements } from '@passionio/feature-plugin';

const Component = () => {
  const { Button } = useGuiElements();
  return <Button>Click me</Button>;
};

Typescript support

Typescript docs

Full scale examples

  • https://github.com/independenc3/feature-plugin-test