Coder Social home page Coder Social logo

forman / extendit Goto Github PK

View Code? Open in Web Editor NEW
20.0 2.0 1.0 1.12 MB

Framework and library for creating extensible and scalable TS/JS applications

Home Page: https://forman.github.io/extendit/

License: MIT License

JavaScript 1.40% HTML 0.17% CSS 0.58% TypeScript 97.85%
api framework javascript library react extensions-management ui dependency-injection dependency-inversion plugin-system

extendit's Introduction

image

CI codecov npm TypeScript Prettier MIT

ExtendIt.js is a framework and library that is used to create extensible and scalable JavaScript applications. Its core API design is largely inspired by the Extension API of Visual Studio Code.

ExtendIt.js provides the means for a host application to dynamically import JavaScript modules - extensions - that add new features and capabilities to the application.

ExtendIt.js has been designed to efficiently work with React, for this purpose it provides a number of
React hooks. However, the library can be used without React too. It's just a peer dependency.

Highlights

  • Simple, low-level API allowing for complex, loosely coupled application designs offered by dependency inversion.
  • Manages extensions, which are JavaScript packages with a minor package.json enhancement.
  • Lets applications and extensions define contribution points that specify the type of contribution that applications and extensions can provide.
  • Lets applications and extensions provide contributions to a given contribution point. Contributions can be
    • JSON entries in the extension's package.json and/or
    • JavaScript values registered programmatically in code.
  • Allows dynamic loading of code:
    • Extensions may be installed at runtime or bound statically.
    • Code contributions are loaded on demand only, while JSON entries can be used right after extension installation.
  • Provides optional utilities for Web UI development:
    • React hooks for reactive access to extensions and contributions.
    • Predefined contribution points for typical UI elements.

Demo

To see the API in action, you can run the Demo code using npm run dev, see section Development below. It is a simple React application that demonstrates how extensions are installed, activated, and how they can contribute elements such as commands or UI components to an application.

Installation

npm install @forman2/extendit

or

yarn add @forman2/extendit

Usage

Extension basics

Any extension must be defined by its extension manifest, which is basically a slightly enhanced package.json.

{
   "name": "my-extension",
   "provider": "my-company",
   "version": "1.0.0",
   "main": "init"
}

The main field is optional. If you provide it as above, it means you provide an extension activator in a submodule named init which defines an activate() function that is called if your extension is activated by the host application:

import { SomeAppApi } from "some-app/api";

export function activate() {
  // Use the SomeAppApi here, e.g., 
  // register your contributions to the app
}

Extension-specific APIs

The activator may also export an extension-specific API for other extensions

import { MyApi } from "./api";

export function activate(): MyApi {
  return new MyApi({ ... });
}

Hence, another dependent extension such as

{
   "name": "other-extension",
   "provider": "other-company",
   "main": "init",
   "dependencies": {
      "@my-company/my-extension": "1.0.0"
   }
}

may consume it in its own init.ts

import { type ExtensionContext, getExtension } from "@forman2/extendit";
import { type MyApi } from "@my-company/my-extension";

export function activate(ctx: ExtensionContext) {
  const myExtension = getExtension("my-company.my-extension");
  const myApi = myExtension.exports as MyApi;
  // Use imported extension API here, e.g., to add some contribution
  myApi.registerViewProvider({ ... });
}

If you add extensionDependencies to your package.json

{
   "extensionDependencies": [
     "my-company.my-extension"
   ]
}

then you can save some lines of code in your activator, because the framework passes desired APIs as a subsequent arguments corresponding to the extensionDependencies entries:

import { type ExtensionContext, getExtension } from "@forman2/extendit";
import { type MyApi } from "@my-company/my-extension";

export function activate(ctx: ExtensionContext, myApi: MyApi) {
  myApi.registerViewProvider({ ... });
}

Extension installation

The host application registers (installs) extensions by using the readExtensionManifest and registerExtension functions:

import { readExtensionManifest, registerExtension } from "@forman2/extendit";

export function initApp() {
   const extensionsUrls: string[] = [
     // Get or read installed extension URLs
   ];
   const pathResolver = (modulePath: string): string => {
     // Resolve a relative "main" entry from package.json
   };  
   extensionUrls.forEach((extensionUrl) => {
     readExtensionManifest(extensionUrl)
     .then((manifest) => 
       registerExtension(manifest, { pathResolver })
     )
     .catch((error) => {
       // Handle installation error
     });
   });
}

Contribution points and contributions

The host application (or an extension) can also define handy contribution points:

import { registerContributionPoint } from "@forman2/extendit";

export function initApp() {
  registerContributionPoint({
    id: "wiseSayings",
    manifestInfo: {
      schema: {
        type: "array",
        items: {type: "string"}
      }
    }
  });
}

Extensions can provide contributions to defined contribution points. Contributions are encoded in the contributes value of an extension's package.json:

{
   "name": "my-extension",
   "provider": "my-company",
   "contributes": {
      "wiseSayings": [
         "Silence is a true friend who never betrays.",
         "Use your head to save your feet.",
         "Before Alice went to Wonderland, she had to fall."
      ]
   }
}

A consumer can access a current snapshot of all contributions found in the application using the getContributions function:

  const wiseSayings = getContributions<string[]>("wiseSayings");

The return value will be the same value, as long as no other extensions are installed that contribute to the contribution point wiseSayings. If this happens, a new snapshot value will be returned.

If you are building a React application, you can use the provided React hooks in @forman2/extend-me/react for accessing contributions (and other elements of the ExtendMe.js API) in a reactive way:

import { useContributions } from "@forman2/extend-me/react";

export default function WiseSayingsComponent() {
  const wiseSayings = useContributions("wiseSayings");   
  return (
    <div>
      <h4>Wise Sayings:</h4>
      <ol>{ wiseSayings.map((wiseSaying) => <li>{wiseSaying}</li>) }</ol>
    </div>
  );
}

The component will be re-rendered if more contributions are added to the contribution point.

A contribution may be fully specified by the JSON data in the contributes object in package.json. It may also require JavaScript to be loaded and executed. Examples are commands or UI components that are rendered by React or another UI library. The following contribution point also defined codeInfo to express its need of JavaScript code:

import { registerCodeContribution } from "@forman2/extendit";

export function activate() {
  registerContributionPoint({
    id: "commands",
    manifestInfo: {
      schema: {
        type: "array",
        items: {
          type: "object",
          properties: {
            id: {type: "string"},
            title: {type: "string"}
          }
        }
      }
    },
    codeInfo: {
      idKey: "id",
      activationEvent: "onCommand:${id}"
    }
  });
}

The entry activationEvent causes the framework to fire an event of the form "onCommand:${id}" if the code contribution with the given "id" is requested. In turn, any extension that listens for the fired event will be activated.

Here is an extension that provide the following JSON contribution to the defined contribution point commands in its package.json

{
  "contributes": {
    "commands": [
      {
        "id": "openMapView",
        "title": "Open Map View"
      }
    ]
  }
}

and also defines the corresponding JavaScript code contribution in its activator:

import { registerCodeContribition } from "@forman2/extendit";
import { openMapView } from "./map-view";

export function activate() {
  registerCodeContribition("commands", "openMapView", openMapView);
}

Such code contributions are loaded lazily. Only the first time a code contribution is needed by a consumer, the contributing extension will be activated. Therefore, code contributions are loaded asynchronously using the loadCodeContribution function:

import { loadCodeContribution } from "@forman2/extendit";
import { type Command } from "./command";

async function getCommand(commandId: string): Promise<Command> {
  return await loadCodeContribution<Command>("commands", commandId);
}  

There is also a corresponding React hook useLoadCodeContribution that is used for implementing components:

import { useLoadCodeContribution } from "@forman2/extendit/react";
import { type Command } from "./command";

interface CommandButtonProps {
  command: Command;  
}

export default function CommandButton({ command }: CommandButtonProps) {
  const commandCode = useLoadCodeContribution("commands", command.id);
  if (!commandCode) {  // Happens on first render only
    return null;
  }
  return (
    <button
      onClick={commandCode.data}
      disabled={commandCode.loading || commandCode.error}      
    >
      {command.title} 
    </button>
  );    
}  

Documentation

We currently only have this file and the API docs, sorry.

Development

Source code

Get sources and install dependencies first:

$ git clone https://github.com/forman/extendit
$ cd extendit
$ npm install

Scripts

Now the following scripts are available that can be started with npm run:

  • dev - run the React Demo in development mode
  • build - build the library, outputs to ./dist
  • lint - run eslint on project sources
  • test - run project unit tests
  • coverage - generate project coverage report in ./coverage
  • typedoc - generate project API docs in ./docs

Configuration

You can use .env files, e.g., .env.local to configure development options:

# As `vite build` runs a production build by default, you can
# change this and run a development build by using a different mode
# and `.env` file configuration:
NODE_ENV=development

# Set the library's log level (ALL, DEBUG, INFO, WARN, ERROR, OFF)
# Logging is OFF by default. 
# Note, if the level is not set or it is OFF, no console outputs 
# are suppressed while unit tests are run.
VITE_LOG_LEVEL=ALL

Contributing

ExtendIt.js welcomes contributions of any form! Please refer to a dedicated document on how to contribute.

Acknowledgements

ExtendIt.js currently uses the awesome libraries

  • ajv for JSON validation (may be turned into peer dependency later)
  • memoize-one for implementing state selector functions
  • zustand for state management

License

Copyright © 2023 Norman Fomferra

Permissions are hereby granted under the terms of the MIT License: https://opensource.org/licenses/MIT.

extendit's People

Contributors

forman avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar

Forkers

anaruescas

extendit's Issues

Address all eslint warnings

Is your feature request related to a problem? Please describe.

We still have >15 eslint warnings of the form

  14:35  warning  Invalid type "unknown" of template literal expression  @typescript-eslint/restrict-template-expressions

Describe the solution you'd like

Address all current eslint warnings. After fixing, turn them into errors. Let CI do linting too.

Make log level a configuration parameter

Is your feature request related to a problem? Please describe.

Log level can be changed only programmatically by a host app:

import { log } from "@forman2/extendit/util"

log.Logger.setGlobalLevel(log.LogLevel.INFO);

Describe the solution you'd like

updateFrameworkConfig({ logLevel: "INFO" });

Type parameter name `Data` is misleading

Is your feature request related to a problem? Please describe.

The type parameter name Data is used for code contributions and in this context the name is misleading, because of Code vs. Data.

Describe the solution you'd like

The name Value is more appropriate and equally generic.

Describe alternatives you've considered

The name Code is not entirely correct, because the contribution value can be data as well.

Function `registerCodeContribution()` should be reactive

Describe the bug

Function registerCodeContribution() should be reactive but is not.
Calling it from extensions does will only cause the framework store to fire for the first contribution registered for the same point. Subsequent registrations for the same point do not fire.

Version: 0.1.0

Log levels may also be given as strings

Is your feature request related to a problem? Please describe.

Currently, log levels must be passed as LogLevel instances, e.g. log.LogLevel.INFO or log.LogLevel.get(levelName), which is verbose.

Describe the solution you'd like

In all API, where log level is expected allow also passing a string, e.g., log level "INFO".

Modify JSON contributions dynamically

Is your feature request related to a problem? Please describe.

In my app, I'm using contribution point menus from the contrib module to let extensions contribute menu items.
Menu items must currently provided by JSON entries in the extension's package.json.
I have a top-level menu "View" with a submenu "Tool Views... >".
This submenu should be filled from the contributions to another contribution point, namely toolViews.
Therefore I need to programmatically add (or replace) JSON entries to (or in) the menus contribution point.

Describe the solution you'd like

Possible options

  • (1) Support an auto-fill marker in the specific submenus contribution point so the framework can generate the menu entries from contributions of another referenced contribution point.
  • (2) Support adding JSON contributions programmatically, e.g., provide a new registerJsonContribution or the like similar to registerCodeContribution .

(1) is more pragmatic and applies to that specific contrib point only
(2) is more generic and flexible

And (2) does not exclude (1).

Contribution points with type guards

Is your feature request related to a problem? Please describe.

  • We currently can only validate JSON entries against a given schema, but we cannot perform any integrity checks.
  • We currently cannot perform any validity/type checking on a code contribution c when registering it using registerCodeContribution<T>(c). We must rely on the user that c is valid and of type T.

Describe the solution you'd like

Allow for optional validators isEntryValid and isContributionValid to ManifestContributionInfo and CodeContributionInfo when defining a new ContributionPoint:

export interface ManifestContributionInfo<TM = unknown, TS = TM> {
  schema: JsonTypedSchema<TM> | JsonSchema;
  processEntry?: (entry: TM) => TS;
  isEntryValid(entry: unknown)?: entry is TM;  /* NEW */
}

export interface CodeContributionInfo<TS = unknown> {
  idKey?: KeyOfObjOrArrayItem<TS>;
  activationEvent?: string;
  isContributionValid<TC = unknown>(contrib: unknown)?: contrib is TC;  /* NEW */
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.