Skip to content

Commit

Permalink
Factor out and use core types (#3)
Browse files Browse the repository at this point in the history
Working candidate
  • Loading branch information
danieldietrich authored Oct 24, 2022
1 parent 1411e38 commit 1d22ee2
Show file tree
Hide file tree
Showing 22 changed files with 15,056 additions and 2,061 deletions.
79 changes: 79 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// see [ESLint Configuration](https://eslint.org/docs/user-guide/configuring)
{
// libs that run in both envs, browser & node, don't access global variables that are browser or node specific
"env": {
"browser": false, // enables Browser global variables, like localStorage
"es6": true, // enables new ES6 global variables, such as Set
"node": true // enables Node global variables, like process
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended"
],
"parser": "@typescript-eslint/parser",
"parserOptions": { // [typescript-eslint parser configuration](https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/parser#configuration)
"project": "tsconfig.eslint.json", // mandatory when using types
"sourceType": "module" // needed in order to use import declarations
},
"plugins": [
"@typescript-eslint",
"typescript-enum",
"unused-imports"
],
"rules": {

// List of [ESLint rules](https://eslint.org/docs/rules/)
"arrow-parens": ["off", "as-needed"], // do not force arrow function parentheses
"constructor-super": "error", // checks the correct use of super() in sub-classes
"curly": "error", // if statement needs curly braces
"dot-notation": "error", // obj.a instead of obj['a'] when possible
"eqeqeq": "error", // ban '==', don't use 'smart' option!
"guard-for-in": "error", // needs obj.hasOwnProperty(key) checks
"new-parens": "error", // new Error() instead of new Error
"no-bitwise": "error", // bitwise operators &, | can be confused with &&, ||
"no-caller": "error", // ECMAScript deprecated arguments.caller and arguments.callee
"no-cond-assign": "error", // assignments if (a = '1') are error-prone
"no-debugger": "error", // disallow debugger; statements
"no-eval": "error", // eval is considered unsafe
"no-inner-declarations": "off", // we need to have 'namespace' functions when using TS 'export ='
"no-labels": "error", // GOTO is only used in BASIC ;)
"no-multiple-empty-lines": ["error", {"max": 1}], // two or more empty lines need to be fused to one
"no-new-wrappers": "error", // there is no reason to wrap primitve values
"no-throw-literal": "error", // only throw Error but no objects {}
"no-trailing-spaces": "error", // trim end of lines
"no-unsafe-finally": "error", // safe try/catch/finally behavior
"no-unused-vars": "off", // we need unused vars for proper typing
"no-var": "error", // use const and let instead of var
"prefer-const": "error", // use const when possible
"quote-props": ["error", "as-needed", { // defines how object-keys are quoted
"keywords": false,
"unnecessary": true,
"numbers": false
}],
"space-before-function-paren": ["error", { // space in function decl: f() vs async () => {}
"anonymous": "never",
"asyncArrow": "always",
"named": "never"
}],
"unused-imports/no-unused-imports": "error", // no unsused imports
"use-isnan": "error", // isNaN(i) Number.isNaN(i) instead of i === NaN

// List of [typescript-enum rules](https://github.com/shian15810/eslint-plugin-typescript-enum)
"typescript-enum/no-enum": "error", // disallow enums, see https://2ality.com/2020/02/enum-alternatives-typescript.html

// List of [@typescript-eslint rules](https://github.com/typescript-eslint/typescript-eslint/tree/master/packages/eslint-plugin#supported-rules)
"@typescript-eslint/adjacent-overload-signatures": "error", // grouping same method names
"@typescript-eslint/array-type": ["error", { // string[] instead of Array<string>
"default": "array"
}],
"@typescript-eslint/indent": "error", // consistent indentation
"@typescript-eslint/consistent-type-assertions": "error", // needed for .tsx, bad = <Foo>bar, good = bar as Foo
"@typescript-eslint/no-misused-new": "error", // no constructors for interfaces or new for classes
"@typescript-eslint/no-parameter-properties": "error", // no property definitions in class constructors
"@typescript-eslint/no-var-requires": "error", // use import instead of require
"@typescript-eslint/prefer-for-of": "error", // prefer for-of loop over arrays
"@typescript-eslint/prefer-namespace-keyword": "error", // prefer namespace over module in TypeScript
"@typescript-eslint/triple-slash-reference": "error", // ban /// <reference />, prefer imports
"@typescript-eslint/type-annotation-spacing": "error" // consistent space around colon ':'
}
}
40 changes: 40 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
---
name: "Bug report"
about: Report a reproducible bug or regression.
labels: 'bug'

---

# Bug Report

Ginject version:

<!--
Please provide a clear and concise description of what the bug is. Include
screenshots if needed. Please test using the latest version of Ginject to
make sure your issue has not already been fixed.
-->

## Steps To Reproduce

1.
2.

<!--
Your bug will get fixed much faster if we can run your code and it doesn't
have dependencies other than Ginject. Issues without reproduction steps or
code examples may be immediately closed as not actionable.
-->

Link to code example:

<!--
Please provide a link to a repository on GitHub or provide a minimal code
example that reproduces the problem. You may provide a screenshot of some
application if you think it is relevant to your bug report. Here are some
tips for providing a minimal example: https://stackoverflow.com/help/mcve.
-->

## The current behavior

## The expected behavior
5 changes: 5 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
blank_issues_enabled: false
contact_links:
- name: Question
url: https://github.com/langium/ginject/discussions
about: Please ask questions here.
8 changes: 8 additions & 0 deletions .github/ISSUE_TEMPLATE/feature_request.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
name: Feature request
about: Suggest an idea for this project
labels: 'feature-request'

---
<!-- Please search existing issues to avoid creating duplicates. -->
<!-- Describe the feature you'd like. -->
7 changes: 7 additions & 0 deletions .github/ISSUE_TEMPLATE/improvement.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
name: General improvement
about: Suggest an improvement for this project

---
<!-- Please search existing issues to avoid creating duplicates. -->
<!-- Describe the improvement you'd like to see in the project. -->
27 changes: 27 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Build

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
build:
name: Ginject
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- name: Checkout
uses: actions/checkout@v2
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: '16'
- name: Build
shell: bash
run: |
npm ci
npm test
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.DS_Store
*.tgz
coverage/
dist/
lib/
node_modules/
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2021 TypeFox GmbH
Copyright (c) 2022 TypeFox GmbH

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
209 changes: 209 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
<div id="ginject-logo" align="center">
<a href="https://github.com/langium/ginject">
<img alt="Ginject Logo" width="450" src="https://user-images.githubusercontent.com/743833/193610222-cf9a7feb-b1d9-4d5c-88de-6ce9fbca8299.png">
</a>
<h3>
Dependency injection done right.
</h3>
</div>

<div id="badges" align="center">

[![npm version](https://img.shields.io/npm/v/ginject?logo=npm&style=flat-square)](https://www.npmjs.com/package/ginject/)
[![build](https://img.shields.io/github/workflow/status/langium/ginject/Build/main?logo=github&style=flat-square)](https://github.com/langium/ginject/actions/workflows/build.yml)
[![Gitpod ready-to-code](https://img.shields.io/badge/Gitpod-ready--to--code-blue?logo=gitpod&style=flat-square)](https://gitpod.io/#https://github.com/langium/ginject)

</div>

<br>

**Ginject** [ʤɪnject] is a non-intrusive and typesafe dependency injection library for Node.js and JavaScript, powered by TypeScript.

**Ginject** empowers developers designing decoupled applications and frameworks. **Ginject**'s main goal is increasing the developer experience by offering a tiny, yet powerful API, keeping dependencies in central module definitions and by using TypeScript's type system to restrain runtime challenges.

The concept of **ginject**'s central module definition is inspired by [Google Guice](https://github.com/google/guice). However, **ginject** is going further by lifting the API to the functional level.

Despite its simplicity, **ginject** is powerful enough to cover all features provided by [Inversify](https://github.com/inversify/InversifyJS). Direct support for classes and constructors, property injection, rebinding dependencies and dependency cycle detection are just a few of the features worth mentioning.

<br>

<div id="ginject vs inversify" align="center">

| | ginject | inversify |
|------------------|:----------:|:-----------:|
| minified | [![minified size](https://img.shields.io/bundlephobia/min/ginject?label=&style=flat-square)](https://bundlephobia.com/result?p=ginject@latest) | [![minified size](https://img.shields.io/bundlephobia/min/inversify?label=&style=flat-square)](https://bundlephobia.com/result?p=inversify@latest) |
| minzipped | [![minzipped size](https://img.shields.io/bundlephobia/minzip/ginject?label=&style=flat-square)](https://bundlephobia.com/result?p=ginject@latest) | [![minzipped size](https://img.shields.io/bundlephobia/minzip/inversify?label=&style=flat-square)](https://bundlephobia.com/result?p=inversify@latest) |
| typesafe |||
| requirements | none | decorators |
| style | functional | imperative |
| API surface area | tiny | non-trivial |

</div>

<br>

## Quickstart

The first step is to add **ginject** to your application.

```sh
npm i ginject
```

Bascially, the only thing needed is to define **modules** of **factories** and finally call **inject**. The resulting **container** provides concrete **instances**.

```ts
import { inject } from 'ginject';

// create an inversion of control container
const container = inject({
hi: () => 'Hi',
sayHi: () => (name: string) => `${container.hi} ${name}!`
});

// prints 'Hi Ginject!'
console.log(container.sayHi('Ginject'));
```

## API

### Terminology

The **inject** function is turning **modules** into a **container**. A **module** is a plain vanilla JS object, composed of nested **groups** and **dependency factories**. Factories may return any JS value, e.g. constants, singletons and providers. Unlike [Inversify](https://github.com/inversify/InversifyJS), there is no need to decorate classes.

```ts
import { inject, Module } from 'ginject';

// Defining a _context_ of dependencies
type Context = {
group: {
value: Value // any JS type, here a class
}
}

// A _module_ contains nested _groups_ (optional) and _factories_
const module: Module<Context> = {
group: {
// a factory of type Factory<Context, Value>
value: (ctx: Context) => new Value(ctx)
}
};

// A _container_ of type Container<Module<Context>> = Context
const container = inject(module);

// Values can be obtained from the container
const value = container.group.value;
```

### Context

A **container** provides each **factory** with a parameter called **context**.

```ts
type C = {
value: string
}

const container = inject({
factory: (ctx: C) => () => ctx.value
});
```

The **context** of type **C** provides a **value** that can't be resolved. The **inject** call is type-checked by TS the way that the completeness of the arguments is checked.

Such **missing dependencies** need to be provided by adding additional **modules** to the **inject** call.

```ts
const container = inject({
factory: (ctx: C) => () => ctx.value
}, {
value: () => '🍸'
});
```

Now the compiler is satisfied and we can start using the **container**.

```ts
// prints 🍸
console.log(container.factory());
```

You might have noticed that the **container** automatically calls the **factory** and **injects** itself as the **context**. The use-site receives the **value**.

### Eager vs lazy initialization

A dependency **container.group.value** is **lazily** initialized when first accessed on the container. Turn a factory **eager** to initialize the dependency at the time of the **inject** call.

A use case for **eager initialization** would be to ensure that **side effects** take place during the initialization of the **container**.

```ts
import { eager, inject, Module } from 'ginject';

type C = {
gin: string
}

const module: Module<C> = {
gin: eager(() => {
const gin = '🍸';
console.log('Gin mixed');
return gin;
})
}

const ctr = inject(module);

console.log('App started');

ctr.gin
```

In the **eager** case, the output is

```
Gin mixed
App started
```

In the **lazy** case, the output is

```
App started
Gin mixed
```

Please note that **eager factories** overwrite **lazy factories** vice versa when **rebinding** them.

### Rebinding dependencies

The main advantage of **dependency injection** arises from the fact that an application is able to **rebind dependencies**. That way the **structure** of a system can be fixated while the **behavior** can be changed.

The main vehicle for **rebinding dependencies** is the **inject** function which receives a variable amount of **modules**.

The behavior of an application can be enhanced by overwriting existing functionality using additional modules.

```ts
type C = {
readonly print: () => void
eval: (a: number, b: number) => number
}

const module_0: Module<C> = {
print: (ctx) => () => {
console.log(ctx.eval(1, 1));
},
eval: () => (a, b) => a + b
};

const ctr = inject(module_0, {
eval: () => (a: number, b: number) => a * b
});

// = 1
ctr.print();
```

### Cyclic Dependencies

### Asynchronous Factories
Loading

0 comments on commit 1d22ee2

Please sign in to comment.