Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@homer0/jimple

Package Overview
Dependencies
Maintainers
1
Versions
20
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@homer0/jimple

An extended version of the Jimple lib, with extra features

  • 3.0.2
  • Source
  • npm
  • Socket score

Version published
Weekly downloads
21
decreased by-22.22%
Maintainers
1
Weekly downloads
 
Created
Source

💉 jimple

An extended version of the Jimple lib, with extra features.

🍿 Usage

If you are wondering why I built this, go to the Motivation section.

⚡️ Features

TypeScript out-of-the-box

Unfourtunately, Jimple doesn't have type declarations, but someone (*) took the time, and wrote a .d.ts for it. The issue is that shipping a custom .d.ts is not super straightforward, and there were a couple of references that I wanted to change (**), so I worked some (ugly) magic, and you can now import the Jimple class from this package and it will already be typed:

import { Jimple } from '@homer0/jimple';

const container = new Jimple();
container.register({
  register: (c) => {
    // `c` is typed to be `Jimple` :D!
  },
});

*: For the life of me, I can't remember from where I copied the .d.ts file. It was like more than a year ago and I couldn't find it again, sorry!! If you are the author, please let know and I'll give you credit. **: If you were to extend the Jimple class, all the references to the container type, inside the class, would point to the original class, not the extended one. That was the main thing I wanted to change.

try method

This method is a shortcut of has and get: if the resource exists in the container, it returns it, otherwise, it returns undefined:

const myService = container.try<MyService>('myService');
// myService is either `undefined` or `MyService`
if (myService) {
  // myService exists
}
Function shorthand

Not a lot of people are happy with OOP nowadays (not me :P), so, why not create a function alternative to create a container?

import { jimple } from '@homer0/jimple';

const container = jimple();
Provider shorthand

Yes, this exists on the original lib, I actually wrote it there, but the difference here is that it's a standalone function, instead of a static method, and it's created using the resource factory:

import { provider } from '@homer0/jimple';

export class MyService {
  // ...
}

export const myService = provider((c) => {
  c.set('myService', () => new MyService());
});

The generated object will look like this:

const myService = {
  provider: true,
  register: (c) => {
    c.set('myService', () => new MyService());
  },
};

Which means that you can go to the container and register it like this:

container.register(myService);
Provider creator

A provider creator is a both a provider, and a function that can create a provider.

Let's say you created a service that can be shared in multiple applications, or as multiple services in the same container, and you would want the implementation to be able to send some options/configuration to the provider, and maybe, also have a version that uses defaults:

import { provider } from '@homer0/jimple';

type Opts = {
  url?: string;
};

export class MyService {
  constructor({ url = 'http://localhost:3000' }: Opts) {
    // ...
  }
}

export const myServiceWithOptions = (options: Opts = {}) =>
  provider((c) => {
    c.set('myService', () => new MyService(options));
  });

export const myService = myServiceWithOptions();

But that looks kind of hacky, and whoever implements it, needs to be aware of the two providers and their differences.

Here's where the providerCreator comes in: this wrapper allows you to wrap the function that generates the provider, and its "default version" in a single provider:

import { providerCreator } from '@homer0/jimple';

type Opts = {
  url?: string;
};

export class MyService {
  constructor({ url = 'http://localhost:3000' }: Opts) {
    // ...
  }
}

export const myService = providerCreator((options: Opts = {}) => (c) => {
  c.set('myService', () => new MyService(options));
});

The magic is that the return of providerCreator is not actually a provider, but a Proxy. If the proxy's register function gets called, it will internally call the "creator function" without any arguments, and return the provider; but at the same time, you can also call the proxy as if it were the "creator function":

container.register(myService);
// or
container.register(myService({ url: 'http://localhost:9000' }));

The providerCreator is created using the resource creator factory.

Providers collection

As you probably already guessed, the idea here is to group multiple providers in a single object, and then register it as a single provider:

import { providers } from '@homer0/jimple';
import { myServiceA } from './services/a';
import { myServiceB } from './services/b';

export const services = providers({
  myServiceA,
  myServiceB,
});

This will generate a provider that, when registered, it will register all the providers in the collection:

container.register(services);

The providers is created using the resources collection factory.

Extending the container and keeping the types

Since provider, providerCreator and providers are standalone functions, if you were to extend the Jimple class, the type for the container you receive on registration would be the original class, not the extended one.

The way to get around this is by "creating your own functions":

import {
  Jimple,
  createProvider,
  createProviderCreator,
  createProviders,
} from '@homer0/jimple';

export class MyContainer {}

export const provider = createProvider<MyContainer>();
export const providerCreator = createProviderCreator<MyContainer>();
export const providers = createProviders<MyContainer>();
Inject helper

The inject helper is a resource you can use when creating reusable services, and it takes care of resolving instances and injections:

Ensuring you always get an instance

Let's say that your service has a dependency on another service, and you want to give the ability to the implementation to inject it, but at the same time, if the implementation doesn't send it, you need an instance of the dependency to work with.

You could do an if with the parameters, but after a few of them, it gets ugly, so that's why the helper has the .get method:

// Let's define our dependency dictionary.
type Dependencies = {
  dep: string;
};
// Create the helper with it.
const helper = new InjectHelper<Dependencies>();
// Define the options for the service, that can be used for the injection.
type MyServiceOptions = {
  inject?: Partial<Dependencies>;
};
const myService = ({ inject = {} }: MyServiceOptions) => {
  // Ensure `dep` is always a `string`
  const dep = helper.get(inject, 'dep', () => 'default');
  console.log('dep:', dep);
};
Resolving dependencies on the provider

Another use case when creating a reusable service, is to give the ability to the provider (creator) to specify the name of the services that should be injected from the container.

Keeping with the previous example, we could look for dep on the container, but if the implementation used a different name? This is why we have the .resolve method:

// Let's define our dependency dictionary.
type Dependencies = {
  dep: string;
};
// Create the helper with it.
const helper = new InjectHelper<Dependencies>();

// Define the options for the provider.
type MyProviderOptions = {
  services?: {
    [key in keyof Dependencies]?: string;
  };
};

const myProvider = providerCreator(
  ({ services = {} }: MyProviderOptions) =>
    (container) => {
      container.set('myService', () => {
        // Tell the helper to look for `dep` in the container.
        const inject = helper.resolve(['dep'], container, services);
        return myService({ inject });
      });
    },
);

For each dependency, the method will first check if a new name was specified (on services), and try to get it with that name, otherwise, it will try to get it with the default name (dep), and if it doesn't exist, it will just keep it as undefined.

⚙️ Factories

The factories are in charge of creating the functions for the providers, and can be used to create other types of objects.

Resource factory

A resource is small object, with a name and a function. For example, a provider:

const resource = {
  provider: true,
  register: () => {},
};

The way the factory works is that it takes the function type, and returns a function that can be used to create the resource:

import { resourceFactory, type Jimple } from '@homer0/jimple';

type ActionFn = (c: Jimple) => void;
const factory = resourceFactory<ActionFn>();

const myAction = factory('action', 'fn', (c) => {
  c.set('foo', 'bar');
});

And action will look like this:

const myAction = {
  action: true,
  fn: (c) => {
    c.set('foo', 'bar');
  },
};

As you may have noticed, the factory task is just to set the type of the function so it can be later validated when creating the resource.

Like I did for provider, you can create a function that wraps the result of the factory, and only takes the function:

import { resourceFactory, type Jimple } from '@homer0/jimple';

type ActionFn = (c: Jimple) => void;
const factory = resourceFactory<ActionFn>();
const action = (fn: ActionFn) => factory('action', 'fn', fn);

const myAction = action((c) => {
  c.set('foo', 'bar');
});
Resource creator factory

In case you missed the section about provider creator, a creator is a proxy that can be used both as a resource, and a function that can create a resource.

Now, this is basically the same as the resource factory: it takes the function type, and returns a function that can be used to create the resource creator.

import { resourceCreatorFactory, type Jimple } from '@homer0/jimple';

type ActionFn = (c: Jimple) => void;

const factory = resourceCreatorFactory<ActionFn>();

const myAction = factory('action', 'fn', (name = 'foo') => (c) => {
  c.set(name, 'bar');
});

Now, you can register with the default name, or with a custom one:

myAction.fn(container);
myAction('fooz')(container);

And like with providerCreator, you can wrap the creation in a function:

import { resourceCreatorFactory, type Jimple } from '@homer0/jimple';

type ActionFn = (c: Jimple) => void;
// it will be typed, but needs to extend from `any`.
type ActionCreatorFn = (...args: any[]) => ActionFn;

const factory = resourceCreatorFactory<ActionFn>();
const actionCreator = <CreatorFn extends ActionCreatorFn>(creator: CreatorFn) =>
  factory('action', 'fn', creator);

const myAction = actionCreator((name = 'foo') => (c) => {
  c.set(name, 'bar');
});
Resources collection factory

The collections are technically resources that contains other resources, and when their function gets called, it calls the function in every resource in the collection.

import { resourcesCollectionFactory, type Jimple } from '@homer0/jimple';
// let's assume we have some actions already.
import { myActionA, myActionB } from './actions';

type ActionFn = (c: Jimple) => void;
const factory = resourcesCollectionFactory<'action', ActionFn>();
const myActions = factory('action', 'fn')({ myActionA, myActionB });

Just like with the others, the creation of the factory adds type validations, and returns a function to actually create collections.

But in this case, to simplify the implementation, you don't need to create an extra wrapper, you can just create the function the specifies the name and key (action and fn), and use its returned function to create the collections:

// ...
export const actions = factory('action', 'fn');
// ...
const myActions = actions({ myActionA, myActionB });

🤘 Development

As this project is part of the packages monorepo, some of the tooling, like lint-staged and husky, are installed on the root's package.json.

Tasks
TaskDescription
lintLints the package.
testRuns the unit tests.
buildTranspiles the project.
types:checkValidates the TypeScript types.

Motivation

This used to be part of the wootils package, my personal lib of utilities, but I decided to extract them into individual packages, as part of the packages monorepo, and take the oportunity to migrate them to TypeScript.

I really like Jimple, I've used in countless projects, but there were customizations I wanted (like the ones in the features section) that didn't make a lot of sense in the original project, plus the factories, which I needed for my jimpex project (jimple + express).

Keywords

FAQs

Package last updated on 22 Feb 2024

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc