Initial commit

This commit is contained in:
Josh Goldberg
2018-01-04 05:06:29 -05:00
commit 8b9a6e9997
17 changed files with 740 additions and 0 deletions
+20
View File
@@ -0,0 +1,20 @@
dist/
docs/generated/
test/
node_modules/
*.css
*.d.ts
*.js*
!./*.js
!*.json
*.html
npm-debug.log
debug.log
# Added by shenanigans-manager for maps testing
Maps.test.ts
# Local development typically uses npm install --link
# Package lock files aren't updated by linked installs
package-lock.json
yarn.lock
+4
View File
@@ -0,0 +1,4 @@
node_modules/
test/
*.test.*
npm-debug.log
+1
View File
@@ -0,0 +1 @@
unsafe-perm = true
+8
View File
@@ -0,0 +1,8 @@
language: node_js
node_js:
- "node"
- "7"
script:
npm run setup && npm run verify
+7
View File
@@ -0,0 +1,7 @@
{
"editor.tabSize": 4,
"editor.trimAutoWhitespace": true,
"tslint.alwaysShowRuleFailuresAsWarnings": true,
"tslint.autoFixOnSave": true,
"typescript.tsdk": "node_modules/typescript/lib"
}
+18
View File
@@ -0,0 +1,18 @@
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+148
View File
@@ -0,0 +1,148 @@
<!-- {{Top}} -->
# BabyIoC
[![Greenkeeper badge](https://badges.greenkeeper.io/FullScreenShenanigans/BabyIoC.svg)](https://greenkeeper.io/)
[![Build Status](https://travis-ci.org/FullScreenShenanigans/BabyIoC.svg?branch=master)](https://travis-ci.org/FullScreenShenanigans/BabyIoC)
[![NPM version](https://badge.fury.io/js/babyioc.svg)](http://badge.fury.io/js/babyioc)
Infantile IoC decorator with almost no features.
<!-- {{/Top}} -->
BabyIoC is the smallest IoC container you'll ever see _(about 100 real lines of code!)_.
It's also got the fewest toys - it's only targeted for use by [GameStartr](https://github.com/FullScreenShenanigans/GameStartr).
Key tenants:
* Use TypeScript.
* All `@components` are members of the parent `@container` container class instance.
* Components and component `@dependency`s are stored as lazily evaluated getters: circular dependencies are fine!
## Usage
Each **@component** is a member of your root **@container** class.
Declare your components with their classes to have them automagically created as members of your class.
```typescript
import { component, container } from "babyioc";
class DependencyA { }
@container
class Container {
@component(DependencyA)
public readonly dependencyA: DependencyA;
}
const { dependencyA } = new Container();
```
Components can take each other in as **`@dependency`s.
```typescript
class DependencyA { }
class DependencyB {
@dependency(DependencyA)
public readonly dependencyA: DependencyA;
}
@container
class Container {
@component(DependencyB)
public readonly dependencyB: DependencyB;
@component(DependencyA)
public readonly dependencyA: DependencyA;
}
const { dependencyA, dependencyB } = new Container();
```
### Factories
Your components don't have to be direct classes with dependencies.
Pass functions that take in your container as an argument.
If you're using anonymous `() =>` arrow lambdas, because they don't have names, pass in the class name or a string identifier to store them under internally.
```typescript
class DependencyA {
public constructor(
public readonly member: string,
) { }
}
const memberValue = "memberValue";
const createDependencyA = () => new DependencyA(memberValue);
@container
class Container {
@component(createDependencyA, DependencyA)
public readonly dependencyA: DependencyA;
}
const { dependencyA } = new Container();
```
These factory functions have access to all the values on the container, including computed getters.
```typescript
class DependencyA {
public constructor(
public readonly memberA: string,
) { }
}
class DependencyB {
public constructor(
public readonly referenceA: DependencyA,
public readonly valueC: string,
) { }
}
const memberValueA = "memberValueA";
const createDependencyA = () => new DependencyA(memberValueA);
const createDependencyB = (instance: Container) => new DependencyB(dependencyA, container.valueC);
@container
class Container {
@component(createDependencyA, DependencyA)
public readonly dependencyA: DependencyA;
@component(createDependencyB, DependencyB)
public readonly dependencyB: DependencyB;
public readonly valueC = "valueC";
}
const { dependencyA, dependencyB } = new Container();
```
<!-- {{Development}} -->
## Development
```
git clone https://github.com/FullScreenShenanigans/BabyIoC
cd BabyIoC
npm run setup
npm run verify
```
* `npm run setup` creates a few auto-generated setup files locally.
* `npm run verify` builds, lints, and runs tests.
### Building
```shell
npm run watch
```
Source files are written under `src/` in TypeScript and compile in-place to JavaScript files.
`npm run watch` will directly run the TypeScript compiler on source files in watch mode.
Use it in the background while developing to keep the compiled files up-to-date.
### Running Tests
```shell
npm run test
```
Test files are alongside source files under `src/` and named `*.test.ts?`.
Whenever you add, remove, or rename a `*.test.ts?` file under `src/`, re-run `npm run test:setup` to regenerate the list of static test files in `test/index.html`.
You can open that file in a browser to debug through the tests.
`npm run test` will run that setup and execute tests using [Puppeteer](https://github.com/GoogleChrome/puppeteer).
<!-- {{/Development}} -->
+72
View File
@@ -0,0 +1,72 @@
{
"author": {
"email": "joshuakgoldberg@outlook.com",
"name": "Josh Goldberg"
},
"browser": "./src/index.js",
"bugs": {
"url": "https://github.com/FullScreenShenanigans/BabyIoC/issues"
},
"dependencies": {
},
"description": "Infantile IoC decorator with almost no features.",
"devDependencies": {
"@types/chai": "^4.0.4",
"@types/lolex": "^2.1.1",
"@types/mocha": "^2.2.44",
"@types/sinon": "^4.0.0",
"@types/sinon-chai": "^2.7.29",
"chai": "^4.1.2",
"glob": "^7.1.2",
"lolex": "^2.3.0",
"mkdirp": "^0.5.1",
"mocha": "^4.0.1",
"mocha-headless-chrome": "^1.7.1",
"requirejs": "^2.3.5",
"run-for-every-file": "^1.1.0",
"shenanigans-manager": "^0.2.7",
"sinon": "^4.1.2",
"sinon-chai": "^2.14.0",
"tslint": "5.8.0",
"tsutils": "^2.14.0",
"typedoc": "^0.9.0",
"typescript": "^2.6.2",
"webpack": "^3.10.0"
},
"license": "MIT",
"name": "babyioc",
"repository": {
"type": "git",
"url": "ssh://git@github.com:FullScreenShenanigans/BabyIoC.git"
},
"scripts": {
"dist": "npm run dist:webpack",
"dist:webpack": "webpack",
"docs": "npm run docs:typedoc",
"docs:typedoc": "typedoc src/ --exclude **/*.d.ts --ignoreCompilerErrors --out docs/generated",
"init": "npm install && npm run setup && npm run verify",
"setup": "npm run setup:copy && npm run setup:package && npm run setup:readme",
"setup:copy": "npm run setup:copy:default",
"setup:copy:default": "run-for-every-file --dot --src \"node_modules/shenanigans-manager/setup/default/\" --file \"**/*\" --run \"mustache package.json {{src-file}} {{file}}\" --dest \".\" --only-files",
"setup:package": "shenanigans-manager hydrate-package-json",
"setup:readme": "shenanigans-manager hydrate-readme",
"src": "npm run src:tsc && npm run src:tslint",
"src:tsc": "tsc -p .",
"src:tslint": "tslint -c tslint.json -e ./node_modules/**/*.ts* -p tsconfig.json -t stylish",
"test": "npm run test:setup && npm run test:run",
"test:run": "mocha-headless-chrome --file test/index.html",
"test:setup": "npm run test:setup:dir && npm run test:setup:copy && npm run test:setup:html && npm run test:setup:tsc",
"test:setup:copy": "npm run test:setup:copy:default",
"test:setup:copy:default": "run-for-every-file --dot --src \"node_modules/shenanigans-manager/setup/test/\" --file \"**/*\" --run \"mustache package.json {{src-file}} ./test/{{file}}\" --dest \".\" --only-files",
"test:setup:dir": "mkdirp test",
"test:setup:html": "shenanigans-manager generate-test-html",
"test:setup:tsc": "tsc -p test",
"verify": "npm run src && npm run test && npm run dist && npm run docs",
"watch": "tsc -p . -w"
},
"shenanigans": {
"name": "BabyIoC"
},
"types": "./src/index.d.ts",
"version": "0.7.0"
}
+69
View File
@@ -0,0 +1,69 @@
import { getFunctionName } from "./Reading";
export type IContainerClass = IClass & {
__listings__?: { [i: string]: IComponentListing };
};
export type IClass = IClassWithArgs | IClassWithoutArgs;
export interface IClassWithArgs {
new(...args: any[]): any;
}
export interface IClassWithoutArgs {
new(): any;
}
export type IComponentClassOrFunction = IComponentClass | IComponentFunction;
export interface IComponentClass {
new(container: IClass): any;
}
export type IComponentFunction = (container: IClass) => any;
/**
* Describes how to create and store a component.
*/
export interface IComponentListing {
/**
* Class or function to create the component.
*/
componentFunction: IComponentClassOrFunction;
/**
* Unique name for the class or function to create this component.
*/
listingName: string;
/**
* Member property name the component will be stored under.
*/
memberName: string;
}
/**
* Adds a member component to a parent container.
*
* @param componentFunction Class or function that creates the component.
* @param name Name to store the component under, if not the class' or function's .name.
*/
export const component = (componentFunction: any /* IComponentClassOrFunction */, name?: string | Function) => {
if (typeof name === "function") {
name = getFunctionName(name);
}
const listingName = name === undefined
? getFunctionName(componentFunction)
: name;
return (parentClass: any /* IContainerClass */, memberName: string) => {
const listing: IComponentListing = { componentFunction, listingName, memberName };
if (parentClass.__listings__ === undefined) {
parentClass.__listings__ = {};
}
parentClass.__listings__[listing.listingName] = listing;
};
};
+163
View File
@@ -0,0 +1,163 @@
import { expect } from "chai";
import { component } from "./Component";
import { container } from "./Container";
import { dependency } from "./Dependency";
describe("container", () => {
it("resolves a component dependency", () => {
// Arrange
class DependencyA { }
@container
class Container {
@component(DependencyA)
public readonly dependencyA: DependencyA;
}
// Act
const { dependencyA } = new Container();
// Assert
expect(dependencyA).to.be.instanceOf(DependencyA);
});
it("resolves two component dependencies out of alphabetical order", () => {
// Arrange
class DependencyA { }
class DependencyB { }
@container
class Container {
@component(DependencyB)
public readonly dependencyB: DependencyB;
@component(DependencyA)
public readonly dependencyA: DependencyA;
}
// Act
const { dependencyA, dependencyB } = new Container();
// Assert
expect(dependencyA).to.be.instanceOf(DependencyA);
expect(dependencyB).to.be.instanceOf(DependencyB);
});
it("adds a dependency to a component", () => {
// Arrange
class DependencyA { }
class DependencyB {
@dependency(DependencyA)
public readonly dependencyA: DependencyA;
}
@container
class Container {
@component(DependencyB)
public readonly dependencyB: DependencyB;
@component(DependencyA)
public readonly dependencyA: DependencyA;
}
// Act
const { dependencyA, dependencyB } = new Container();
// Assert
expect(dependencyB.dependencyA).to.be.equal(dependencyA);
});
it("creates a component using a factory", () => {
// Arrange
class DependencyA {
public constructor(
public readonly member: string,
) { }
}
const memberValue = "memberValue";
const createDependencyA = () => new DependencyA(memberValue);
@container
class Container {
@component(createDependencyA, DependencyA)
public readonly dependencyA: DependencyA;
}
// Act
const { dependencyA } = new Container();
// Assert
expect(dependencyA.member).to.be.equal(memberValue);
});
it("creates different components using factories and their naming classes", () => {
// Arrange
class DependencyA {
public constructor(
public readonly memberA: string,
) { }
}
class DependencyB {
public constructor(
public readonly memberB: string,
) { }
}
const memberValueA = "memberValueA";
const memberValueB = "memberValueB";
const createDependencyA = () => new DependencyA(memberValueA);
const createDependencyB = () => new DependencyB(memberValueB);
@container
class Container {
@component(createDependencyA, DependencyA)
public readonly dependencyA: DependencyA;
@component(createDependencyB, DependencyB)
public readonly dependencyB: DependencyB;
}
// Act
const { dependencyA, dependencyB } = new Container();
// Assert
expect(dependencyA.memberA).to.be.equal(memberValueA);
expect(dependencyB.memberB).to.be.equal(memberValueB);
});
it("passes the container after creating getters to factories", () => {
// Arrange
class DependencyA {
public constructor(
public readonly memberA: string,
) { }
}
class DependencyB {
public constructor(
public readonly referenceA: DependencyA,
public readonly valueC: string,
) { }
}
const memberValueA = "memberValueA";
const createDependencyA = () => new DependencyA(memberValueA);
const createDependencyB = (instance: Container) => new DependencyB(dependencyA, instance.valueC);
@container
class Container {
@component(createDependencyA, DependencyA)
public readonly dependencyA: DependencyA;
@component(createDependencyB, DependencyB)
public readonly dependencyB: DependencyB;
public readonly valueC: string;
}
// Act
const { dependencyA, dependencyB } = new Container();
// Assert
expect(dependencyA.memberA).to.be.equal(memberValueA);
expect(dependencyB.referenceA).to.be.equal(dependencyA);
});
});
+70
View File
@@ -0,0 +1,70 @@
import { IClassWithArgs, IComponentListing } from "./Component";
/**
* Creates a getter method for a lazily computed instance.
*
* @param resolve Resolves the value of the instance.
* @returns A getter method for a lazilyi computed instance.
*/
const createLazyInstance = <TInstance>(resolve: () => any) => {
let instance: TInstance | undefined;
return () => {
if (instance === undefined) {
instance = resolve();
}
return instance;
};
};
/**
* Resolves the value of a lazily instantiated component.
*
* @param parentContainerInstance Parent container creating this component.
*
*/
const resolveComponent = (parentContainerInstance: any, listing: IComponentListing): any => {
const { componentFunction } = listing;
const componentInstance = new (componentFunction as IClassWithArgs)(parentContainerInstance);
const dependencies = componentFunction.prototype.__dependencies__;
if (dependencies !== undefined) {
for (const dependency of dependencies) {
Object.defineProperty(componentInstance, dependency.memberName, {
configurable: true,
get: createLazyInstance(() =>
parentContainerInstance[parentContainerInstance.__listings__[dependency.dependencyName].memberName]),
});
}
}
return componentInstance;
};
/**
* Creates a class that creates getters to resolve its components.
*/
export const container = (containerClass: { new(...args: any[]): any }): any => {
const createdComponents: any = {};
return class extends containerClass {
public constructor(...args: any[]) {
super(...args);
const listings = containerClass.prototype.__listings__;
for (const listingName in listings) {
if (!{}.hasOwnProperty.call(listings, listingName)) {
continue;
}
const listing = listings[listingName];
Object.defineProperty(this, listing.memberName, {
configurable: true,
get: createLazyInstance(() => resolveComponent(this, listing)),
});
}
}
};
};
+37
View File
@@ -0,0 +1,37 @@
import { getFunctionName } from "./Reading";
/**
* Describes a dependency of a class.
*/
export interface IDependencyListing {
/**
* Class or function name of the dependency.
*/
dependencyName: string;
/**
* Member property name the instance will be stored under.
*/
memberName: string;
}
/**
* Adds a dependency to a component class.
*
* @param componentFunction Class or function creates the component, unique string name thereof.
*/
export const dependency = (dependencyName: string | Function): any => {
if (typeof dependencyName === "function") {
dependencyName = getFunctionName(dependencyName);
}
return (dependingClass: any, memberName: string) => {
const listing = { dependencyName, memberName };
if (dependingClass.__dependencies__) {
dependingClass.__dependencies__.push(listing);
} else {
dependingClass.__dependencies__ = [listing];
}
};
};
+51
View File
@@ -0,0 +1,51 @@
import { expect } from "chai";
import { getFunctionName } from "./Reading";
describe("Reading", () => {
describe("getFunctionName", () => {
it("returns the name of a function when there is a .name", () => {
// Arrange
const name = "test";
const object = { name };
// Act
const result = getFunctionName(object);
// Assert
expect(result).to.be.equal(name);
});
it("returns the name of a function when there is no .name and .toString results in a function", () => {
// Arrange
const name = "test";
const object = {
toString() {
return `function ${name}() { /* ... */ }`;
},
};
// Act
const result = getFunctionName(object as any);
// Assert
expect(result).to.be.equal(name);
});
it("returns the name of a function when there is no .name and .toString results in a class", () => {
// Arrange
const name = "test";
const object = {
toString() {
return `class ${name} { /* ... */ }`;
},
};
// Act
const result = getFunctionName(object as any);
// Assert
expect(result).to.be.equal(name);
});
});
});
+28
View File
@@ -0,0 +1,28 @@
/**
* Object with a name.
*/
export interface IObjectWithName {
/**
* Name of the object.
*/
name: string;
}
/**
* Gets the friendly name of a class or function.
*
* @param method Class or function which may have a name.
* @returns Friendly name of the class or function.
*/
export const getFunctionName = (method: IObjectWithName | Function): string => {
if ((method as IObjectWithName).name !== undefined) {
return (method as IObjectWithName).name;
}
const stringified = method.toString();
const typeDescriptorMatch = stringified.match(/class|function/)!;
const indexOfNameSpace = typeDescriptorMatch.index! + typeDescriptorMatch[0].length;
const indexOfNameAfterSpace = stringified.search(/\(|\{/);
return stringified.substring(indexOfNameSpace, indexOfNameAfterSpace).trim();
};
+3
View File
@@ -0,0 +1,3 @@
export { component } from "./Component";
export { container } from "./Container";
export { dependency } from "./Dependency";
+25
View File
@@ -0,0 +1,25 @@
{
"compilerOptions": {
"declaration": true,
"experimentalDecorators": true,
"jsx": "react",
"lib": ["dom", "es2015.collection", "es2015.promise", "es5"],
"module": "amd",
"moduleResolution": "node",
"noImplicitAny": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noFallthroughCasesInSwitch": true,
"pretty": true,
"strictNullChecks": true,
"target": "es5"
},
"exclude": [
"dist",
"node_modules"
],
"include": [
"./src/**/*.ts",
"./src/**/*.tsx"
]
}
+16
View File
@@ -0,0 +1,16 @@
{
"extends": "./node_modules/shenanigans-manager/setup/tslint.json",
"linterOptions": {
"exclude": [
"./node_modules/**/*"
]
},
"rules": {
"no-any": false,
"no-non-null-assertion": false,
"no-parameter-properties": false,
"no-unsafe-any": false,
"ban-types": false,
"max-classes-per-file": [true, "exclude-class-expressions"]
}
}