sastre v0.3.10
Install with npm:
$ npm install sastre --saveOr yarn:
$ yarn add sastreQuick start
Please see the example file for a reference integration which includes:
- Support for
module-alias - Container interface for
models:User— Sequelize modelToken— ES6 class definition
- Container interface for
controllers:UserController— GRPC controller
Also, you can execute
npm run test:exampleto run their unit-tests.
How it works?
The Resolver class will turn directories containing index.js files into object definitions.
$ tree example/src/api/models
├── Token
│ └── index.js
└── User
├── classMethods
│ └── add
│ ├── index.js
│ ├── index.test.js
│ └── provider.js
└── index.js
4 directories, 5 filesAs result you'll get approximately the following module definition:
const Token = require('./Token');
const add = require('./User/classMethods/add');
module.exports = {
Token,
User: {
classMethods: {
add,
},
},
};Above:
- Index files are our entry points, even having tons of files, only indexes are taken into account to conform the object shape.
- Entry points can be skipped on intermediate modules like
classMethods.
Nesting modules has no depth limits, you can produce almost any kind of object using this technique.
Since 0.1.1 module and method names are converted from dash-case to PascalCase and camelCase respectively. This would help to alleviate issues with case-sensitiveness on some environments, especially when doing cross-environment development, e.g. Linux vs OSx.
Since 0.3.0 module resolution supports both CommonJS and ESM, the Resolver class now has a resolve() method that works asynchronously. This would help to load ESM modules and CJS without issues, previous code must be updated to use this method.
Containers
An instantiated Resolver becomes a container instance once resolved, you name it:
const { Resolver } = require('sastre');
const optionalHooks = {
before(name, definition) {},
after(name, definition) {},
};
const sourcesDirectory = `${__dirname}/lib`;
async function main() {
const container = await new Resolver(null, sourcesDirectory, optionalHooks).resolve();
}
main();You can access found modules through the get() method:
const Test = container.get('Test');From this point you can start hacking happily.
Providers
The Injector class is used to flag out any entry point that demands extra stuff.
Any found provider.js files will be used to retrieve dependencies from the container:
module.exports = {
getAnything() { /* yes, it's empty; keep reading ;-) */ },
getFromContainer() {
// `this` is null here due `new Resolver(null, ...)`
return this;
},
};Above:
- Function names are used to retrieve dependencies from the container.
- It's OK if they're empty, but if you return any truthy value it'll be used in place.
- Provider functions can access the context given on
new Resolver()instantiation.
Now, index.js MUST exports a function factory which will only receive the resolved dependencies.
module.exports = ({ Anything }) =>
function something() {
return new Anything();
};Please use arrow-functions to inject values ase they're detected for this purpose, regular functions will not work.
Any returned value is placed into the final module definition, as their lexical scope makes this DI pattern works.
This would work for any kind of methods, even those from prototype.
Hooks
Modules resolved can be modified or replaced by custom decorators too.
— before
Once Resolver.scanFiles() is done, use this hook to modify or replace the original definition received.
During this hook you can access the original class if given, without instantiate.
— after
Once a definition is unwrapped through container.get() and still unlocked, use this hook to modify or replace the final definition built.
Here you can access the instantiated class if given, partially injected. In this step you can perform property-injection based on your needs.
FAQ's
Why module-alias/register?
The main goal of this library is getting rid of require calls from a well-know architecture do we use.
We're also looking the benefits and pitfalls beyond splitting everything into submodules.
But loading parent stuff for common fixtures it's still an issue, e.g. ../../../../
Should be enough to
require('@src/models/User/fixtures')isn't it?
Why get<WHATEVER>?
E.g. you want a Session object within a method.
Using Session() { ... } is not enough clear, because Session() says nothing.
Instead, getSession() { ... } helps to understand the purpose of the method... effectively.
The
getprefix is always stripped out, so you can useSessionorgetSessionand the identifier will remain the same. This is useful if you want to get simple values instead, e.g.serviceLocator() { ... }
How compose stuff?
A nice way to build complex dependencies through provider.js files is:
module.exports = {
userRepo({ User, Repository, SequelizeDatasource }) {
const dbUser = new SequelizeDatasource(User);
const repo = new Repository(dbUser);
return repo;
},
getUser() {},
getRepository() {},
getSequelizeDatasource() {},
};And then you can inject them, e.g.
module.exports = ({ userRepo }) =>
async function getUsers() {
return await userRepo.findAll();
};How inject classes?
The included example implements an advanced container for higher IoC composition.
ES6 classes are automatically decorated to receive all provided dependencies.
You can access other containers through the root-container, it is given as
thison all providers.
What are root-providers?
Individual provider.js files are intended to decorate in-place methods or whole containers.
So, placing provider.js files in the same directory where modules are scanned is enough to serve as defaults, e.g.
$ tree example/src/api/controllers
├── UserController
│ └── index.js
└── provider.js
1 directory, 2 filesWhat are chainables?
The Chainable class lets you do things like this:
const run = new Chainable(null, {
async test() {
return Promise.resolve(42);
},
anythingElse() {
console.log('OK');
},
});
await run($ => $.test.anythingElse());
await run($ => $.anythingElse.test());
await run(({ test }) => test.anythingElse());
await run(({ anythingElse }) => anythingElse.test());This would help you to chain functions in order, both sync or async are supported.
It's possible to use TypeScript?
Yes, basic support for .d.ts files is built-in, just call Resolve.typesOf(container) to get an array of the samples.
Each sample contains a chunk, a string containing the type definition, and optionally a type property that contains the module name, e.g.
const buffer = Resolver.typesOf(container).map(x => (x.type ? [`// ${x.type}`] : []).concat(x.chunk).join('\n')).join('\n');The resulting buffer should be something like this:
import type { nested as TestSubNestedModule } from './Test/sub/nested';
import type { method as NamePropMethodModule } from './Name/prop/method';
import type { injectableMethod as NamePropInjectableMethodModule } from './Name/prop/injectableMethod';
import type { withDashesAnd as OtherTestWithDashesAndModule } from './OtherTest/with-dashes-and';
import type { sub as TestSubModule } from './Test/sub';
import type TestModule from './Test';
import type ExampleModule from './Example';
// Example
export interface ExampleInterface extends ExampleModule {}
declare namespace Example {}
// Test
export interface TestInterface extends TestModule {
sub: Test.Sub;
}
declare namespace Test {
export interface Sub extends TestSubModule {
nested: typeof TestSubNestedModule;
}
}
// OtherTest
export interface OtherTestInterface {
withDashesAnd: OtherTest.WithDashesAnd;
}
declare namespace OtherTest {
export interface WithDashesAnd extends OtherTestWithDashesAndModule {}
}
// Name
export interface NameInterface {
prop: Name.Prop;
}
declare namespace Name {
export interface Prop {
injectableMethod: typeof NamePropInjectableMethodModule;
method: typeof NamePropMethodModule;
}
}Use the provided CLI to generate those declarations out of your containers, e.g.
sastre example/src/api -ti ../container :controllers :models -r module-alias/registerRun
sastre --helpto show all available options. TypeScript>=4.4.xis required to use the CLI.
The compiler would load any container from its path, if it contains a typedefs property it'll be used to write out the index.d.ts declaration.
Imported types are guessed from their directory names, e.g. ./Path/to/fn/index.js will result in a import { fn as PathToFnModule } from './Path/to/fn'; snippet.
Such modules should export their types to make TypeScript happy:
export type { fn };
export default function fn(): void {
console.log('OSOM');
}If you're injecting values through a provider.js file, your signature may look like this:
import type DI from '../../../provider';
declare function fn(x?: number): string;
export type { fn };
export default ({ dep }: DI): typeof fn => function fn() {
return dep.value;
}2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
5 years ago
6 years ago
6 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago
7 years ago