npm i cheap-di-ts-transform --save-dev
Typescript code transformer. It produces constructor dependencies information to be able to use Dependency Injection approach with cheap-di
package
// no constructors => no depdendencies
abstract class Logger {
abstract debug: (message: string) => void;
}
// no constructors => no depdendencies
class ConsoleLogger extends Logger {
debug(message: string) {
console.log(message);
}
}
// has constructor => has depdendencies => leads to code generation
class Service {
constructor(public logger: Logger) {}
doSome() {
this.logger.debug('Hello world!');
}
}
/** cheap-di-ts-transform will add folowwing code:
* @example
* try {
* const cheapDi = require('cheap-di');
* cheapDi.saveConstructorMetadata(Service, Logger);
* } catch (error: unknown) {
* console.warn(error);
* }
* */
// somewhere
import { container } from 'cheap-di';
container.registerImplementation(ConsoleLogger).as(Logger);
const service = container.resolve(Service);
console.log(service instanceof Service); // true
console.log(service.logger instanceof ConsoleLogger); // true
console.log(service.doSome()); // 'Hello world!'
more examples:
// no constructors => no depdendencies
class JustSomeClass {}
class Example1 {
// string (as well as any non-class parameters) will interpreted as 'unknown' dependency
constructor(name: string) {}
}
/** cheap-di-ts-transform will add folowwing code:
* @example
* try {
* const cheapDi = require('cheap-di');
* cheapDi.saveConstructorMetadata(Example1, 'unknown');
* } catch (error: unknown) {
* console.warn(error);
* }
* */
interface MyInterface {
//
}
class Example2 {
constructor(
service: Service,
some: number, // 'unknown'
example1: Example1,
foo: boolean, // 'unknown'
logger: Logger,
bar: { data: any }, // 'unknown'
callback: () => void, // 'unknown'
myInterface: MyInterface // 'unknown'
) {}
}
/** cheap-di-ts-transform will add folowwing code:
* @example
* try {
* const cheapDi = require('cheap-di');
* cheapDi.saveConstructorMetadata(Example2, Service, "unknown", Example1, "unknown", Logger, "unknown", unknown, "unknown");
* } catch (error: unknown) {
* console.warn(error);
* }
* */
in case when you use class from some package:
import { SomeClass } from 'some-package';
class Example3 {
constructor(service: SomeClass) {}
}
/** cheap-di-ts-transform will add folowwing code:
* @example
* try {
* const cheapDi = require('cheap-di');
* const { SomeClass } = require('some-package');
* cheapDi.saveConstructorMetadata(Example3, SomeClass);
* } catch (error: unknown) {
* console.warn(error);
* }
* */
If the imported class also used a class in its constructor:
Note: works with
deepRegistration: true
// "some-package"
// there are 3 files:
// SomeClass.ts, AnotherClass.ts, index.ts
// AnotherClass.ts
export class AnotherClass {}
// SomeClass.ts
import { AnotherClass } from './AnotherClass';
export class SomeClass {
constructor(anotherClass: AnotherClass) {}
}
// index.ts
export * from './AnotherClass';
export * from './SomeClass';
// end of "some-package"
// your application
import { SomeClass } from 'some-package';
class Example3 {
constructor(service: SomeClass) {}
}
/** cheap-di-ts-transform will add folowwing code:
* @example
* try {
* const cheapDi = require('cheap-di');
* const { SomeClass } = require('some-package');
* cheapDi.saveConstructorMetadata(Example3, SomeClass);
*
* try {
* const { AnotherClass } = require('some-package');
* cheapDi.saveConstructorMetadata(SomeClass, AnotherClass);
* } catch (error: unknown) {
* console.warn(error);
* }
* } catch (error: unknown) {
* console.warn(error);
* }
*/
Be careful. If you use dependencies from package, that use another depenedncies in this package, that are not exported from there. You will get an error during bundling with webpack.
// if "index.ts" in "some-package" from example above will look like:
export * from './SomeClass';
// and no export of AnotherClass
// you will get error during in bunlding time
name | value by default | description |
---|---|---|
debug | false |
gets node names if you want to debug transformation |
addDetailsToUnknownParameters | false |
adds primitive types information of class parameters, to debug if something went wrong, instead of just unknown you will get something like primitive /<parameter-name>/ :string |
logRegisteredMetadata | false |
adds console.debug call before saveConstructorMetadata function call. Useful to get debug information traced. You will see this information at runtime in console |
errorsLogLevel | "warn" |
used in try-catch statements to log registration errors |
esmImports | false |
use await import('package') instead of require('package') . It works with top level await in esm |
deepRegistration | false |
add dependencies of dependencies right in place or not |
deepRegistration
example:
import { Repository } from './Repository';
export class Service1 {
constructor(private repository: Repository) {}
data() {
return this.repository.users();
}
}
// region: deepRegistration: false
try {
const cheapDi = await import('cheap-di');
const { Repository } = await import('./Repository.ts');
cheapDi.saveConstructorMetadata(Service1, Repository);
} catch (error) {
console.warn(error);
}
// endregion
// region: deepRegistration: true
try {
const cheapDi = await import('cheap-di');
const { Repository } = await import('./Repository.ts');
cheapDi.saveConstructorMetadata(Service1, Repository);
try {
const { Logger } = await import('./Repository.ts');
cheapDi.saveConstructorMetadata(Repository, Logger);
} catch (error) {
console.warn(error);
}
} catch (error) {
console.warn(error);
}
// endregion
Warning
The transformer does not work properly when used together with fork-ts-checker-webpack-plugin
.
If you have any thoughts on why is it and/or how we can fix it, please open the issue with details.
// webpack.config.ts
import path from 'path';
import { transformer } from 'cheap-di-ts-transform';
const tsconfigFilePath = path.join(__dirname, 'tsconfig.json');
const config = {
// ...
module: {
rules: [
// ...
{
loader: 'ts-loader',
test: /\.ts$/,
options: {
getCustomTransformers: (program) => ({
before: [
transformer(
{ program },
{
// options are optional
debug: false,
addDetailsToUnknownParameters: false,
logRegisteredMetadata: false,
errorsLogLevel: "warn",
esmImports: false
}
),
],
}),
configFile: tsconfigFilePath,
},
},
],
},
};
export default config;
You may use the transformer in nodejs, but in this case you need to use a compiler like ts-patch
.
tsconfig.json
{
"compilerOptions": {
// [...]
"plugins": [
{
"transform": "cheap-di-ts-transform",
// all options are optional
"debug": false,
"addDetailsToUnknownParameters": false,
"logRegisteredMetadata": false,
"errorsLogLevel": "warn",
"esmImports": false
}
]
},
"ts-node": {
"compiler": "ts-patch/compiler"
}
}
{
// [...]
"transform": {
"^.+\\.ts?$": [
"ts-jest",
{
"astTransformers": {
"before": [
{
"path": "cheap-di-ts-transform",
// all options are optional
"options": {
"debug": false,
"addDetailsToUnknownParameters": false,
"logRegisteredMetadata": false,
"errorsLogLevel": "warn",
"esmImports": false
}
}
]
}
}
]
}
}
// vite.config.ts
import { defineConfig } from 'vite';
import typescript from '@rollup/plugin-typescript';
import { transformer } from 'cheap-di-ts-transform';
export default defineConfig({
plugins: [
// ...
typescript({
transformers: {
before: [
{
type: 'program',
factory: (program) =>
transformer(
{ program },
{
// (required) use esm imports to dependency registration
esmImports: true,
// (optional) debugging options
debug: true,
addDetailsToUnknownParameters: true,
logRegisteredMetadata: true,
errorsLogLevel: 'debug',
}
),
},
],
},
}),
],
});