├── .yarnrc ├── packages ├── pigly │ ├── src │ │ ├── declarations.ts │ │ ├── _constructor.ts │ │ ├── predicates │ │ │ ├── index.ts │ │ │ ├── named.ts │ │ │ ├── injected-into.ts │ │ │ └── has-ancestor.ts │ │ ├── _resolution.ts │ │ ├── _service.ts │ │ ├── errors.ts │ │ ├── _binding.ts │ │ ├── providers │ │ │ ├── maybe.ts │ │ │ ├── to-const.ts │ │ │ ├── when.ts │ │ │ ├── index.ts │ │ │ ├── when-all.ts │ │ │ ├── name.ts │ │ │ ├── to-func.ts │ │ │ ├── to-self.ts │ │ │ ├── to.ts │ │ │ ├── to-all.ts │ │ │ ├── to-class.ts │ │ │ └── defer.ts │ │ ├── _resolver-root.ts │ │ ├── _read-only-kernel.ts │ │ ├── _provider.ts │ │ ├── _request.ts │ │ ├── _kernel.ts │ │ ├── index.ts │ │ ├── _context.ts │ │ ├── _scope.ts │ │ └── kernel.ts │ ├── gen-package.mjs │ ├── tsconfig.json │ ├── tsconfig.build.cjs.json │ ├── tsconfig.build.esm.json │ ├── LICENSE.md │ ├── package.json │ ├── test │ │ ├── scoping.spec.ts │ │ └── kernel.spec.ts │ └── README.md └── pigly-transformer │ ├── test │ ├── _foo.ts │ ├── scoping.spec.ts │ ├── has-ancestor.spec.ts │ ├── named.spec.ts │ ├── injected-into.spec.ts │ ├── symbol-for.spec.ts │ ├── to-self.spec.ts │ └── kernel.spec.ts │ ├── src │ ├── index.ts │ └── transformer.ts │ ├── example.ts │ ├── tsconfig.build.cjs.json │ ├── tsconfig.json │ ├── LICENSE.md │ ├── README.md │ ├── .vscode │ └── launch.json │ └── package.json ├── .gitignore ├── lerna.json ├── tsconfig.json ├── pigly.code-workspace ├── codecov.yml ├── package.json ├── .vscode └── launch.json ├── .github └── dependabot.yml ├── LICENSE.md ├── .circleci └── config.yml └── README.md /.yarnrc: -------------------------------------------------------------------------------- 1 | workspaces-experimental true -------------------------------------------------------------------------------- /packages/pigly/src/declarations.ts: -------------------------------------------------------------------------------- 1 | export declare function SymbolFor(): symbol; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/coverage 3 | **/.nyc_output 4 | **/dist/ 5 | **/.nx -------------------------------------------------------------------------------- /packages/pigly-transformer/test/_foo.ts: -------------------------------------------------------------------------------- 1 | export interface IFoo { 2 | message: string; 3 | } 4 | -------------------------------------------------------------------------------- /packages/pigly/src/_constructor.ts: -------------------------------------------------------------------------------- 1 | export type Constructor = { 2 | new(...args: any[]): any; 3 | } 4 | -------------------------------------------------------------------------------- /packages/pigly-transformer/src/index.ts: -------------------------------------------------------------------------------- 1 | import {transformer} from "./transformer.js"; 2 | 3 | export = transformer; -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "packages/*" 4 | ], 5 | "npmClient": "yarn", 6 | "version": "2.0.0-alpha.9" 7 | } 8 | -------------------------------------------------------------------------------- /packages/pigly/src/predicates/index.ts: -------------------------------------------------------------------------------- 1 | export * from './has-ancestor' 2 | export * from './injected-into'; 3 | export * from './named'; -------------------------------------------------------------------------------- /packages/pigly/src/_resolution.ts: -------------------------------------------------------------------------------- 1 | export interface IResolution extends Iterable { 2 | toArray(): T[]; 3 | first(): T | undefined; 4 | } 5 | -------------------------------------------------------------------------------- /packages/pigly/src/_service.ts: -------------------------------------------------------------------------------- 1 | export type Service = symbol; 2 | 3 | export function isService(obj: any): obj is Service { 4 | return typeof obj === "symbol"; 5 | } -------------------------------------------------------------------------------- /packages/pigly/gen-package.mjs: -------------------------------------------------------------------------------- 1 | import fs from 'fs/promises' 2 | 3 | await fs.writeFile("./dist/esm/package.json",`{"type":"module"}`); 4 | await fs.writeFile("./dist/cjs/package.json",`{"type":"commonjs"}`); -------------------------------------------------------------------------------- /packages/pigly/src/errors.ts: -------------------------------------------------------------------------------- 1 | export class ResolveError extends Error{ 2 | constructor(public service, message: string = ""){ 3 | super("could not resolve " + service.valueOf().toString() + message) 4 | } 5 | } -------------------------------------------------------------------------------- /packages/pigly/src/_binding.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from "./_context"; 2 | import { Scope } from './_scope'; 3 | 4 | export interface IBinding { 5 | provider: (ctx: IContext) => any 6 | site: string; 7 | scope: Scope; 8 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* tsconfig.json */ 2 | { 3 | "compilerOptions": { 4 | "experimentalDecorators": true, 5 | /* other options */ 6 | "baseUrl": "./packages", 7 | "paths": { 8 | "@pigly/*": ["./*/src"] 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /packages/pigly/src/predicates/named.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from "../_context"; 2 | 3 | /** returns true if the parent's target name matches the parameter */ 4 | export function named(name: string): (ctx: IContext) => boolean { 5 | return (ctx: IContext) => ctx.parent && ctx.parent.target == name; 6 | } -------------------------------------------------------------------------------- /packages/pigly/src/providers/maybe.ts: -------------------------------------------------------------------------------- 1 | import { IProvider } from "../_provider"; 2 | 3 | export function maybe(provider: IProvider, fallback: T) { 4 | return (ctx) => { 5 | try { 6 | return provider(ctx); 7 | } catch (err) { 8 | return fallback; 9 | } 10 | } 11 | } -------------------------------------------------------------------------------- /packages/pigly/src/providers/to-const.ts: -------------------------------------------------------------------------------- 1 | import { IProvider } from "../_provider"; 2 | 3 | export function toValue(value: T): IProvider { 4 | //scoping? 5 | return (_) => {return value}; 6 | } 7 | 8 | export function toConst(value: T): IProvider { 9 | return _ => value; 10 | } -------------------------------------------------------------------------------- /packages/pigly/src/_resolver-root.ts: -------------------------------------------------------------------------------- 1 | import { IRequest } from "./_request"; 2 | import { IResolution } from "./_resolution"; 3 | import { Scope } from './_scope'; 4 | import { Service } from './_service'; 5 | 6 | export interface IResolverRoot { 7 | resolve(request: IRequest): IResolution; 8 | } 9 | -------------------------------------------------------------------------------- /packages/pigly/src/providers/when.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from "../_context"; 2 | import { IProvider } from "../_provider"; 3 | 4 | export function when(predicate: (ctx: IContext) => boolean, provider: IProvider) { 5 | return (ctx: IContext) => { 6 | if (predicate(ctx)) return provider(ctx); 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /pigly.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "name": "[root]", 5 | "path": "." 6 | }, 7 | { 8 | "name": "pigly", 9 | "path": "packages\\pigly" 10 | }, 11 | { 12 | "name": "@pigly/transformer", 13 | "path": "packages\\pigly-transformer" 14 | } 15 | ], 16 | "settings": {} 17 | } -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: 4 | default: 5 | threshold: 0.10% 6 | core: 7 | flags: core 8 | viewer: 9 | flags: viewer 10 | patch: off 11 | flags: 12 | core: 13 | paths: 14 | - packages/pigly 15 | viewer: 16 | paths: 17 | - packages/pigly-transformer 18 | -------------------------------------------------------------------------------- /packages/pigly/src/providers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './defer'; 2 | export * from './maybe'; 3 | export * from './name'; 4 | export * from './to-all'; 5 | export * from './to-class'; 6 | export * from './to-const'; 7 | export * from './to-func'; 8 | export * from './to-self'; 9 | export * from './to'; 10 | export * from './when'; 11 | export * from './when-all'; -------------------------------------------------------------------------------- /packages/pigly/src/_read-only-kernel.ts: -------------------------------------------------------------------------------- 1 | import { IResolverRoot } from "./_resolver-root"; 2 | import { Scope } from './_scope'; 3 | import { Service } from "./_service"; 4 | 5 | 6 | export interface IReadOnlyKernel extends IResolverRoot { 7 | get(): T; 8 | get(service: Service): T; 9 | getAll(): T[]; 10 | getAll(service: Service): T[]; 11 | } -------------------------------------------------------------------------------- /packages/pigly/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "lib": [ 7 | "es2015", 8 | "dom", 9 | ], 10 | "types": [ 11 | "mocha", 12 | "chai", 13 | "node" 14 | ] 15 | }, 16 | "exclude": [ 17 | "node_modules", 18 | ], 19 | } -------------------------------------------------------------------------------- /packages/pigly/src/_provider.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from "./_context"; 2 | 3 | export interface IProvider { 4 | (ctx: IContext): T; 5 | } 6 | 7 | export type ProviderWrap = T extends any[] 8 | ? { 9 | [P in keyof T]: IProvider; 10 | } : []; 11 | 12 | export function isProvider(obj: any): obj is IProvider{ 13 | return typeof(obj) == "function" 14 | } -------------------------------------------------------------------------------- /packages/pigly/tsconfig.build.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "CommonJS", 6 | "moduleResolution": "Node", 7 | "declaration": true, 8 | "sourceMap": false, 9 | "outDir": "./dist/cjs", 10 | "rootDir": "src/" 11 | }, 12 | "include": ["src/"], 13 | "exclude": ["node_modules"] 14 | } 15 | -------------------------------------------------------------------------------- /packages/pigly/src/providers/when-all.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from "../_context"; 2 | import { IProvider } from "../_provider"; 3 | 4 | export function whenAll(predicates: Array<(ctx: IContext) => boolean>, provider: IProvider) { 5 | return (ctx: IContext) => { 6 | for(let predicate of predicates){ 7 | if(predicate(ctx) == false) return undefined; 8 | } 9 | return provider(ctx); 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /packages/pigly/src/predicates/injected-into.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from "../_context"; 2 | 3 | export function injectedInto(symbol: symbol): (ctx: IContext) => boolean 4 | export function injectedInto(): never; 5 | export function injectedInto

(): (ctx: IContext) => boolean; 6 | export function injectedInto(symbol?: symbol): (ctx: IContext) => boolean { 7 | return (ctx: IContext) => ctx.parent && ctx.parent.service == symbol; 8 | } -------------------------------------------------------------------------------- /packages/pigly/tsconfig.build.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "ESNext", 6 | "moduleResolution": "Bundler", 7 | "declaration": true, 8 | "sourceMap": false, 9 | "outDir": "./dist/esm", 10 | "rootDir": "src/", 11 | }, 12 | "include": [ 13 | "src/" 14 | ], 15 | "exclude": [ 16 | "node_modules" 17 | ] 18 | } -------------------------------------------------------------------------------- /packages/pigly-transformer/example.ts: -------------------------------------------------------------------------------- 1 | import { Kernel, toConst, toSelf, whenAll, named, injectedInto, toClass, to, Scope, SymbolFor } from 'pigly'; 2 | 3 | function main() { 4 | const kernel = new Kernel(); 5 | 6 | class A { constructor() { } }; 7 | 8 | kernel.bind(SymbolFor(), toSelf(A), Scope.Singleton); 9 | 10 | console.log(kernel.get()); 11 | } 12 | 13 | console.log(main.toString()); 14 | 15 | main(); 16 | 17 | -------------------------------------------------------------------------------- /packages/pigly/src/_request.ts: -------------------------------------------------------------------------------- 1 | import { Service } from "./_service"; 2 | import { IContext } from "./_context"; 3 | import { Scope } from './_scope'; 4 | 5 | type ScopeObject = Record; 6 | 7 | export interface IRequest { 8 | /** the service being requested */ 9 | service: Service; 10 | /** when not null, the parent context */ 11 | parent?: IContext; 12 | /** name tag requested */ 13 | target?: string; 14 | } -------------------------------------------------------------------------------- /packages/pigly/src/providers/name.ts: -------------------------------------------------------------------------------- 1 | import { IProvider } from "../_provider"; 2 | 3 | /** 4 | * names the target site - child providers can then predicate on (parent) name. 5 | * note: js strict mode disallows access to constructor field names 6 | **/ 7 | export function name(target: string, provider: IProvider): IProvider { 8 | return (ctx) => { 9 | return provider(ctx.createContext({target})); 10 | } 11 | } -------------------------------------------------------------------------------- /packages/pigly-transformer/tsconfig.build.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "target": "ESNext", 5 | "module": "CommonJS", 6 | "moduleResolution": "Node", 7 | "esModuleInterop": false, 8 | "declaration": true, 9 | "sourceMap": false, 10 | "outDir": "./dist/cjs", 11 | "rootDir": "src/", 12 | }, 13 | "include": [ 14 | "src/" 15 | ], 16 | "exclude": [ 17 | "node_modules" 18 | ] 19 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "devDependencies": { 5 | "lerna": "^4.0.0" 6 | }, 7 | "workspaces": { 8 | "packages": [ 9 | "packages/*" 10 | ] 11 | }, 12 | "version": "0.0.0", 13 | "scripts": { 14 | "build": "lerna run build", 15 | "test": "lerna run test", 16 | "clean:dist": "rimraf **/*.tsbuildinfo && rimraf **/dist", 17 | "clean": "npm run clean:dist && rimraf **/node_modules", 18 | "coverage": "lerna run coverage" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /packages/pigly/src/providers/to-func.ts: -------------------------------------------------------------------------------- 1 | import { IProvider, ProviderWrap } from "../_provider"; 2 | import { IContext } from "../_context"; 3 | 4 | export function toFunc any)>( 5 | func: F, ...providers: ProviderWrap>): IProvider> { 6 | 7 | if (func === undefined) throw Error('called "toFunc" without a function argument'); 8 | 9 | return (ctx: IContext) => { 10 | return func(...providers.map(provider => provider ? provider(ctx) : undefined)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/pigly/src/providers/to-self.ts: -------------------------------------------------------------------------------- 1 | import { Constructor } from "../_constructor"; 2 | import { IProvider, ProviderWrap } from "../_provider"; 3 | import { toClass } from "./to-class"; 4 | 5 | ///** REQUIRES TRANSFORMER - create a class provider where the constructor arguments are inferable */ 6 | export function toSelf(ctor: C): IProvider> 7 | export function toSelf(ctor: C, ...providers: ProviderWrap>): IProvider> { 8 | return toClass(ctor, ...providers); 9 | } -------------------------------------------------------------------------------- /packages/pigly/src/providers/to.ts: -------------------------------------------------------------------------------- 1 | import { IProvider } from "../_provider"; 2 | import { isService, Service } from "../_service"; 3 | 4 | /** REQUIRES TRANSFORMER - create a provider that resolves to another type */ 5 | export function to(): IProvider 6 | /** create a provider that resolves to another symbol */ 7 | export function to(service: Service): IProvider 8 | export function to(service?: Service): IProvider { 9 | if (!isService(service)) throw Error('called "to" without a service symbol'); 10 | return (ctx) => ctx.resolve({service}).first(); 11 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "Launch Webpack", 8 | "program": "${workspaceFolder}/node_modules/webpack/bin/webpack.js", 9 | "args": [ 10 | "--config", 11 | "${workspaceFolder}/packages/pigly-example/webpack.config.js" 12 | ], 13 | "cwd": "${workspaceFolder}/packages/pigly-example", 14 | "outFiles": [ 15 | "${workspaceFolder}/**/dist/**/*.js" 16 | ], 17 | "sourceMaps": true 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /packages/pigly-transformer/test/scoping.spec.ts: -------------------------------------------------------------------------------- 1 | import { IBinding, Scope, IProvider, Kernel, Service, toConst, toSelf, toClass } from "pigly"; 2 | import { expect } from "chai"; 3 | 4 | 5 | describe("Scoping", () => { 6 | it("can bind singleton", () => { 7 | const kernel = new Kernel(); 8 | 9 | interface IFoo {} 10 | class Foo implements IFoo {} 11 | 12 | kernel.bind(toClass(Foo), Scope.Singleton); 13 | 14 | let a = kernel.get(); 15 | let b = kernel.get(); 16 | 17 | expect(a === b, "same instance").to.be.true; 18 | }) 19 | }); 20 | -------------------------------------------------------------------------------- /packages/pigly/src/_kernel.ts: -------------------------------------------------------------------------------- 1 | import { IBinding } from './_binding'; 2 | import { IProvider } from "./_provider"; 3 | import { IReadOnlyKernel } from "./_read-only-kernel"; 4 | import { Service } from "./_service"; 5 | import { Scope } from "./_scope"; 6 | 7 | export interface IKernel extends IReadOnlyKernel { 8 | /**Bind a Symbol to a provider */ 9 | bind(service: Service, provider: IProvider, scope?: Scope): IBinding; 10 | /**Bind interface T to a provider - note requires compile-time @pigly/transformer */ 11 | bind(provider: IProvider, scope?: Scope): IBinding; 12 | } -------------------------------------------------------------------------------- /packages/pigly-transformer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "lib": [ 7 | "es2015", 8 | "dom", 9 | ], 10 | "declaration": true, 11 | "esModuleInterop": true, 12 | "sourceMap": false, 13 | "outDir": "./dist", 14 | "rootDir": "src/", 15 | "plugins": [ 16 | { 17 | "transform": "@pigly/transformer" 18 | }, 19 | ] 20 | }, 21 | "include": [ 22 | "src/" 23 | ], 24 | "exclude": [ 25 | "node_modules", 26 | ], 27 | } -------------------------------------------------------------------------------- /packages/pigly/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './predicates'; 2 | export * from './providers'; 3 | 4 | export * from './_binding'; 5 | export * from './_constructor'; 6 | export * from './_context'; 7 | export * from './_kernel'; 8 | export * from './_provider'; 9 | export * from './_read-only-kernel'; 10 | export * from './_request'; 11 | export * from './_resolution'; 12 | export * from './_resolver-root'; 13 | export * from './_service'; 14 | export * from './_scope'; 15 | 16 | export * from './declarations'; 17 | 18 | export * from './kernel'; 19 | export * from './errors'; 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/pigly-transformer/test/has-ancestor.spec.ts: -------------------------------------------------------------------------------- 1 | import { Kernel, when, hasAncestor, toConst, to } from "pigly"; 2 | import { expect } from "chai"; 3 | 4 | describe("hasAncestor", () => { 5 | it("can inject interface if ancestor matches", () => { 6 | const kernel = new Kernel(); 7 | 8 | interface A { } 9 | interface B { } 10 | interface C { } 11 | 12 | kernel.bind(when(hasAncestor(), toConst("foo"))); 13 | kernel.bind(to()); 14 | kernel.bind(to()); 15 | 16 | let c = kernel.get(); 17 | 18 | expect(c).to.be.eq("foo"); 19 | }) 20 | }) -------------------------------------------------------------------------------- /packages/pigly/src/providers/to-all.ts: -------------------------------------------------------------------------------- 1 | import { IProvider } from "../_provider"; 2 | import { Service, isService } from "../_service"; 3 | 4 | /** REQUIRES TRANSFORMER - create a provider that resolves all bindings to a type */ 5 | export function toAll(): IProvider 6 | /** create a provider that resolves all bindings to a symbol */ 7 | export function toAll(service: Service): IProvider 8 | export function toAll(service?: Service): IProvider { 9 | if (!isService(service)) throw Error('called "toAll" without a service symbol'); 10 | return (ctx) => ctx.resolve({ service }).toArray(); 11 | } -------------------------------------------------------------------------------- /packages/pigly/src/predicates/has-ancestor.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from "../_context"; 2 | 3 | export function hasAncestor(symbol: symbol): (ctx: IContext) => boolean 4 | export function hasAncestor(): never; 5 | export function hasAncestor

(): (ctx: IContext) => boolean; 6 | export function hasAncestor(symbol?: symbol): (ctx: IContext) => boolean { 7 | return (ctx: IContext) => { 8 | let _parent = ctx.parent; 9 | while (_parent != undefined) { 10 | if(_parent.service == symbol){ 11 | return true; 12 | } 13 | _parent = _parent.parent; 14 | }; 15 | return false; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/pigly/src/providers/to-class.ts: -------------------------------------------------------------------------------- 1 | import { IProvider, ProviderWrap } from "../_provider"; 2 | import { IContext } from "../_context"; 3 | import { Constructor } from "../_constructor"; 4 | 5 | /** manually bind a class constructor to argument providers */ 6 | export function toClass(ctor: C, ...providers: ProviderWrap>): IProvider> { 7 | if (ctor === undefined) throw Error('called "toClass" without a Constructor argument'); 8 | 9 | return (ctx: IContext) => { 10 | return new ctor(...providers.map(provider => provider ? provider(ctx) : undefined)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/pigly-transformer/test/named.spec.ts: -------------------------------------------------------------------------------- 1 | import { Kernel, when, toConst, toSelf, named } from "pigly"; 2 | import { expect } from "chai"; 3 | 4 | describe("named", () => { 5 | xit("can conditionally inject service if context name matches", () => { 6 | const kernel = new Kernel(); 7 | 8 | class A { constructor(public c: C){}} 9 | class B { constructor(public b: C){}} 10 | 11 | interface C { message: string } 12 | 13 | kernel.bind(toSelf(A)); 14 | kernel.bind(toSelf(B)); 15 | 16 | kernel.bind(when(named("c"), toConst({message: "hello"}))); 17 | 18 | let a = kernel.get(); 19 | 20 | expect(a.c.message).to.be.eq("hello"); 21 | expect(()=>{ 22 | kernel.get(); 23 | }).throws("could not resolve Symbol(C) > Symbol(B)"); 24 | }) 25 | }) -------------------------------------------------------------------------------- /packages/pigly-transformer/test/injected-into.spec.ts: -------------------------------------------------------------------------------- 1 | import { Kernel, when, toConst, to, toSelf, injectedInto } from "pigly"; 2 | import { expect } from "chai"; 3 | 4 | describe("injectedInto", () => { 5 | it("can conditionally inject interface", () => { 6 | const kernel = new Kernel(); 7 | 8 | class A { constructor(public c: C){}} 9 | class B { constructor(public c: C){}} 10 | interface C { message: string } 11 | 12 | kernel.bind(toSelf(A)); 13 | kernel.bind(toSelf(B)); 14 | kernel.bind(when(injectedInto(),toConst({message: "hello"}))); 15 | kernel.bind(when(injectedInto(),toConst({message: "world"}))); 16 | 17 | let a = kernel.get(); 18 | let b = kernel.get(); 19 | 20 | expect(a.c.message).to.be.eq("hello"); 21 | expect(b.c.message).to.be.eq("world"); 22 | }) 23 | }) -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/packages/pigly" 5 | schedule: 6 | interval: monthly 7 | open-pull-requests-limit: 10 8 | versioning-strategy: widen 9 | ignore: 10 | - dependency-name: highlight.js 11 | versions: 12 | - 10.5.0 13 | - 10.6.0 14 | - dependency-name: ini 15 | versions: 16 | - 1.3.8 17 | - dependency-name: yargs-parser 18 | versions: 19 | - 13.1.2 20 | - dependency-name: y18n 21 | versions: 22 | - 4.0.0 23 | - dependency-name: ts-auto-mock 24 | versions: 25 | - 3.0.0 26 | - 3.1.0 27 | - 3.1.1 28 | - dependency-name: "@types/node" 29 | versions: 30 | - 14.14.17 31 | - 14.14.22 32 | - dependency-name: "@types/sinon" 33 | versions: 34 | - 9.0.10 35 | - dependency-name: "@types/mocha" 36 | versions: 37 | - 8.2.0 38 | -------------------------------------------------------------------------------- /packages/pigly-transformer/test/symbol-for.spec.ts: -------------------------------------------------------------------------------- 1 | import { SymbolFor } from "pigly"; 2 | import { expect } from "chai"; 3 | 4 | 5 | describe("SymbolFor", () => { 6 | it("can create symbol for generic interface", ()=>{ 7 | interface A{} 8 | interface B{} 9 | interface C{} 10 | 11 | let AB = Symbol.for("A"); 12 | let AC = Symbol.for("A"); 13 | 14 | let ab = SymbolFor>(); 15 | let ac = SymbolFor>(); 16 | 17 | expect(ab).to.be.equal(AB); 18 | expect(ac).to.be.equal(AC); 19 | }); 20 | 21 | it("can create symbol for generic interface, regardless of formatting", ()=>{ 22 | interface A{} 23 | interface B{} 24 | interface C{} 25 | 26 | let AB = Symbol.for("A"); 27 | let AC = Symbol.for("A"); 28 | 29 | let ab = SymbolFor>(); 30 | let ac = SymbolFor>(); 31 | 32 | expect(ab).to.be.equal(AB); 33 | expect(ac).to.be.equal(AC); 34 | }); 35 | }) -------------------------------------------------------------------------------- /packages/pigly/src/_context.ts: -------------------------------------------------------------------------------- 1 | import { IResolverRoot } from "./_resolver-root"; 2 | import { IReadOnlyKernel } from "./_read-only-kernel"; 3 | import { Service } from "./_service"; 4 | import { Scope } from './_scope'; 5 | import { IBinding } from "./_binding"; 6 | import { IRequest } from "./_request"; 7 | 8 | export interface IContext extends IResolverRoot { 9 | /** the kernel being used */ 10 | kernel: IReadOnlyKernel; 11 | /** origin request */ 12 | request: IRequest 13 | /** the service being requested */ 14 | service: Service; 15 | /** the target site name (field or argument) */ 16 | target?: string; 17 | /** parent context */ 18 | parent?: IContext; 19 | /** the binding chosen */ 20 | binding: IBinding; 21 | /** post-constructor clean-up - called after root request instantiated */ 22 | finally?: (instance: any) => void; 23 | /** creates a child context - typically for sub-field resolution */ 24 | createContext(ctx?: Partial): IContext; 25 | } 26 | -------------------------------------------------------------------------------- /packages/pigly/src/_scope.ts: -------------------------------------------------------------------------------- 1 | import { IBinding } from "./_binding"; 2 | import { IContext } from "./_context"; 3 | import { IKernel } from "./_kernel"; 4 | import { IReadOnlyKernel } from "./_read-only-kernel"; 5 | 6 | export interface Scope { 7 | getCache(ctx: IContext): WeakMap; 8 | } 9 | 10 | export class SingletonScope implements Scope { 11 | private _cache: WeakMap> = new WeakMap() 12 | getCache(ctx: IContext): WeakMap{ 13 | const kernel = ctx.kernel; 14 | if(this._cache.has(kernel) == false){ 15 | this._cache.set(kernel, new WeakMap()); 16 | } 17 | return this._cache.get(kernel); 18 | } 19 | } 20 | 21 | export class TransientScope implements Scope { 22 | getCache(ctx: IContext): WeakMap{ 23 | return new WeakMap(); 24 | } 25 | } 26 | 27 | export namespace Scope { 28 | export const Singleton: Scope = new SingletonScope(); 29 | export const Transient: Scope = new TransientScope(); 30 | } 31 | -------------------------------------------------------------------------------- /packages/pigly/src/providers/defer.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from "../_context"; 2 | import { IProvider } from "../_provider"; 3 | import { Service } from "../_service"; 4 | import { IRequest } from "../_request"; 5 | 6 | let __setImmediate; 7 | 8 | if (global && global.setImmediate) { 9 | __setImmediate = global.setImmediate; 10 | } else { 11 | __setImmediate = function (cb, ...args) { 12 | return setTimeout(cb, 0, ...args); 13 | }; 14 | } 15 | 16 | export type DeferFieldProviders = { 17 | [P in keyof T]?: IProvider 18 | } 19 | 20 | /** 21 | * 22 | * NOT RECOMMENDED: still possible to stack-overflow on non-singleton cyclic dependencies */ 23 | export function defer(provider: IProvider, inject: DeferFieldProviders) { 24 | return (ctx: IContext) => { 25 | let kernel = ctx.kernel; 26 | let resolved = provider(ctx); 27 | __setImmediate(() => { 28 | for (let [key, provider] of Object.entries(inject)) { 29 | let _ctx: IContext = ctx.createContext({target: key}); 30 | resolved[key] = (provider as any)(_ctx); 31 | } 32 | }) 33 | return resolved; 34 | }; 35 | } -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Meirion Hughes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/pigly/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Meirion Hughes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/pigly-transformer/LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Meirion Hughes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:14 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: yarn install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # build 37 | - run: yarn build 38 | 39 | # run tests! 40 | - run: yarn test 41 | 42 | # run tests! 43 | - run: yarn coverage 44 | -------------------------------------------------------------------------------- /packages/pigly-transformer/README.md: -------------------------------------------------------------------------------- 1 | # Pigly/transformer 2 | ![CircleCI](https://img.shields.io/circleci/build/github/pigly-di/pigly?token=abc123def456) ![npm](https://img.shields.io/npm/v/pigly) ![npm](https://img.shields.io/npm/dm/pigly) ![Codecov](https://img.shields.io/codecov/c/gh/pigly-di/pigly) 3 | 4 | the typescript plugin to help emit type symbols for the pigly kernel. 5 | 6 | ![alt](https://avatars0.githubusercontent.com/u/50213493?s=400&u=65942b405a979397a2c358366db85c3d06f521f5&v=4) 7 | 8 | ## Usage 9 | 10 | you must use a custom typescript compiler that facilitates using a typescript transformer. see 11 | https://github.com/cevek/ttypescript 12 | 13 | with `@pigly/transformer` transformer active, in your code: 14 | 15 | ``` 16 | import { SymbolFor } from 'pigly'; 17 | 18 | let $IFoo = SymbolFor() 19 | ``` 20 | ...which will get compiled such that `SymbolFor` will be replaced with `symbol.for("...")` 21 | 22 | currently the transformer is just looking for a method `SymbolFor()` and replaces it with the typescript-id for `T`. Any changes to how this works will result in a major version bump of this package. 23 | 24 | ## License 25 | MIT 26 | 27 | ## Credits 28 | 29 | "pig" licensed under CC from Noun Project, Created by habione 404, FR 30 | 31 | @pigly/transformer was derived from https://github.com/YePpHa/ts-di-transformer (MIT) -------------------------------------------------------------------------------- /packages/pigly-transformer/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Coverage", 11 | "skipFiles": [ 12 | "/**" 13 | ], 14 | "program": "${workspaceFolder}\\node_modules\\.bin\\nyc.cmd", 15 | "args": [ 16 | "-x", 17 | "test", 18 | "--reporter=lcov", 19 | "--reporter=text", 20 | "node", 21 | "--inspect-brk", 22 | "${workspaceFolder}/node_modules/.bin/mocha", 23 | "test", 24 | "--recursive", 25 | "--timeout=300000" 26 | ] 27 | }, 28 | { // https://code.visualstudio.com/Docs/editor/debugging#_launch-versus-attach-configurations 29 | "type": "node", 30 | "name": "AttachMocha", 31 | "request": "attach", 32 | "port": 9229 33 | } 34 | ], 35 | // https://code.visualstudio.com/Docs/editor/debugging#_compound-launch-configurations 36 | "compounds": [ 37 | { 38 | "name": "NYC/Mocha 2", 39 | "configurations": [ 40 | "AttachMocha", 41 | "Coverage" 42 | ] 43 | } 44 | ] 45 | } -------------------------------------------------------------------------------- /packages/pigly-transformer/test/to-self.spec.ts: -------------------------------------------------------------------------------- 1 | import { Kernel, when, toConst, to, toSelf, injectedInto } from "pigly"; 2 | import { expect } from "chai"; 3 | 4 | describe("toSelf", () => { 5 | it("can infer constructor interface argument and inject", () => { 6 | const kernel = new Kernel(); 7 | 8 | class A { constructor(public b: B) { } } 9 | interface B { message: string } 10 | 11 | kernel.bind(toSelf(A)); 12 | kernel.bind(toConst({ message: "hello" })); 13 | 14 | let a = kernel.get(); 15 | 16 | expect(a.b.message).to.be.eq("hello"); 17 | }) 18 | 19 | it("can infer constructor interface array-argument and inject", () => { 20 | const kernel = new Kernel(); 21 | 22 | class A { constructor(public b: B[]) { } } 23 | interface B { message: string } 24 | 25 | kernel.bind(toSelf(A)); 26 | kernel.bind(toConst({ message: "hello" })); 27 | kernel.bind(toConst({ message: "world" })); 28 | 29 | let a = kernel.get(); 30 | 31 | expect(a.b).to.be.eql([{ message: "hello" }, { message: "world" }]); 32 | }) 33 | it("can infer constructor multiple arguments and inject", () => { 34 | const kernel = new Kernel(); 35 | 36 | class A { constructor(public b: B, public c: C, public d: D) { } } 37 | interface B { message: string } 38 | interface C { message: string } 39 | interface D { message: string } 40 | 41 | kernel.bind(toSelf(A)); 42 | kernel.bind(toConst({ message: "B" })); 43 | kernel.bind(toConst({ message: "C" })); 44 | kernel.bind(toConst({ message: "D" })); 45 | 46 | let a = kernel.get(); 47 | 48 | expect(a.b.message).to.be.eq("B"); 49 | expect(a.c.message).to.be.eq("C"); 50 | expect(a.d.message).to.be.eq("D"); 51 | }) 52 | }) -------------------------------------------------------------------------------- /packages/pigly/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pigly", 3 | "version": "2.0.0-alpha.8", 4 | "main": "dist/cjs/index.js", 5 | "typings": "dist/cjs/index.d.ts", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/pigly-di/pigly.git" 10 | }, 11 | "author": { 12 | "name": "Meirion Hughes", 13 | "url": "https://github.com/MeirionHughes" 14 | }, 15 | "devDependencies": { 16 | "@types/chai": "^4.2.16", 17 | "@types/mocha": "^8.2.2", 18 | "@types/node": "^18.0.0", 19 | "chai": "^4.3.4", 20 | "codecov": "^3.8.1", 21 | "mocha": "^8.3.2", 22 | "nyc": "^15.1.0", 23 | "source-map-support": "^0.5.19", 24 | "ts-node": "^8.1.0", 25 | "typescript": "^5.2.2" 26 | }, 27 | "scripts": { 28 | "build": "npm run build:cjs", 29 | "build:esm": "tsc --build tsconfig.build.esm.json", 30 | "build:cjs": "tsc --build tsconfig.build.cjs.json", 31 | "build:pack": "node ./gen-package.mjs", 32 | "test": "nyc mocha test/**/*.spec.ts", 33 | "coverage": "codecov --flags pigly" 34 | }, 35 | "nyc": { 36 | "exclude": [ 37 | "node_modules/", 38 | "test/" 39 | ], 40 | "extension": [ 41 | ".ts" 42 | ], 43 | "require": [ 44 | "source-map-support/register", 45 | "ts-node/register" 46 | ], 47 | "reporter": [ 48 | "text-summary", 49 | "html", 50 | "lcov" 51 | ], 52 | "sourceMap": true, 53 | "instrument": true 54 | }, 55 | "files": [ 56 | "/dist/*", 57 | "README.md" 58 | ], 59 | "keywords": [ 60 | "dependency-injection", 61 | "dependency", 62 | "injection", 63 | "DI", 64 | "IOC", 65 | "container", 66 | "typescript", 67 | "plugin" 68 | ], 69 | "gitHead": "e2d738ed0497a0e92c74cb5b609b08c187f2b6be" 70 | } 71 | -------------------------------------------------------------------------------- /packages/pigly-transformer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@pigly/transformer", 3 | "version": "2.0.0-alpha.9", 4 | "main": "./dist/cjs/index.js", 5 | "typings": "./dist/cjs/index.d.ts", 6 | "license": "MIT", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/pigly-di/pigly.git" 10 | }, 11 | "author": { 12 | "name": "Meirion Hughes", 13 | "url": "https://github.com/MeirionHughes" 14 | }, 15 | "devDependencies": { 16 | "@types/chai": "^4.2.16", 17 | "@types/mocha": "^8.2.2", 18 | "@types/node": "^18.0.0", 19 | "chai": "^4.3.4", 20 | "codecov": "^3.8.1", 21 | "cross-env": "^7.0.0", 22 | "mocha": "^10", 23 | "nyc": "^15.1.0", 24 | "pigly": "^2.0.0-alpha.8", 25 | "source-map-support": "^0.5.19", 26 | "ts-patch": "^3.0.1", 27 | "typescript": "^5.2.2" 28 | }, 29 | "scripts": { 30 | "build": "npm run build:cjs", 31 | "build:cjs": "tsc --build tsconfig.build.cjs.json", 32 | "build:pack": "node ./gen-package.mjs", 33 | "test": "cross-env TS_NODE_COMPILER=ts-patch/compiler nyc mocha test/**/*.spec.ts", 34 | "example": "ts-node --compiler ts-patch/compiler example.ts", 35 | "coverage": "codecov --flags transformer" 36 | }, 37 | "nyc": { 38 | "exclude": [ 39 | "node_modules/", 40 | "test/" 41 | ], 42 | "extension": [ 43 | ".ts" 44 | ], 45 | "require": [ 46 | "source-map-support/register", 47 | "ts-node/register" 48 | ], 49 | "reporter": [ 50 | "text-summary", 51 | "html", 52 | "lcov" 53 | ], 54 | "sourceMap": true, 55 | "instrument": true 56 | }, 57 | "peerDependencies": { 58 | "typescript": ">=5.0.0" 59 | }, 60 | "files": [ 61 | "/dist/*", 62 | "README.md" 63 | ], 64 | "keywords": [ 65 | "dependency-injection", 66 | "dependency", 67 | "injection", 68 | "DI", 69 | "IOC", 70 | "container", 71 | "typescript", 72 | "plugin" 73 | ], 74 | "gitHead": "e2d738ed0497a0e92c74cb5b609b08c187f2b6be" 75 | } 76 | -------------------------------------------------------------------------------- /packages/pigly/test/scoping.spec.ts: -------------------------------------------------------------------------------- 1 | import { Kernel, toConst, toClass, toFunc, to, IContext, when, defer, hasAncestor, IProvider, IBinding } from "../src"; 2 | import { expect } from 'chai'; 3 | import { Scope } from '../src/_scope'; 4 | import { nextTick } from 'process'; 5 | 6 | function scope(provider: IProvider): IProvider { 7 | return function (ctx: IContext) { 8 | return provider(ctx); 9 | } 10 | } 11 | 12 | describe("Kernel Scoping", () => { 13 | it("can bind to scope and instances within scope are same", (done) => { 14 | const kernel = new Kernel(); 15 | 16 | const $Request = Symbol.for("Req"); 17 | const RequestScope = new class implements Scope { 18 | private _cache: WeakMap> = new WeakMap(); 19 | getCache(ctx: IContext): WeakMap { 20 | let _ctx = ctx; 21 | let _current = ctx; 22 | /** 23 | * locate the shallowest context for the request service 24 | **/ 25 | while (_current != undefined) { 26 | if (_current.service === $Request) { 27 | _ctx = _current; 28 | } 29 | _current = _current.parent; 30 | } 31 | if (_ctx == undefined || _ctx.service !== $Request) 32 | throw Error("Did not find Request in dependency hierarchy"); 33 | if (this._cache.has(_ctx) == false) { 34 | this._cache.set(_ctx, new WeakMap()); 35 | } 36 | return this._cache.get(_ctx); 37 | } 38 | } 39 | 40 | const $A = Symbol.for("A"); 41 | const $B = Symbol.for("B"); 42 | const $X = Symbol.for("X"); 43 | 44 | class Req { constructor(public a: A, public b: B) { } } 45 | class A { constructor(public x: X) { } } 46 | class B { constructor(public x: X) { } } 47 | class X { public req: Req; constructor() { } } 48 | 49 | /** bind request service within its own scope */ 50 | kernel.bind($Request, toClass(Req, to($A), to($B)), RequestScope); 51 | kernel.bind($X, defer(toClass(X), { req: to($Request) }), RequestScope); 52 | kernel.bind($A, toClass(A, to($X))); 53 | kernel.bind($B, toClass(B, to($X))); 54 | 55 | let req1 = kernel.get($Request); 56 | let req2 = kernel.get($Request); 57 | 58 | setImmediate(() => { 59 | //requests are different... 60 | expect(req1).not.eq(req2); 61 | //a and b instances are different... 62 | expect(req1.a).not.eq(req2.a); 63 | expect(req1.b).not.eq(req2.b); 64 | // x between requests is different... 65 | expect(req1.a.x).not.eq(req2.a.x); 66 | expect(req1.b.x).not.eq(req2.b.x); 67 | // x within requests is same... 68 | expect(req1.a.x).eq(req1.b.x); 69 | expect(req2.a.x).eq(req2.b.x); 70 | //deferred cyclic injection is same 71 | expect(req1).eq(req1.a.x.req); 72 | done(); 73 | }); 74 | }) 75 | }); -------------------------------------------------------------------------------- /packages/pigly-transformer/test/kernel.spec.ts: -------------------------------------------------------------------------------- 1 | import { IBinding, IProvider, Kernel, Service, toConst, toSelf } from "pigly"; 2 | import { expect } from "chai"; 3 | 4 | import { IFoo } from './_foo'; 5 | 6 | import { IFoo as IBar } from './_foo'; 7 | 8 | describe("kernel", () => { 9 | it("can bind interface", () => { 10 | const kernel = new Kernel(); 11 | 12 | class A { constructor(public b: B) { } } 13 | interface B { message: string }; 14 | 15 | kernel.bind(toSelf(A)); 16 | kernel.bind(toConst({ message: "hello" }));; 17 | 18 | let a = kernel.get(); 19 | 20 | expect(a.b.message).to.be.eq("hello"); 21 | }) 22 | 23 | it("can bind generic interface", () => { 24 | const kernel = new Kernel(); 25 | 26 | interface A { } 27 | interface B { } 28 | interface C { } 29 | 30 | kernel.bind>(toConst("AB")); 31 | kernel.bind>(toConst("AC")); 32 | 33 | let ab = kernel.get>(); 34 | let ac = kernel.get>(); 35 | 36 | expect(ab).to.be.eq("AB"); 37 | expect(ac).to.be.eq("AC"); 38 | }) 39 | it("can bind generic interface, regardless of formatting", () => { 40 | const kernel = new Kernel(); 41 | 42 | interface A { } 43 | interface B { } 44 | interface C { } 45 | 46 | kernel.bind>(toConst("AB")); 47 | kernel.bind>(toConst("AC")); 48 | 49 | let ab = kernel.get>(); 50 | let ac = kernel.get>(); 51 | 52 | expect(ab).to.be.eq("AB"); 53 | expect(ac).to.be.eq("AC"); 54 | }) 55 | 56 | it("can bind imported interface", () => { 57 | const kernel = new Kernel(); 58 | 59 | class A { constructor(public b: IFoo) { } } 60 | 61 | kernel.bind(toSelf(A)); 62 | kernel.bind(toConst({ message: "hello" }));; 63 | 64 | let a = kernel.get(); 65 | 66 | expect(a.b.message).to.be.eq("hello"); 67 | }) 68 | 69 | xit("can bind aliased interface", () => { 70 | const kernel = new Kernel(); 71 | 72 | class A { constructor(public b: IBar) { } } 73 | 74 | kernel.bind(toSelf(A)); 75 | kernel.bind(toConst({ message: "hello" }));; 76 | 77 | let a = kernel.get(); 78 | 79 | expect(a.b.message).to.be.eq("hello"); 80 | }) 81 | 82 | it("can bind primitive: string", () => { 83 | const kernel = new Kernel(); 84 | 85 | class A { constructor(public b: string) { } } 86 | 87 | kernel.bind(toSelf(A)); 88 | kernel.bind(toConst("hello")); 89 | 90 | let a = kernel.get(); 91 | 92 | expect(a.b).to.be.eq("hello"); 93 | }) 94 | 95 | it("can bind primitive: number", () => { 96 | const kernel = new Kernel(); 97 | 98 | class A { constructor(public b: number) { } } 99 | 100 | kernel.bind(toSelf(A)); 101 | kernel.bind(toConst(10)); 102 | 103 | let a = kernel.get(); 104 | 105 | expect(a.b).to.be.eq(10); 106 | }) 107 | 108 | it("can bind typeof types", () => { 109 | const kernel = new Kernel(); 110 | 111 | class A { constructor(public b: string) { } }; 112 | const a: A = new A("moo"); 113 | 114 | kernel.bind(toConst(a)); 115 | 116 | expect(a.b).to.be.eq("moo"); 117 | }) 118 | 119 | // it("can be derived", () => { 120 | // class MyKernel extends Kernel { 121 | // bind(...args: any[]): IBinding { 122 | // const service: Service = args[0]; 123 | // const provider: IProvider = args[1]; 124 | // const scope: Scope = args[2] ?? Scope.Transient; { 125 | // if (typeof service == "symbol") { 126 | // let wrapped = function (ctx) { 127 | // let ori = (provider as IProvider)(ctx); 128 | // if (typeof ori == "number") { 129 | // return ori * 2; 130 | // } 131 | // return ori; 132 | // } 133 | // return super.bind(service, wrapped); 134 | // } else throw Error("must use transformer"); 135 | // } 136 | // } 137 | // } 138 | // const kernel = new MyKernel(); 139 | 140 | // class A { constructor(public b: number) { } } 141 | 142 | // kernel.bind(toSelf(A)); 143 | // kernel.bind(toConst(10)); 144 | 145 | // let a = kernel.get(); 146 | 147 | // expect(a.b).to.be.eq(20); 148 | // }) 149 | }) -------------------------------------------------------------------------------- /packages/pigly/src/kernel.ts: -------------------------------------------------------------------------------- 1 | import { IContext } from "./_context"; 2 | import { IProvider, isProvider } from "./_provider"; 3 | import { IBinding } from "./_binding"; 4 | import { IKernel } from "./_kernel"; 5 | import { Service, isService } from "./_service"; 6 | import { IRequest } from "./_request"; 7 | import { IResolution } from "./_resolution"; 8 | import { Scope } from './_scope'; 9 | import { ResolveError } from "./errors"; 10 | 11 | function getCallSite(stack: string): string { 12 | const logLines = stack.split('\n'); 13 | let caller = logLines[2]; 14 | if (caller) { return caller.trim(); } 15 | return null; 16 | } 17 | 18 | 19 | export class Kernel implements IKernel { 20 | private _bindings = new Map(); 21 | 22 | constructor(private opts = { verbose: false }) { 23 | 24 | } 25 | 26 | /**Bind a Symbol to a provider */ 27 | bind(service: Service, provider: IProvider, scope?: Scope): IBinding; 28 | /**Bind interface T to a provider - note requires compile-time @pigly/transformer */ 29 | bind(provider: IProvider, scope?: Scope): IBinding; 30 | bind(...args: any[]): IBinding { 31 | const service: Service = args[0]; 32 | const provider: IProvider = args[1]; 33 | const scope: Scope = args[2] ?? Scope.Transient; 34 | 35 | if (isService(service) === false) { 36 | throw Error("first argument must be a service type"); 37 | } 38 | if (isProvider(provider) === false) { 39 | throw Error("second argument must be a provider function"); 40 | } 41 | 42 | const site = getCallSite((new Error()).stack); 43 | 44 | let bindings: IBinding[] = []; 45 | 46 | if (this._bindings.has(service)) { 47 | bindings = this._bindings.get(service); 48 | } 49 | 50 | let binding = { provider, site, scope }; 51 | 52 | bindings.push(binding); 53 | 54 | this._bindings.set(service, bindings); 55 | 56 | return binding; 57 | } 58 | 59 | resolve(request: IRequest): IResolution { 60 | const self = this; 61 | return { 62 | [Symbol.iterator]() { return self._resolve(request) }, 63 | toArray: function () { 64 | return Array.from(self._resolve(request)) 65 | }, 66 | first: function () { 67 | return self._resolve(request).next().value; 68 | } 69 | } 70 | } 71 | 72 | /** Resolve the request to bindings and return resolution results */ 73 | private *_resolve(request: IRequest) { 74 | const service = request.service; 75 | const bindings = this._bindings.get(service); 76 | 77 | let wasResolved = false; 78 | 79 | if (bindings != undefined) { 80 | for (let binding of bindings) { 81 | const resolve = function (this: IContext, req: IRequest) { 82 | return this.kernel.resolve(Object.assign({ parent: this }, req)); 83 | }; 84 | const ctx: IContext = { 85 | kernel: this, 86 | request, 87 | parent: request.parent, 88 | service: request.service, 89 | binding: binding, 90 | resolve: null, 91 | createContext: function (_ctx: Partial) { 92 | let result = Object.assign({}, ctx, _ctx, { parent: ctx }); 93 | result.resolve = resolve.bind(result); 94 | return result 95 | } 96 | } 97 | ctx.resolve = resolve.bind(ctx); 98 | 99 | const scope = binding.scope; 100 | const cache = scope.getCache(ctx); 101 | 102 | if (cache.has(binding)) { 103 | yield cache.get(binding); 104 | continue; 105 | } else { 106 | this._checkCyclicDependency(request); 107 | 108 | let resolved = binding.provider(ctx); 109 | 110 | if (resolved !== undefined) { 111 | wasResolved = true; 112 | cache.set(binding, resolved); 113 | yield resolved; 114 | } 115 | } 116 | } 117 | } 118 | 119 | if (wasResolved == false) { 120 | let history = []; 121 | let _parent = request.parent; 122 | while (_parent != undefined) { 123 | history.push(_parent.service); 124 | _parent = _parent.parent; 125 | }; 126 | 127 | let msg = history.reduceRight((p, n) => p += " > " + n.toString(), "") 128 | 129 | throw new ResolveError(request.service, msg); 130 | } 131 | } 132 | private _checkCyclicDependency(request: IRequest) { 133 | let target = request.service; 134 | let _parent = request.parent?.request; 135 | while (_parent != undefined) { 136 | if (_parent.service == target) { 137 | reportCyclicError(request) 138 | } 139 | _parent = _parent.parent; 140 | }; 141 | } 142 | 143 | get(): T; 144 | get(service: Service): T; 145 | get(service?: Service): any | Array { 146 | if (isService(service) == false) throw Error('called "get" without a service'); 147 | return this.resolve({ service }).first(); 148 | } 149 | getAll(): T; 150 | getAll(service: Service): T; 151 | getAll(service?: Service): any | Array { 152 | if (isService(service) == false) throw Error('called "get" without a service'); 153 | return this.resolve({ service }).toArray(); 154 | } 155 | } 156 | 157 | function reportCyclicError(request: IRequest) { 158 | let history: IContext[] = []; 159 | let _parent = request.parent; 160 | while (_parent != undefined) { 161 | history.push(_parent); 162 | _parent = _parent.parent; 163 | }; 164 | 165 | let msg = "Pigly Cyclic Dependency Found \n"; 166 | 167 | msg += " Requested: " + request.service.toString() + (request.target?`(${request.target})`: "") + "\n"; 168 | 169 | for (let ctx of history) { 170 | msg += " Into: " + ctx.service.toString() + (ctx.target?`(${ctx.target})`: "") + "\n"; 171 | msg += " Config: " + ctx.binding.site + "\n"; 172 | } 173 | 174 | throw Error(msg); 175 | } -------------------------------------------------------------------------------- /packages/pigly/README.md: -------------------------------------------------------------------------------- 1 | # Pigly 2 | unobtrusive, manually configured, dependency-injection for javascript/typescript 3 | 4 | ![alt](https://avatars0.githubusercontent.com/u/50213493?s=400&u=65942b405a979397a2c358366db85c3d06f521f5&v=4) 5 | 6 | ⚠️⚠️ WARNING: experimental - lock to minor versions ⚠️⚠️ 7 | 8 | ![CircleCI](https://img.shields.io/circleci/build/github/pigly-di/pigly?token=abc123def456) ![npm](https://img.shields.io/npm/v/pigly) ![npm](https://img.shields.io/npm/dm/pigly) ![Codecov](https://img.shields.io/codecov/c/gh/pigly-di/pigly) 9 | 10 | 11 | ## Philosophy 12 | 13 | > don't pollute the code with DI concerns 14 | 15 | pigly is a simple helper to manually configure a DI container to bind symbols to providers. It explicitly avoids decorators, or any other changes to existing code, and on its own doesn't require any other dependency / compilation-step to work. However, when combined with the typescript transformer `@pigly/transformer` we can reduce the amount of boiler-plate and simply describe the bindings as: 16 | 17 | ```ts 18 | let kernel = new Kernel(); 19 | 20 | kernel.bind(toSelf(Foo)); 21 | kernel.bind(toSelf(Bar)); 22 | 23 | kernel.bind(to()); 24 | kernel.bind(to()); 25 | 26 | let foo = kernel.get(); 27 | ``` 28 | 29 | ## Planned features 30 | 31 | * Scoping 32 | * Better inferring of constructors 33 | 34 | ## Native Usage 35 | 36 | native usage relates to using this package directly without any typescript transformer. 37 | 38 | Its pretty simple: create a kernel, create symbol-to-provider bindings, then get the resolved result with `get(symbol)` 39 | 40 | ```ts 41 | import { Kernel } from 'pigly'; 42 | 43 | let kernel = new Kernel(); 44 | 45 | kernel.bind(Symbol.for("Foo"), (ctx)=>{ return "foo" }); 46 | 47 | let foo = kernel.get(Symbol.for("Foo")); 48 | ``` 49 | 50 | ### .bind(symbol, provider) 51 | 52 | bind links a specific symbol to a provider of the form (context)=>value; 53 | 54 | ```ts 55 | kernel.bind(A, _=>{ return "hello world" }) 56 | ``` 57 | 58 | ### .get(symbol) 59 | 60 | resolve all the bindings for a symbol and return the first one 61 | 62 | ```ts 63 | const A = Symbol.for("A") 64 | 65 | kernel.bind(A, _=> "hello"); 66 | kernel.bind(A, _=> " world"); 67 | 68 | let result = kernel.get(A); // "hello"; 69 | ``` 70 | 71 | ### .getAll(symbol) 72 | 73 | resolve all the bindings for a symbol and return all of the results. 74 | 75 | ```ts 76 | const A = Symbol.for("A") 77 | 78 | kernel.bind(A, _=> "hello"); 79 | kernel.bind(A, _=> " world"); 80 | 81 | let results = kernel.getAll(A); // ["hello", " world"]; 82 | ``` 83 | 84 | ## Providers 85 | 86 | ### to(symbol) 87 | 88 | used to redirect a binding to and resolve it through a different symbol. 89 | 90 | ```ts 91 | const A = Symbol.for("A") 92 | const B = Symbol.for("B") 93 | 94 | kernel.bind(A, to(B)); 95 | kernel.bind(B, _ => "hello world"); 96 | ``` 97 | 98 | ### toAll(symbol) 99 | 100 | used to resolve a symbol to all its bindings 101 | 102 | ```ts 103 | const A = Symbol.for("A") 104 | const B = Symbol.for("A"); 105 | 106 | kernel.bind(A, _ => "hello"); 107 | kernel.bind(A, _ => "world"); 108 | kernel.bind(B, toAll(A)); 109 | 110 | kernel.get(B); // ["hello", "world"] 111 | 112 | ``` 113 | 114 | ### toClass(Ctor, ...providers) 115 | 116 | used to provide an instantiation of a class. first parameter should be the class constructor and then it takes a list of providers that will be used, in the given order, to resolve the constructor arguments. 117 | 118 | ```ts 119 | class Foo{ 120 | constructor(public message: string) 121 | } 122 | 123 | const A = Symbol.for("A") 124 | const B = Symbol.for("B") 125 | 126 | kernel.bind(B, _=>"hello world"); 127 | kernel.bind(A, toClass(Foo, to(B))) 128 | ``` 129 | 130 | ### toConst(value) 131 | 132 | a more explicit way to provide a constant 133 | 134 | ```ts 135 | kernel.bind(B, toConst("hello world")); 136 | ``` 137 | 138 | ### asSingleton(provider) 139 | 140 | used to cache the output of the given provider so that subsequent requests will return the same result. 141 | 142 | ```ts 143 | const A = Symbol.for("A"); 144 | const B = Symbol.for("B"); 145 | 146 | kernel.bind(A, toClass(Foo)); 147 | kernel.bind(B, asSingleton(to(A))); 148 | ``` 149 | 150 | ### when(predicate, provider) 151 | 152 | used to predicate a provider for some condition. **any provider that explicitly returns `undefined` is ignored** 153 | 154 | ```ts 155 | const A = Symbol.for("A"); 156 | const B = Symbol.for("B"); 157 | const C = Symbol.for("C"); 158 | 159 | kernel.bind(A, toClass(Foo, to(C) )); 160 | kernel.bind(B, toClass(Foo, to(C) )); 161 | 162 | kernel.bind(C, when(x=>x.parent.target == A, toConst("a"))); 163 | kernel.bind(C, when(x=>x.parent.target == B, toConst("b"))); 164 | ``` 165 | 166 | ## Predicates 167 | 168 | ### injectedInto(symbol) 169 | 170 | returns true if `ctx.parent.target == symbol` 171 | 172 | ```ts 173 | const A = Symbol.for("A"); 174 | const B = Symbol.for("B"); 175 | const C = Symbol.for("C"); 176 | 177 | kernel.bind(A, toClass(Foo, to(C) )); 178 | kernel.bind(B, toClass(Foo, to(C) )); 179 | 180 | kernel.bind(C, when(injectedInto(A), toConst("a"))); 181 | kernel.bind(C, when(injectedInto(B), toConst("b"))); 182 | ``` 183 | 184 | ### hasAncestor(symbol) 185 | 186 | returns true if an request ancestor is equal to the symbol. 187 | 188 | ```ts 189 | const A = Symbol.for("A"); 190 | const B = Symbol.for("B"); 191 | const C = Symbol.for("C"); 192 | 193 | kernel.bind(A, when(hasAncestor(C), toConst("foo"))); 194 | kernel.bind(A, toConst("bar"))); 195 | kernel.bind(B, to(A)); 196 | kernel.bind(C, to(B)); 197 | 198 | let c = kernel.get(C); // "foo" 199 | let b = kernel.get(B); // "bar" 200 | ``` 201 | 202 | ## Transformer Usage 203 | 204 | with '@pigly/transformer' installed (see https://github.com/pigly-di/pigly/tree/develop/packages/pigly-transformerr) you are able to omit manually creating a symbol. Currently 205 | 206 | * `.bind(provider)` 207 | * `.get()` 208 | * `to()` 209 | * `toAll()` 210 | * `toSelf(Class)` 211 | * `injectedInto()` 212 | * `hasAncestor()` 213 | * `Inject()` 214 | 215 | are supported. 216 | 217 | ### Example 218 | 219 | ```ts 220 | class Foo implements IFoo{ 221 | constructor(public name: string){} 222 | } 223 | 224 | let kernel = new Kernel(); 225 | 226 | kernel.bind(toSelf(Foo)); 227 | 228 | kernel.bind( 229 | when(injectedInto( 230 | toConst("joe"))); 231 | 232 | kernel.bind(to()); 233 | 234 | let foo = kernel.get(); 235 | ``` 236 | 237 | ## toSelf(Class) 238 | 239 | attempts to infer the constructor arguments and generate the providers needed to initialise the class. It can only do so if the constructor arguments are simple. Currently only supports the _first_ constructor. 240 | 241 | ```ts 242 | kernel.bind(toSelf(Foo)); 243 | ``` 244 | is equivalent to 245 | ```ts 246 | kernel.bind(toClass(Foo, to, to... 247 | ``` 248 | 249 | 250 | ## SymbolFor() 251 | 252 | calls to SymbolFor() get replaced with `symbol.for("")` through `@pigly/transformer` and can be used if you want to be closer to the native usage i.e. 253 | 254 | ```ts 255 | let kernel = new Kernel(); 256 | 257 | const $IFoo = SymbolFor(); 258 | const $IBar = SymbolFor(); 259 | 260 | kernel.bind($IFoo, toClass(Foo, to($IBar))); 261 | kernel.bind($IBar, toClass(Bar)); 262 | 263 | let foo = kernel.get($IFoo); 264 | ``` 265 | 266 | The current approach in the transformer, to make the type's symbol, is to use the imported name directly i.e. `SymbolFor()` is converted to `Symbol.for("IFoo")`. The intention here is to give most flexibility and consistently in how the Symbols are created, especially if you want to configure a container across multiple independently-compiled libraries, or when using the transformer in a "transform only" build stage, as is typically the case with Webpack and Vue. The downside is that you must be consistent with type names, avoid renaming during imports and do not implement two or more interfaces with the exact same identifier-name. 267 | 268 | ## License 269 | MIT 270 | 271 | ## Credits 272 | 273 | "pig" licensed under CC from Noun Project, Created by habione 404, FR 274 | 275 | @pigly/transformer was derived from https://github.com/YePpHa/ts-di-transformer (MIT) 276 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pigly 2 | unobtrusive, manually configured, dependency-injection for javascript/typescript 3 | 4 | ![alt](https://avatars0.githubusercontent.com/u/50213493?s=400&u=65942b405a979397a2c358366db85c3d06f521f5&v=4) 5 | 6 | ![CircleCI](https://img.shields.io/circleci/build/github/pigly-di/pigly?token=abc123def456) ![npm](https://img.shields.io/npm/v/pigly) ![npm](https://img.shields.io/npm/dm/pigly) ![Codecov](https://img.shields.io/codecov/c/gh/pigly-di/pigly) 7 | 8 | > don't pollute the code with DI concerns 9 | 10 | pigly is a simple helper to manually configure a DI container to bind symbols to providers. It explicitly avoids decorators, or any other changes to existing code, and on its own doesn't require any other dependency / compilation-step to work. However, when combined with the typescript transformer `@pigly/transformer` we can reduce the amount of boiler-plate and simply describe the bindings as: 11 | 12 | ```ts 13 | interface INinja { 14 | weapon: IWeapon; 15 | } 16 | interface IWeapon { 17 | name: string; 18 | } 19 | 20 | class Ninja implements INinja { 21 | constructor(public weapon: IWeapon) { } 22 | } 23 | 24 | class Axe implements IWeapon { 25 | name = "axe"; 26 | } 27 | 28 | let kernel = new Kernel(); 29 | 30 | kernel.bind(toSelf(Ninja)); 31 | kernel.bind(toSelf(Axe)); 32 | 33 | let ninja = kernel.get(); 34 | ``` 35 | 36 | ## Planned features 37 | 38 | * Better inferring of constructors 39 | 40 | ## Native Usage 41 | 42 | native usage relates to using this package directly without any typescript transformer. 43 | 44 | Its pretty simple: create a kernel, create symbol-to-provider bindings, then get the resolved result with `get(symbol)` 45 | 46 | ```ts 47 | import { Kernel } from 'pigly'; 48 | 49 | let kernel = new Kernel(); 50 | 51 | kernel.bind(Symbol.for("Foo"), (ctx)=>{ return "foo" }); 52 | 53 | let foo = kernel.get(Symbol.for("Foo")); 54 | ``` 55 | 56 | ### .bind(symbol, provider) 57 | 58 | bind links a specific symbol to a provider of the form (context)=>value; 59 | 60 | ```ts 61 | kernel.bind(A, _=>{ return "hello world" }) 62 | ``` 63 | 64 | ### .get(symbol) 65 | 66 | resolve all the bindings for a symbol and return the first one 67 | 68 | ```ts 69 | const A = Symbol.for("A") 70 | 71 | kernel.bind(A, _=> "hello"); 72 | kernel.bind(A, _=> " world"); 73 | 74 | let result = kernel.get(A); // "hello"; 75 | ``` 76 | 77 | ### .getAll(symbol) 78 | 79 | resolve all the bindings for a symbol and return all of the results. 80 | 81 | ```ts 82 | const A = Symbol.for("A") 83 | 84 | kernel.bind(A, _=> "hello"); 85 | kernel.bind(A, _=> " world"); 86 | 87 | let results = kernel.getAll(A); // ["hello", " world"]; 88 | ``` 89 | 90 | ## Providers 91 | 92 | ### to(symbol) 93 | 94 | used to redirect a binding to and resolve it through a different symbol. 95 | 96 | ```ts 97 | const A = Symbol.for("A") 98 | const B = Symbol.for("B") 99 | 100 | kernel.bind(A, to(B)); 101 | kernel.bind(B, _ => "hello world"); 102 | ``` 103 | 104 | ### toAll(symbol) 105 | 106 | used to resolve a symbol to all its bindings 107 | 108 | ```ts 109 | const A = Symbol.for("A") 110 | const B = Symbol.for("B"); 111 | 112 | kernel.bind(A, _ => "hello"); 113 | kernel.bind(A, _ => "world"); 114 | kernel.bind(B, toAll(A)); 115 | 116 | kernel.get(B); // ["hello", "world"] 117 | 118 | ``` 119 | 120 | ### toClass(Ctor, ...providers) 121 | 122 | used to provide an instantiation of a class. first parameter should be the class constructor and then it takes a list of providers that will be used, in the given order, to resolve the constructor arguments. 123 | 124 | ```ts 125 | class Foo{ 126 | constructor(public message: string) 127 | } 128 | 129 | const A = Symbol.for("A") 130 | const B = Symbol.for("B") 131 | 132 | kernel.bind(B, _=>"hello world"); 133 | kernel.bind(A, toClass(Foo, to(B))) 134 | ``` 135 | 136 | ### toConst(value) 137 | 138 | a more explicit way to provide a constant 139 | 140 | ```ts 141 | kernel.bind(B, toConst("hello world")); 142 | ``` 143 | 144 | ### asSingleton(provider) 145 | 146 | used to cache the output of the given provider so that subsequent requests will return the same result. 147 | 148 | ```ts 149 | const A = Symbol.for("A"); 150 | const B = Symbol.for("B"); 151 | 152 | kernel.bind(A, toClass(Foo)); 153 | kernel.bind(B, asSingleton(to(A))); 154 | ``` 155 | 156 | ### when(predicate, provider) 157 | 158 | used to predicate a provider for some condition. **any provider that explicitly returns `undefined` is ignored** 159 | 160 | ```ts 161 | const A = Symbol.for("A"); 162 | const B = Symbol.for("B"); 163 | const C = Symbol.for("C"); 164 | 165 | kernel.bind(A, toClass(Foo, to(C) )); 166 | kernel.bind(B, toClass(Foo, to(C) )); 167 | 168 | kernel.bind(C, when(x=>x.parent.target == A, toConst("a"))); 169 | kernel.bind(C, when(x=>x.parent.target == B, toConst("b"))); 170 | ``` 171 | 172 | ### defer(provider, opts: {[field] : provider}) 173 | Used to defer injection (lazy injection) into the created object. This allows you to work around cyclic dependencies, by having one of them lazy inject into the other. You MUST be careful to ensure you're injecting constants or singletons, otherwise you can still cause a cyclic-loop. 174 | 175 | ``` 176 | class Foo { 177 | constructor(public bar: Bar) { } 178 | } 179 | 180 | class Bar { 181 | foo: Foo; 182 | constructor() { } 183 | } 184 | 185 | const $Foo = Symbol.for("Foo"); 186 | const $Bar = Symbol.for("Bar"); 187 | 188 | kernel.bind($Foo, 189 | /* IMPORTANT */ 190 | asSingleton( 191 | toClass(Foo, to($Bar)) 192 | )); 193 | 194 | kernel.bind($Bar, 195 | /* IMPORTANT */ 196 | asSingleton( 197 | defer( 198 | toClass(Bar), 199 | { 200 | foo: to($Foo) 201 | } 202 | ))); 203 | 204 | let foo = kernel.get($Foo); 205 | let bar = kernel.get($Bar); 206 | ``` 207 | 208 | 209 | ## Predicates 210 | 211 | ### injectedInto(symbol) 212 | 213 | returns true if `ctx.parent.target == symbol` 214 | 215 | ```ts 216 | const A = Symbol.for("A"); 217 | const B = Symbol.for("B"); 218 | const C = Symbol.for("C"); 219 | 220 | kernel.bind(A, toClass(Foo, to(C) )); 221 | kernel.bind(B, toClass(Foo, to(C) )); 222 | 223 | kernel.bind(C, when(injectedInto(A), toConst("a"))); 224 | kernel.bind(C, when(injectedInto(B), toConst("b"))); 225 | ``` 226 | 227 | ### hasAncestor(symbol) 228 | 229 | returns true if an request ancestor is equal to the symbol. 230 | 231 | ```ts 232 | const A = Symbol.for("A"); 233 | const B = Symbol.for("B"); 234 | const C = Symbol.for("C"); 235 | 236 | kernel.bind(A, when(hasAncestor(C), toConst("foo"))); 237 | kernel.bind(A, toConst("bar"))); 238 | kernel.bind(B, to(A)); 239 | kernel.bind(C, to(B)); 240 | 241 | let c = kernel.get(C); // "foo" 242 | let b = kernel.get(B); // "bar" 243 | ``` 244 | 245 | ## Transformer Usage 246 | 247 | with '@pigly/transformer' installed (see https://github.com/pigly-di/pigly/tree/develop/packages/pigly-transformer) you are able to omit manually creating a symbol. Currently 248 | 249 | * `.bind(provider)` 250 | * `.get()` 251 | * `to()` 252 | * `toAll()` 253 | * `toSelf(Class)` 254 | * `injectedInto()` 255 | * `hasAncestor()` 256 | * `Inject()` 257 | 258 | are supported. 259 | 260 | ### Example 261 | 262 | ```ts 263 | class Foo implements IFoo{ 264 | constructor(public name: string){} 265 | } 266 | 267 | let kernel = new Kernel(); 268 | 269 | kernel.bind(toSelf(Foo)); 270 | 271 | kernel.bind( 272 | when(injectedInto( 273 | toConst("joe"))); 274 | 275 | kernel.bind(to()); 276 | 277 | let foo = kernel.get(); 278 | ``` 279 | 280 | ## toSelf(Class) 281 | 282 | attempts to infer the constructor arguments and generate the providers needed to initialise the class. It can only do so if the constructor arguments are simple. Currently only supports the _first_ constructor. 283 | 284 | ```ts 285 | kernel.bind(toSelf(Foo)); 286 | ``` 287 | is equivalent to 288 | ```ts 289 | kernel.bind(toClass(Foo, to, to... 290 | ``` 291 | 292 | 293 | ## SymbolFor() 294 | 295 | calls to SymbolFor() get replaced with `symbol.for("")` through `@pigly/transformer` and can be used if you want to be closer to the native usage i.e. 296 | 297 | ```ts 298 | let kernel = new Kernel(); 299 | 300 | const $IFoo = SymbolFor(); 301 | const $IBar = SymbolFor(); 302 | 303 | kernel.bind($IFoo, toClass(Foo, to($IBar))); 304 | kernel.bind($IBar, toClass(Bar)); 305 | 306 | let foo = kernel.get($IFoo); 307 | ``` 308 | 309 | The current approach in the transformer, to make the type's symbol, is to use the imported name directly i.e. `SymbolFor()` is converted to `Symbol.for("IFoo")`. The intention here is to give most flexibility and consistently in how the Symbols are created, especially if you want to configure a container across multiple independently-compiled libraries, or when using the transformer in a "transform only" build stage, as is typically the case with Webpack and Vue. The downside is that you must be consistent with type names, avoid renaming during imports and do not implement two or more interfaces with the exact same identifier-name. 310 | 311 | ## Scoping 312 | 313 | Scoping affects how and when a service resolution is cached. By default all bindings are transient. 314 | 315 | ### Singleton 316 | the resolution of a service is cached in the root resolver, such that every request for the same service will receive the same instance 317 | 318 | ```ts 319 | kernel.bind(toClass(Foo), Scope.Singleton); 320 | 321 | const a = kernel.get(); 322 | const b = kernel.get(); 323 | 324 | assert(a === b); //true 325 | ``` 326 | ### Transient 327 | the resolution of a service is cached in the root resolver, such that every request for the same service will receive the same instance 328 | 329 | ```ts 330 | kernel.bind(toClass(Foo), Scope.Transient); 331 | // or 332 | kernel.bind(toClass(Foo)); 333 | 334 | const a = kernel.get(); 335 | const b = kernel.get(); 336 | 337 | assert(a !== b); //true 338 | ``` 339 | 340 | ### Request 341 | 342 | with request scoping, the resolution is cached by the current request 'scope' symbol. In a HTTP2 server example, we could decide to make a new 'scope' for each new stream connection. In this context, we can bind services to be unique, but cached, within each HTTP2 Stream 343 | 344 | ```ts 345 | class Foo { 346 | constructor( 347 | public bar1: Bar, 348 | public bar2: Bar){ 349 | } 350 | }; 351 | 352 | const HTTP2Request = Symbol('http2') 353 | 354 | kernel.bind(toClass(Foo, to()))); 355 | kernel.bind(toClass(Bar), HTTP2Request); 356 | 357 | const a = kernel.get(); 358 | const b = kernel.get(); 359 | 360 | assert(a !== b); //true - Foo binding transient 361 | assert(b.bar1 !== a.bar1); //true - 362 | assert(a.bar1 === a.bar2); //true 363 | assert(b.bar1 === b.bar2); //true 364 | ``` 365 | 366 | 367 | 368 | 369 | ## License 370 | MIT 371 | 372 | ## Credits 373 | 374 | "pig" licensed under CC from Noun Project, Created by habione 404, FR 375 | 376 | @pigly/transformer was derived from https://github.com/YePpHa/ts-di-transformer (MIT) 377 | -------------------------------------------------------------------------------- /packages/pigly/test/kernel.spec.ts: -------------------------------------------------------------------------------- 1 | import { Kernel, name, toConst, toClass, toFunc, to, IContext, when, defer, hasAncestor, named } from "../src"; 2 | import { expect } from 'chai'; 3 | import { Scope } from "../src/_scope"; 4 | import { ResolveError } from "../src/errors"; 5 | 6 | interface IFoo { 7 | bar: IBar; 8 | } 9 | interface IBar { 10 | value: number; 11 | } 12 | 13 | class Bar implements IBar { 14 | constructor(public value: number) { } 15 | } 16 | 17 | class Foo implements IFoo { 18 | constructor(public bar: IBar) { 19 | } 20 | } 21 | 22 | describe("Kernel Basics", () => { 23 | it("can bind and call provider", () => { 24 | const kernel = new Kernel(); 25 | 26 | const $IFoo = Symbol.for("IFoo"); 27 | 28 | let wasCalled = false; 29 | 30 | kernel.bind($IFoo, () => { wasCalled = true; return 10 }); 31 | 32 | let result = kernel.get($IFoo); 33 | 34 | expect(wasCalled, "provider was called").is.true; 35 | expect(result, "result is 10").is.equal(10); 36 | }) 37 | 38 | it("can bind multiple symbols and call provider for specific symbol", () => { 39 | const kernel = new Kernel(); 40 | 41 | const $IFoo = Symbol.for("IFoo"); 42 | const $IBar = Symbol.for("IBar"); 43 | 44 | let wasTargetCalled = false; 45 | let wasOtherCalled = false; 46 | 47 | kernel.bind($IFoo, () => { wasTargetCalled = true; return 10 }); 48 | kernel.bind($IBar, () => { wasOtherCalled = true; return 11 }); 49 | 50 | let result = kernel.get($IFoo); 51 | 52 | expect(wasTargetCalled, "target provider was called").is.true; 53 | expect(wasOtherCalled, "other provider was not called").is.false; 54 | expect(result, "result is 10").is.equal(10); 55 | }) 56 | 57 | it("can multi-bind and get first", () => { 58 | const kernel = new Kernel(); 59 | 60 | const $IFoo = Symbol.for("IFoo"); 61 | 62 | kernel.bind($IFoo, _ => 10); 63 | kernel.bind($IFoo, _ => 11); 64 | 65 | let result = kernel.get($IFoo); 66 | 67 | expect(result, "result is 10").to.eql(10); 68 | }) 69 | 70 | it("can multi-bind and resolve all ", () => { 71 | const kernel = new Kernel(); 72 | 73 | const $IFoo = Symbol.for("IFoo"); 74 | 75 | kernel.bind($IFoo, _ => 10); 76 | kernel.bind($IFoo, _ => 11); 77 | 78 | let result = []; 79 | 80 | for (let item of kernel.resolve({ service: $IFoo })) { 81 | result.push(item); 82 | } 83 | 84 | expect(result, "result is 10").to.eql([10, 11]); 85 | }) 86 | 87 | it("should ignore providers returning undefined", () => { 88 | const kernel = new Kernel(); 89 | 90 | const A = Symbol.for("A"); 91 | 92 | kernel.bind(A, _ => undefined); 93 | kernel.bind(A, toConst("hello")); 94 | 95 | let result = kernel.get(A); 96 | 97 | expect(result).to.be.eq("hello"); 98 | }) 99 | 100 | it("should lazily resolve bindings", () => { 101 | const kernel = new Kernel(); 102 | 103 | const A = Symbol.for("A"); 104 | 105 | kernel.bind(A, _ => undefined); 106 | kernel.bind(A, toConst("hello")); 107 | 108 | let result = kernel.get(A); 109 | 110 | expect(result).to.be.eq("hello"); 111 | }) 112 | }) 113 | 114 | describe("Resolving Context", () => { 115 | it("can keep the resolution hierarchy", () => { 116 | const kernel = new Kernel(); 117 | 118 | const A = Symbol.for("A"); 119 | const B = Symbol.for("B"); 120 | const C = Symbol.for("C"); 121 | 122 | let result: IContext = undefined; 123 | let wasCalled = false; 124 | 125 | kernel.bind(A, to(B)); 126 | kernel.bind(B, to(C)); 127 | kernel.bind(C, ctx => { 128 | result = ctx; 129 | wasCalled = true; 130 | return 10; 131 | }); 132 | 133 | let resolved = kernel.get(A); 134 | 135 | expect(wasCalled, "resolved to provider").is.true; 136 | expect(result.parent.service, "parent service is B").to.eql(B); 137 | expect(result.parent.parent.service, "parent parent service is A").to.eql(A); 138 | expect(resolved, "result is 10").to.eql(10); 139 | }) 140 | 141 | it("should throw on cyclic dependency", () => { 142 | const kernel = new Kernel(); 143 | 144 | const A = Symbol.for("A"); 145 | const B = Symbol.for("B"); 146 | const C = Symbol.for("C"); 147 | 148 | kernel.bind(A, to(B)); 149 | kernel.bind(B, to(C)); 150 | kernel.bind(C, to(A)); 151 | 152 | expect(() => { 153 | kernel.get(A); 154 | }).throws("Cyclic Dependency Found"); 155 | }) 156 | 157 | it("should throw on no resolution", () => { 158 | const kernel = new Kernel(); 159 | 160 | const A = Symbol.for("A"); 161 | 162 | kernel.bind(A, _ => undefined); 163 | 164 | expect(() => { 165 | kernel.get(A); 166 | }).throws("could not resolve Symbol(A)").instanceOf(ResolveError) 167 | }) 168 | }) 169 | 170 | describe("Providers", () => { 171 | it("can resolve a constant", () => { 172 | const kernel = new Kernel(); 173 | 174 | const $IFoo = Symbol.for("IFoo"); 175 | 176 | kernel.bind($IFoo, toConst(10)); 177 | 178 | let result = kernel.get($IFoo); 179 | 180 | expect(result, "result is 10").to.eql(10); 181 | }) 182 | 183 | it("can resolve a function", () => { 184 | const kernel = new Kernel(); 185 | 186 | const $IFoo = Symbol.for("IFoo"); 187 | 188 | kernel.bind($IFoo, _ => { return {} }); 189 | 190 | let result1 = kernel.get($IFoo); 191 | let result2 = kernel.get($IFoo); 192 | 193 | expect(result1, "results are different instances").is.not.equal(result2); 194 | }) 195 | 196 | it("can resolve a class", () => { 197 | const kernel = new Kernel(); 198 | 199 | class Foo { 200 | constructor(public arg1: number) { } 201 | } 202 | 203 | const $Foo = Symbol.for("Foo"); 204 | 205 | kernel.bind($Foo, toClass(Foo, _ => 10)); 206 | 207 | let result1 = kernel.get($Foo); 208 | let result2 = kernel.get($Foo); 209 | 210 | expect(result1, "result is instance of Foo").is.instanceOf(Foo); 211 | expect(result2, "result is instance of Foo").is.instanceOf(Foo); 212 | expect(result1.arg1, "ctor parameter was provided").is.eql(10); 213 | expect(result1, "results are different instances").is.not.equal(result2); 214 | }) 215 | 216 | it("can resolve a function", () => { 217 | const kernel = new Kernel(); 218 | 219 | function foo(arg1: number) { 220 | return arg1 * 2; 221 | } 222 | 223 | const $Foo = Symbol.for("Foo"); 224 | 225 | let wasCalled = false; 226 | 227 | kernel.bind($Foo, toFunc(foo, _ => { wasCalled = true; return 10 })); 228 | 229 | let result1 = kernel.get($Foo); 230 | 231 | expect(wasCalled, "function factory was called").is.true; 232 | expect(result1, "result is expected number").is.equal(20); 233 | }) 234 | 235 | 236 | it("can resolve a singleton", () => { 237 | const kernel = new Kernel(); 238 | 239 | const $IFoo = Symbol.for("IFoo"); 240 | 241 | kernel.bind($IFoo, _ => ({}), Scope.Singleton) 242 | 243 | let foo1 = kernel.get($IFoo); 244 | let foo2 = kernel.get($IFoo); 245 | 246 | expect(foo1, "results are same instance").is.equal(foo2); 247 | }) 248 | 249 | it("can resolve to all bindings", () => { 250 | const kernel = new Kernel(); 251 | 252 | const $IFoo = Symbol.for("IFoo"); 253 | 254 | kernel.bind($IFoo, _ => 1); 255 | kernel.bind($IFoo, _ => 2); 256 | kernel.bind($IFoo, _ => 3); 257 | 258 | interface A { 259 | all?: boolean; 260 | } 261 | 262 | let a: A & { all: true }; 263 | 264 | 265 | let foo = kernel.getAll($IFoo); 266 | 267 | expect(foo, "result is array").is.eql([1, 2, 3]) 268 | }) 269 | }) 270 | 271 | describe("Conditional", () => { 272 | it("can predicate provider with target name", () => { 273 | const kernel = new Kernel(); 274 | 275 | class Foo { 276 | constructor(public foo: string) { } 277 | } 278 | 279 | const A = Symbol.for("A"); 280 | const B = Symbol.for("B"); 281 | 282 | kernel.bind(A, when(named("foo"), toConst("FOO"))); 283 | kernel.bind(A, when(named("bar"), toConst("BAR"))); 284 | 285 | kernel.bind(B, toClass(Foo, name("foo", to(A)))); 286 | 287 | let resultA = kernel.get(B); 288 | expect(resultA.foo).is.eq("FOO"); 289 | }) 290 | 291 | it("can predicate provider with when", () => { 292 | const kernel = new Kernel(); 293 | 294 | class Foo { 295 | constructor(public arg1: string) { } 296 | } 297 | 298 | const A = Symbol.for("A"); 299 | const B = Symbol.for("B"); 300 | const C = Symbol.for("C"); 301 | 302 | kernel.bind(A, toClass(Foo, to(C))); 303 | kernel.bind(B, toClass(Foo, to(C))); 304 | 305 | kernel.bind(C, when(x => x.parent.service == A, toConst("a"))); 306 | kernel.bind(C, when(x => x.parent.service == B, toConst("b"))); 307 | 308 | let resultA = kernel.get(A); 309 | let resultB = kernel.get(B); 310 | 311 | expect(resultA.arg1).is.eq("a"); 312 | expect(resultB.arg1).is.eq("b"); 313 | 314 | }) 315 | }) 316 | 317 | describe("Deferred Injection", () => { 318 | it("can inject singleton cyclic-loop dependency", (done) => { 319 | const kernel = new Kernel(); 320 | 321 | class Foo { 322 | constructor(public bar: Bar) { } 323 | } 324 | 325 | class Bar { 326 | foo: Foo; 327 | constructor() { } 328 | } 329 | 330 | const $Foo = Symbol.for("Foo"); 331 | const $Bar = Symbol.for("Bar"); 332 | 333 | kernel.bind($Foo, toClass(Foo, to($Bar)), Scope.Singleton); 334 | kernel.bind($Bar, defer(toClass(Bar), { foo: to($Foo) }), Scope.Singleton); 335 | 336 | let foo = kernel.get($Foo); 337 | let bar = kernel.get($Bar); 338 | 339 | setImmediate(() => { 340 | expect(foo.bar).is.instanceOf(Bar); 341 | expect(bar.foo).is.instanceOf(Foo); 342 | expect(foo.bar).is.equal(bar); 343 | expect(bar.foo).is.equal(foo); 344 | done(); 345 | }); 346 | }) 347 | 348 | it("can predicate provider with target name", (done) => { 349 | const kernel = new Kernel(); 350 | 351 | class Foo { 352 | constructor(public foo: string) { } 353 | } 354 | 355 | const A = Symbol.for("A"); 356 | const B = Symbol.for("B"); 357 | 358 | kernel.bind(A, when(named("foo"), toConst("FOO"))); 359 | kernel.bind(A, when(named("bar"), toConst("BAR"))); 360 | 361 | // inject with "bar" as target, then defer assign with "foo" target 362 | kernel.bind(B, defer(toClass(Foo, name("bar", to(A))), { foo: to(A) })) 363 | 364 | let resultA = kernel.get(B); 365 | setImmediate(() => { 366 | expect(resultA.foo).is.eq("FOO"); 367 | done(); 368 | }) 369 | }) 370 | }) 371 | 372 | 373 | describe("Predicates", () => { 374 | describe("hasAncestor", () => { 375 | it("can inject symbol if ancestor matches", () => { 376 | const kernel = new Kernel(); 377 | 378 | const A = Symbol.for("A"); 379 | const B = Symbol.for("B"); 380 | const C = Symbol.for("C"); 381 | 382 | kernel.bind(A, when(hasAncestor(C), toConst("foo"))); 383 | kernel.bind(B, to(A)); 384 | kernel.bind(C, to(B)); 385 | 386 | let c = kernel.get(C); 387 | 388 | expect(c).to.be.eq("foo"); 389 | }) 390 | it("can inject symbol if ancestor matches and fall-back to other bindings", () => { 391 | const kernel = new Kernel(); 392 | 393 | const A = Symbol.for("A"); 394 | const B = Symbol.for("B"); 395 | const C = Symbol.for("C"); 396 | 397 | kernel.bind(A, when(hasAncestor(C), toConst("foo"))); 398 | kernel.bind(A, toConst("bar")); 399 | kernel.bind(B, to(A)); 400 | kernel.bind(C, to(B)); 401 | 402 | let c = kernel.get(C); 403 | let b = kernel.get(B); 404 | 405 | expect(c).to.be.eq("foo"); 406 | expect(b).to.be.eq("bar"); 407 | }) 408 | 409 | it("throws if request does not have required ancestor", () => { 410 | const kernel = new Kernel(); 411 | 412 | const A = Symbol.for("A"); 413 | const B = Symbol.for("B"); 414 | const C = Symbol.for("C"); 415 | 416 | kernel.bind(A, when(hasAncestor(C), toConst("foo"))); 417 | kernel.bind(B, to(A)); 418 | kernel.bind(C, to(B)); 419 | 420 | expect(() => { 421 | let b = kernel.get(B); 422 | }).throws() 423 | }) 424 | }) 425 | }) -------------------------------------------------------------------------------- /packages/pigly-transformer/src/transformer.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | function isServiceType(node: ts.Node, typeChecker: ts.TypeChecker){ 4 | let type = typeChecker.getTypeAtLocation(node); 5 | let flags = type.getFlags(); 6 | 7 | return flags == ts.TypeFlags.ESSymbol 8 | } 9 | 10 | function createSymbolFor(escapedName: string) { 11 | return ts.factory.createCallExpression( 12 | ts.factory.createPropertyAccessExpression( 13 | ts.factory.createIdentifier('Symbol'), 14 | ts.factory.createIdentifier('for') 15 | ), 16 | [], 17 | [ 18 | ts.factory.createStringLiteral(escapedName) 19 | ] 20 | ) 21 | 22 | // return ts.createCall(ts.createIdentifier('Symbol.for'), [], [ts.createStringLiteral(escapedName)]); 23 | } 24 | 25 | function createTypeSymbolFromCallExpressionTypeArguments(node: ts.CallExpression, typeChecker: ts.TypeChecker) { 26 | let typeSymbol: ts.CallExpression; 27 | 28 | if (node.typeArguments && node.typeArguments[0]) { 29 | const typeArgument = node.typeArguments[0]; 30 | 31 | if (ts.isTypeReferenceNode(typeArgument)) { 32 | //console.log("is type ref"); 33 | /** crude brute-force escaping of the type argument */ 34 | let typeString = typeArgument.getText().replace(/\s/g, ''); 35 | typeSymbol = createSymbolFor(typeString); 36 | } 37 | else if (ts.isToken(typeArgument)) { 38 | //console.log("is token"); 39 | switch (typeArgument.kind) { 40 | case ts.SyntaxKind.StringKeyword: 41 | typeSymbol = createSymbolFor("string"); 42 | break; 43 | case ts.SyntaxKind.NumberKeyword: 44 | typeSymbol = createSymbolFor("number"); 45 | break; 46 | } 47 | }else if(ts.isTypeQueryNode(typeArgument)){ 48 | //console.log("is query node"); 49 | 50 | //let type = typeChecker.getTypeAtLocation(typeArgument); 51 | 52 | //console.log(typeArgument.exprName.getText()); 53 | 54 | //if(type.symbol){ 55 | // let typeString = type.symbol.getEscapedName().toString().replace(/\s/g, ''); 56 | // typeSymbol = createSymbolFor(typeString); 57 | //} 58 | //else{ 59 | let typeString = typeArgument.getText().toString(); 60 | typeSymbol = createSymbolFor(typeString); 61 | //} 62 | //console.log("moo"); 63 | //console.log(typeChecker.getTypeAtLocation(typeArgument.exprName)); 64 | //console.log(typeArgument.exprName); 65 | }else{ 66 | //console.log("unknown", typeArgument.kind); 67 | } 68 | } 69 | else { 70 | let typeArgument = inferTypeArguments(node, typeChecker); 71 | 72 | if (typeArgument && typeArgument[0]) { 73 | typeSymbol = createSymbolFor(typeArgument[0].symbol.escapedName.toString()); 74 | } 75 | 76 | /*if (ts.isTypeReferenceNode(typeArgument)) { 77 | typeSymbol = createSymbolFor(typeArgument.getFullText()); 78 | } else if (ts.isToken(typeArgument)) { 79 | switch (typeArgument.kind) { 80 | case ts.SyntaxKind.StringKeyword: 81 | typeSymbol = createSymbolFor("string"); 82 | break; 83 | } 84 | }*/ 85 | } 86 | 87 | return typeSymbol; 88 | } 89 | 90 | 91 | function createCallWithInjectedSymbol(node: ts.CallExpression, typeChecker: ts.TypeChecker, visit:(node: ts.Node)=>ts.Node) { 92 | 93 | // const type = typeChecker.getTypeFromTypeNode(typeArgument); 94 | //console.log("Identifier", typeArgument); 95 | 96 | let typeSymbol = createTypeSymbolFromCallExpressionTypeArguments(node, typeChecker); 97 | 98 | 99 | 100 | if (typeSymbol !== undefined) { 101 | const args = []; 102 | 103 | args.push(typeSymbol); 104 | for (let arg of node.arguments) { 105 | args.push(visit(arg)); 106 | } 107 | //args.push(...appendedParams); 108 | 109 | //const nodeResult = ts.getMutableClone(node); 110 | 111 | // nodeResult.arguments = ts.createNodeArray(args); 112 | 113 | const nodeResult = ts.factory.createCallExpression( 114 | node.expression, 115 | null, 116 | args 117 | ) 118 | 119 | //console.log("injected Symbol into call expression: ", node.getText() ) 120 | 121 | return nodeResult; 122 | } 123 | 124 | return node; 125 | } 126 | 127 | 128 | function createSelfCtorCallWithInjectedProviders(node: ts.CallExpression, typeChecker: ts.TypeChecker) { 129 | 130 | //console.log(node); 131 | const ctorArg = node.arguments[0]; 132 | const type = typeChecker.getTypeAtLocation(ctorArg); 133 | 134 | if (type.symbol == undefined) { 135 | throw Error(`class constructor cannot be located - use explicit providers or disable transpileOnly`); 136 | } 137 | 138 | if (type.symbol.valueDeclaration != null) { 139 | let ctors = getClassConstructSignatures(type as ts.InterfaceType, typeChecker); 140 | 141 | let ctor = ctors[0]; 142 | 143 | let providerCalls = getConstructorProviders(ctor, typeChecker); 144 | 145 | if (providerCalls.findIndex(x => x === null) != -1) { 146 | throw Error(`class ${type.symbol.name}'s constructor cannot be inferred - use explicit providers`); 147 | } 148 | 149 | const nodeResult = ts.factory.createCallExpression( 150 | node.expression, 151 | [], 152 | [node.arguments[0], ...providerCalls] 153 | ) 154 | 155 | //const nodeResult = ts.getMutableClone(node); 156 | 157 | //console.log(typeArgument); 158 | 159 | //nodeResult.typeArguments = ts.createNodeArray(); 160 | // nodeResult.arguments = ts.createNodeArray([ 161 | // node.arguments[0], 162 | // , ...providerCalls 163 | //]); 164 | return nodeResult; 165 | } 166 | 167 | return node; 168 | } 169 | 170 | function getClassConstructSignatures(type: ts.InterfaceType, typeChecker: ts.TypeChecker) { 171 | let symbol = type.symbol; 172 | let constructorType = typeChecker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration); 173 | return constructorType.getConstructSignatures(); 174 | } 175 | 176 | function getConstructorProviders(ctor: ts.Signature, typeChecker: ts.TypeChecker): ts.Expression[] { 177 | let params: ts.Expression[] = []; 178 | for (let param of ctor.parameters) { 179 | let paramDecl = param.declarations[0]; 180 | 181 | if (ts.isParameter(paramDecl)) { 182 | 183 | let paramType = paramDecl.type; 184 | let paramName = paramDecl.name; 185 | let isArray = false; 186 | 187 | if (ts.isArrayTypeNode(paramType)) { 188 | paramType = paramType.elementType; 189 | isArray = true; 190 | } 191 | 192 | if (ts.isTypeReferenceNode(paramType)) { 193 | const symbol = typeChecker.getSymbolAtLocation(paramType.typeName); 194 | const type = typeChecker.getDeclaredTypeOfSymbol(symbol); 195 | params.push(createProvider(createSymbolFor(type.symbol.name), isArray, paramName.getText())); 196 | 197 | } else if (ts.isToken(paramDecl.type)) { 198 | switch (paramDecl.type.kind) { 199 | case ts.SyntaxKind.StringKeyword: 200 | params.push(createProvider(createSymbolFor("string"), isArray, paramName.getText())); 201 | break; 202 | case ts.SyntaxKind.NumberKeyword: 203 | params.push(createProvider(createSymbolFor("number"), isArray)); 204 | break; 205 | default: 206 | params.push(null); 207 | } 208 | } 209 | else { 210 | params.push(null); 211 | } 212 | } 213 | } 214 | return params; 215 | } 216 | 217 | function createProvider(symbol: ts.CallExpression, isArray: boolean, name?: string) { 218 | let props: ts.ObjectLiteralElementLike[] = [ 219 | ts.factory.createPropertyAssignment("service", symbol) 220 | ] 221 | 222 | if (name) { 223 | props.push( 224 | ts.factory.createPropertyAssignment("name", ts.factory.createStringLiteral(name)) 225 | ) 226 | } 227 | 228 | let request = ts.factory.createObjectLiteralExpression(props); 229 | let deref = (isArray) ? "toArray" : "first"; 230 | 231 | let elmt = ts.factory.createCallExpression( 232 | ts.factory.createPropertyAccessExpression( 233 | ts.factory.createCallExpression( 234 | ts.factory.createPropertyAccessExpression( 235 | ts.factory.createIdentifier("ctx"), ts.factory.createIdentifier("resolve")), 236 | undefined, 237 | [request]), ts.factory.createIdentifier(deref)), undefined, []); 238 | 239 | return ts.factory.createArrowFunction( 240 | undefined, 241 | undefined, 242 | [ 243 | ts.factory.createParameterDeclaration([], undefined, "ctx", undefined, ts.factory.createTypeReferenceNode('any', [])) 244 | ], 245 | undefined, 246 | undefined, 247 | elmt 248 | ) 249 | } 250 | 251 | //https://stackoverflow.com/questions/48886508/typechecker-api-how-do-i-find-inferred-type-arguments-to-a-function 252 | 253 | /* @internal */ 254 | const enum TypeMapKind { 255 | Simple, 256 | Array, 257 | Function, 258 | Composite, 259 | Merged, 260 | } 261 | 262 | /* @internal */ 263 | type TypeMapper = 264 | | { kind: TypeMapKind.Simple, source: ts.Type, target: ts.Type } 265 | | { kind: TypeMapKind.Array, sources: readonly ts.Type[], targets: readonly ts.Type[] | undefined } 266 | | { kind: TypeMapKind.Function, func: (t: ts.Type) => ts.Type } 267 | | { kind: TypeMapKind.Composite | TypeMapKind.Merged, mapper1: TypeMapper, mapper2: TypeMapper }; 268 | 269 | 270 | function typeMapper(mapper: TypeMapper, source: ts.Type): ts.Type { 271 | 272 | switch (mapper.kind) { 273 | case TypeMapKind.Simple: 274 | return mapper.target; 275 | case TypeMapKind.Array: 276 | throw Error("not implemented"); 277 | case TypeMapKind.Function: 278 | return mapper.func(source); 279 | case TypeMapKind.Composite: 280 | case TypeMapKind.Merged: 281 | return typeMapper(mapper.mapper2, source); 282 | } 283 | } 284 | 285 | function inferTypeArguments(node: ts.CallExpression, typeChecker: ts.TypeChecker): ts.Type[] { 286 | const signature: ts.Signature = typeChecker.getResolvedSignature(node); 287 | const targetParams: ts.TypeParameter[] = signature['target'] && signature['target'].typeParameters; 288 | 289 | if (!targetParams) { 290 | return []; 291 | } 292 | 293 | if (signature['mapper'] == undefined) 294 | return targetParams; 295 | 296 | //typescript <= 3.8 297 | if (typeof signature['mapper'] == "function") 298 | return targetParams.map(p => signature['mapper'](p)); 299 | //typescript >= 3.9.... 300 | return targetParams.map(p => typeMapper(signature['mapper'] as TypeMapper, p)); 301 | } 302 | 303 | //need to ensure imports still work for toClass() - 304 | //https://github.com/Microsoft/TypeScript/issues/18369 305 | export function transformer(program: ts.Program/*, opts?:{debug?: boolean}*/) : ts.TransformerFactory { 306 | const typeChecker = program.getTypeChecker(); 307 | //console.log("PIGLY!"); 308 | return ((context: ts.TransformationContext) => { 309 | function visit(node: ts.Node) { 310 | if (ts.isCallExpression(node)) { 311 | let methodArgs = node.arguments; 312 | let typeArgs = node.typeArguments; 313 | let methodName = undefined; 314 | //console.log("is call expression"); 315 | 316 | if (ts.isIdentifier(node.expression)) { 317 | methodName = node.expression.escapedText.toString(); 318 | 319 | if (!methodName) { 320 | //console.log("no method name"); 321 | return node; 322 | } 323 | 324 | if (methodName == "SymbolFor") { 325 | //console.log("SymbolFor") 326 | return createTypeSymbolFromCallExpressionTypeArguments(node, typeChecker); 327 | } 328 | 329 | if (((methodName == "Inject" || methodName == "to" || methodName == "toAll" || methodName == "injectedInto" || methodName == "hasAncestor") && typeArgs && typeArgs.length == 1 && methodArgs.length == 0)) { 330 | return createCallWithInjectedSymbol(node, typeChecker, visit); 331 | } 332 | 333 | if ((methodName == "toSelf" && methodArgs.length == 1)) { 334 | return createSelfCtorCallWithInjectedProviders(node, typeChecker); 335 | } 336 | } 337 | else if (ts.isPropertyAccessExpression(node.expression)) { 338 | methodName = node.expression.name.escapedText.toString(); 339 | 340 | if ((methodName == "bind" && methodArgs.length > 0 && isServiceType(methodArgs[0], typeChecker) == false)) { 341 | return createCallWithInjectedSymbol(node, typeChecker, visit); 342 | } 343 | if (((methodName == "get" || methodName == "getAll") && typeArgs && typeArgs.length == 1 && methodArgs.length == 0)) { 344 | return createCallWithInjectedSymbol(node, typeChecker, visit); 345 | } 346 | } 347 | } 348 | return ts.visitEachChild(node, visit, context); 349 | } 350 | 351 | return (sourceFile: ts.SourceFile) => ts.visitNode(sourceFile, visit) 352 | }) as ts.TransformerFactory 353 | } 354 | --------------------------------------------------------------------------------