Skip to main content

Transformer Plugin

@automapper/classes/transformer-plugin is part of the public API of @automapper/classes.

Problem

Decorating Classes' members with @AutoMap() is verbose, even when we're being meticulous about what members are being auto-configured vs custom-configured. In some other cases, the Classes themselves are being generated, and/or from external libraries that the consumers cannot touch.

@automapper/classes/transformer-plugin is to ease this pain point when using @automapper/classes

How it works

Let's look at the following classes

class Profile {
bio!: string;
age!: number;
}
class User {
firstName!: string;
lastName!: string;
profile!: Profile;
}

Throughout the documentation, we all know that the above code will be compiled to

class Profile {}
class User {}

The requirement for @automapper/classes to work is to decorate all the members of both classes with @AutoMap in order for @automapper/classes to keep track of the metadata of each class.

class Profile {
@AutoMap()
bio!: string;
@AutoMap()
age!: number;
}
class User {
@AutoMap()
firstName!: string;
@AutoMap()
lastName!: string;
@AutoMap(() => Profile)
profile!: Profile;
}

This will get very verbose very soon.

@automapper/classes/transformer-plugin runs a before transformer that affects the AST directly before the Compilation step.

The transformer will

  • Look at files that end with .entity.ts, .model.ts, .vm.ts, and .dto.ts (this can be changed via transformer plugin options)
  • Iterate through all the members (PropertyDeclaration) of each class (ClassDeclaration) that it finds
  • Store the members in a list that @automapper/classes can understand
  • Add to each class a static method and return the list.

Let's look at the above snippet again

// your code
class Profile {
bio!: string;
age!: number;
}
class User {
firstName!: string;
lastName!: string;
profile!: Profile;
}

// after "before" transformer runs through your code

class Profile {
bio!: string;
age!: number;

static __AUTOMAPPER_METADATA_FACTORY__() {
return [
['bio', { type: () => String, depth: 1 }],
['age', { type: () => Number, depth: 1 }],
];
}
}
class User {
firstName!: string;
lastName!: string;
profile!: Profile;

static __AUTOMAPPER_METADATA_FACTORY__() {
return [
['firstName', { type: () => String, depth: 1 }],
['lastName', { type: () => String, depth: 1 }],
['profile', { type: () => Profile, depth: 1 }],
];
}
}

After compilation, the members will be gone, but the static function will stay making it available to be called at runtime. Hence, the metadata will be available. @automapper/classes/transformer-plugin only adds the minimum amount of code needed to keep track of the metadata.

Limitations

Currently, @automapper/classes/transformer-plugin will handle most Nullable (type | null) and Maybe (propKey?: type) cases. However, for complex cases where you have unions with different types (string | number | boolean or ClassA | ClassB), please consider decorate the property (field) manually with @AutoMap() decorator.

Usage

This is utilizing an experimental feature of TypeScript. Hence, you need to modify the build step of your project to use @automapper/classes/transformer-plugin

Ignore a property

The plugin will automatically construct the metadata for all properties that aren't decorated with @AutoMap. However, there are also cases where you neither want @AutoMap nor having the plugin processing a property (eg: you want to take the control 100% with manual configuration). To ignore a property completely, you can use a JSDoc tag @autoMapIgnore on a property

class User {
firstName!: string;
lastName!: string;
profile!: Profile;
/**
* @autoMapIgnore
*/
ignoreMe!: string;
}

Options

export interface AutomapperTransformerPluginOptions {
modelFileNameSuffix?: string[];
}

modelFileNameSuffix is default to ['.entity.ts', '.model.ts', '.vm.ts', '.dto.ts']

Webpack

If you use ts-loader or some fork of ts-loader, you can configure Webpack config to turn on Transformers

// snip
const automapperTransformerPlugin = require('@automapper/classes/transformer-plugin');
const pluginOptions = {
modelFileNameSuffix: [
/*...*/
],
};
module.exports = {
// snip
module: {
rules: [
// snip
{
test: /\.tsx?$/,
loader: 'ts-loader',
options: {
getCustomTransformers: (program) => ({
before: [
automapperTransformerPlugin(program, pluginOptions)
.before,
],
}),
},
},
// snip
],
},
// snip
};

Rollup

Use rollup-plugin-typescript2 as it has transformers capability

import automapperTransformerPlugin from '@automapper/classes/transformer-plugin';
import typescript from 'rollup-plugin-typescript2';
const pluginOptions = {
modelFileNameSuffix: [
/*...*/
],
};
export default {
// snip
preserveModules: true, // <-- turn on preserveModules
plugins: [
// snip
typescript({
transformers: [
(service) => ({
before: [
automapperTransformerPlugin(
service.getProgram(),
pluginOptions
).before,
],
}),
],
}),
// snip
],
};

ttypescript

ttypescript patches typescript in order to use transformers in tsconfig.json. See ttypescript's README for how to use this with module bundlers such as webpack or Rollup.

{
"compilerOptions": {
...,
"plugins": [
{
"transform": "@automapper/classes/transformer-plugin",
"modelFileNameSuffix": [...]
}
],
...
}
}

NestJS CLI

nestjs/cli can enable Transformers by default. To use this plugin with nestjs/cli, modify your nest-cli.json

{
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"plugins": ["@automapper/classes/transformer-plugin"]
}
}

or with options

{
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"plugins": [
{
"name": "@automapper/classes/transformer-plugin",
"options": {
"modelFileNameSuffix": [".dto.ts", ".vm.ts"]
}
}
]
}
}

NestJS with Nx

Nx v12.8 adds support for TypeScript Compiler plugins via an option called tsPlugins (now transformers) in their @nrwl/node:webpack executor, which is what @nrwl/nest is using for building the application.

Read more about the usage here: Nx 12.8 Blog post

Pre 12.8

NestJS in Nx workspace utilizes nrwl/node:build executor (formerly, builder) which allows you to pass in a custom Webpack config. However, to turn on Transformer, there's still this open issue in which you can find multiple solutions/workarounds as of the moment.

Angular

Angular CLI has sophisticated build process so please take a look at this article and related articles mentioned to come up with your approach of turning on Transformers for Angular projects