Security News
Research
Data Theft Repackaged: A Case Study in Malicious Wrapper Packages on npm
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
@glimmer/di
Advanced tools
@glimmer/di is a dependency injection library designed for use with Glimmer.js applications. It allows developers to manage dependencies in a clean and modular way, promoting better code organization and easier testing.
Registering Services
This feature allows you to register services within a registry and then look them up from a container. This is useful for managing dependencies and ensuring that services are only instantiated when needed.
const { Container, Registry } = require('@glimmer/di');
class MyService {
constructor() {
this.name = 'MyService';
}
}
const registry = new Registry();
registry.register('service:my-service', MyService);
const container = new Container(registry);
const myService = container.lookup('service:my-service');
console.log(myService.name); // Output: MyService
Injecting Dependencies
This feature allows you to inject dependencies into your services. By registering injections, you can ensure that services receive the dependencies they need without having to manually pass them around.
const { Container, Registry, inject } = require('@glimmer/di');
class Logger {
log(message) {
console.log(message);
}
}
class MyService {
constructor(logger) {
this.logger = logger;
}
doSomething() {
this.logger.log('Doing something!');
}
}
const registry = new Registry();
registry.register('service:logger', Logger);
registry.register('service:my-service', MyService);
registry.registerInjection('service:my-service', 'logger', 'service:logger');
const container = new Container(registry);
const myService = container.lookup('service:my-service');
myService.doSomething(); // Output: Doing something!
Singleton Services
This feature allows you to register services as singletons, ensuring that only one instance of the service exists within the container. This is useful for services that maintain state or need to be shared across different parts of the application.
const { Container, Registry } = require('@glimmer/di');
class SingletonService {
constructor() {
this.counter = 0;
}
increment() {
this.counter++;
}
}
const registry = new Registry();
registry.register('service:singleton', SingletonService, { singleton: true });
const container = new Container(registry);
const singleton1 = container.lookup('service:singleton');
const singleton2 = container.lookup('service:singleton');
singleton1.increment();
console.log(singleton2.counter); // Output: 1
Inversify is a powerful and flexible dependency injection library for JavaScript and TypeScript. It provides a rich set of features for managing dependencies, including support for decorators, middleware, and more. Compared to @glimmer/di, Inversify offers a more extensive API and is suitable for a wider range of applications beyond Glimmer.js.
Awilix is a lightweight dependency injection container for JavaScript and TypeScript. It focuses on simplicity and ease of use, providing a straightforward API for managing dependencies. While it may not have all the advanced features of Inversify, it is a good alternative for developers looking for a simple and effective DI solution. Compared to @glimmer/di, Awilix is more general-purpose and not specifically tied to any framework.
TypeDI is a dependency injection library for TypeScript and JavaScript that leverages decorators and metadata to provide a clean and intuitive API. It is designed to work well with TypeScript's type system, making it a good choice for TypeScript projects. Compared to @glimmer/di, TypeDI offers a more modern and type-safe approach to dependency injection.
Dependency injection for Glimmer applications.
Dependency injection is a pattern that increases the flexibility, testability and consistency of your code.
The three key ideas are:
Let's look at a short example that does not use dependency injection. We'll write a hypothetical server that renders a short HTML document when an incoming request is received:
import HTTPServer from "./servers/http";
export default class HelloWorldServer {
constructor() {
let server = new HTTPServer({
port: 80
});
server.on('request', req => {
req.write("<html><body>Hello, world!</body></html>");
});
}
}
This is great, but there's one problem. As you can see, our Hello World server is importing the HTTP library directly. If we want to support both HTTP and HTTP/2 (or even something like a WebSocket), this code is not reusable.
We would have to either duplicate this code, or add some configuration options to let the user tell us which protocol they want to use. Of course, that would work today, but if we wanted to support HTTP/3 in the future, we'd have to come back and add a new configuration option for every new protocol.
What if, instead of telling the server what protocol to use, we could instead provide it with an object that encapsulated all of those concerns?
Instead of having our HelloWorldServer
import and instantiate HTTPServer
directly, we can provide it with an object that we guarantee implements the same
interface. In this case, that means any object that emits a 'request'
event
and supports adding an event listener with the on()
method.
Let's look at what that updated example might look like:
export default class HelloWorldServer {
constructor(server) {
server.on('request', req => {
req.write("<html><body>Hello, world!</body></html>");
});
}
}
Now we're no longer concerned with instantiating and configuring an HTTP server.
All we have to know is that whatever object gets passed to our class has an
on()
method that lets us add an event listener.
Now, let's look at a few different ways we can use our newly improved Hello World server.
import HelloWorldServer from "./hello-world-server";
import HTTPServer from "./servers/http";
import HTTP2Server from "./servers/http2";
import WebSocketServer from "./servers/web-socket";
// HTTP 1
let httpServer = new HTTPServer({
port: 80
});
new HelloWorldServer(httpServer);
// HTTP 2
let http2Server = new HTTP2Server({
port: 4200
});
new HelloWorldServer(http2Server);
// WebSocket
let wsServer = new WebSocketServer();
new HelloWorldServer(wsServer);
With that one small change, we've dramatically improved the reusability and flexibility of our Hello World server. It can now handle any protocol, even ones that didn't exist when it was written, so long as they can be adapted to follow the simple interface we've defined.
This idea may seem simple, but it has profound implications for managing the complexity of your code as your application grows. And it means that you can swap in different pieces of code easily depending on the environment.
For example, in unit tests we may want to swap in some stub objects to verify some behavior. Dependency injection makes it easy and avoids having to override global values.
We can also make it possible to run the same application on both Node.js and the browser, by swapping in one piece of framework code when you have a full DOM implementation and another implementation when you don't.
While dependency injection is just a simple pattern, it helps to have that pattern formalized into code. That's exactly what this library does: implement an incredibly lightweight version of dependency injection, with some utilities to help us clean up after ourselves when we're done running the app.
The two core parts of the Glimmer DI system are the Registry
and the Container
.
Here's how to remember the role of each:
Registry
is where you register code (that is, JavaScript classes).Container
contains objects, and is where you request instances of
registered classes.If that sounds confusing, let's look at an example that should make it clearer.
Let's say I have a class for a UI component that I want to make available to the
system. The first thing I would do is create a new Registry
instance and tell
it about my class.
import { Registry } from '@glimmer/di';
import ProfileComponent from './components/profile';
let registry = new Registry();
registry.register('component:profile', ProfileComponent);
You probably noticed the string that we're passing to the register
method:
'component:profile'
. This is what we call a specifier, which is a unique
identifier for a class. They take the form of ${type}:${name}
. In this case,
we have a UI component called Profile
so its specifier would be
'component:profile'
. If instead we had an blog post model, its specifier might
be 'model:blog-post'
.
So now we've told the Registry
about our component. Let's get an instance of
that component now. To do that, we'll need to create a new Container
, tell it
about our registry, and then ask it for the component we want:
import { Container } from '@glimmer/di';
// Create the container and pass in the registry we previously created.
let container = new Container(registry);
let component = container.lookup('component:profile');
Now our component
variable contains an instance of the previously-registered
profile component.
One important thing to note is that (by default) every time you call the
lookup
method, you'll get the same instance of the component:
let component1 = container.lookup('component:profile');
let component2 = container.lookup('component:profile');
component1 === component2; // => true
But that's not the behavior we want: in an app, you need to be able to create many instances of the same component.
In this case, we want to change the default behavior and tell the registry that
we should always get a new instance when we call
lookup('component:profile')
:
registry.registerOption('component:profile', 'singleton', false);
Here, we've set the singleton
option to false
for this component. We could
have also configured this setting back when we originally registered the component:
registry.register('component:profile', ProfileComponent, {
singleton: false
});
Now if we lookup multiple components, we'll get a different instance each time:
let component3 = container.lookup('component:profile');
let component4 = container.lookup('component:profile');
component3 === component4; // => false
So far, this doesn't seem to offer any benefits over just instantiating the class ourselves whenever we need a new instance. Let's look at one of the killer features: injections.
An injection is a rule that tells the container to automatically give one object access to another.
For example, let's imagine we have a centralized data store that we want to make available to all of our components, so they can retrieve model data over the network. Without worrying about how components get created in our framework, we just want to say: "every time a new component is instantiated, make sure it has access to the data store."
We can set this up automatically with an injection. First, let's register the data store with the registry:
import DataStore from "./data/store";
registry.register('store:main', DataStore);
Because we want components to share a single store instance, note that we didn't
disable the default singleton
setting. For the whole app, there will be just
one store.
(If there's only one instance of a particular type in an app, we often call it
main
. In this case, because there's one store and it's a singleton, its
specifier is store:main
. There's nothing special about this name, though; it's
just a common convention.)
Next, we'll create a rule that tells the registry that new components should be provided with the data store instance:
registry.registerInjection('component', 'store', 'store:main');
Let's look at each of these arguments to registerInjection
. Each one helps define part
of the injection rule. In this case, it means:
component
created,store
property tostore:main
In other words, every time container.lookup('component:profile')
gets called,
something like this is happening under the hood:
let store = container.lookup('store:main');
return ProfileComponent.create({ store });
The nice thing about injections is that we can set up a rule once and not worry about the details of where and how instances actually get created. This separation of concerns allows for less brittle code.
You've also now seen why specifiers contain information about both name and type. Injections let us specify rules that apply to all instances of a component, say, without having to repeat that rule for every component in the system.
So far, we've always had to tell the Registry
about a class before we're able
to get an instance from the Container
. But if we're being good developers, and
organizing our code well and being consistent in our naming, shouldn't our app
be able to find our classes automatically?
That's exactly what the Resolver
helps us do. With a resolver, we can define
rules that map specifiers (like component:profile
) on to module names (like
app/components/profile.js
).
A simple Resolver
implements a single method, retrieve()
, which takes a
specifier and returns the associated class.
Lets write a resolver that will load the component class using CommonJS instead of having to eagerly register every component in our app:
class Resolver {
retrieve(specifier) {
let [type, name] = specifier.split(':');
if (type !== 'component') { throw new Error("Unsupported type"); }
return require(`./app/${type}s/${name}.js`);
}
}
let registry = new Registry();
let resolver = new Resolver();
let container2 = new Container(registry, resolver);
// Make sure components aren't singletons
registry.registerOption('component', 'singleton', false);
// Requires and instantiates `./app/components/admin-page.js`:
let adminPage = container2.lookup('component:admin-page');
Note that retrieve()
must return synchronously. Your module loader therefore
must return synchronously, as it does in this CommonJS example. If you're using an
asynchronous module loader, you'll need to make sure modules are loaded before you
start instantiating objects.
As a general rule, this package is designed to be synchronous to achieve maximum performance; it is your responsibility to ensure that code is ready before it is needed.
One last thing: you may have noticed that the container in this example has both a registry and a resolver. The container will look for classes in both, but the registry always takes precedence. If the registry is empty, the container will fall back to asking the resolver for its help.
Thanks to Monegraph and Cerebris for funding the initial development of this library.
MIT License.
FAQs
Dependency injection support for Glimmer applications.
We found that @glimmer/di demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 11 open source maintainers collaborating on the project.
Did you know?
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.
Security News
Research
The Socket Research Team breaks down a malicious wrapper package that uses obfuscation to harvest credentials and exfiltrate sensitive data.
Research
Security News
Attackers used a malicious npm package typosquatting a popular ESLint plugin to steal sensitive data, execute commands, and exploit developer systems.
Security News
The Ultralytics' PyPI Package was compromised four times in one weekend through GitHub Actions cache poisoning and failure to rotate previously compromised API tokens.