Skip to content

Latest commit

History

History
300 lines (249 loc) 路 8.67 KB

File metadata and controls

300 lines (249 loc) 路 8.67 KB

@automapper/classes/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 you'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 property on 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, .model.ts, .vm.ts, and .dto.ts (this can be changed via transformer plugin options)
  • Iterate through all the members (PropertyDeclaration and GetAccessorDeclaration) of each class (ClassDeclaration) that it finds
  • Store the members in a metadata list that @automapper/classes can understand
  • Add to each class a static method and return the metadata list that is later retrieved and stored on the Mapper.

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.

Experimental?

@automapper/classes/transformer-plugin utilizes TypeScript Compiler API to traverse and manipulate the Abstract Syntax Tree (AST) to automate the process of decorating the Classes. The Compiler API, while having been around for quite some time, is still marked as not yet stable.

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 or use forMember() to provide a more comprehensive mapping configuration.

Matrices (Array of Array type. Eg: string[][]) should also be handled manually.

Usage

As mentioned above, 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

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
    ],
};

ts-patch

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

{
  "compilerOptions": {
    ...,
    "plugins": [
        {
            "transform": "@automapper/classes/transformer-plugin",
            "import": "tspBefore",
            "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

In Nx workspaces, NestJS applications and buildable libraries are handled with nrwl/node:webpack and nrwl/js:tsc executors respectively. Both of these executors have an option called transformers that you can utilize to provide the plugin.

{
    "app": {
        "targets": {
            "build": {
                "executor": "@nrwl/node:webpack",
                "options": {
                    "transformers": [
                        {
                            "name": "@automapper/classes/transformer-plugin",
                            "options": {
                                "modelFileNameSuffix": [".dto.ts"]
                            }
                        }
                    ]
                }
            }
        }
    },
    "lib": {
        "targets": {
            "build": {
                "executor": "@nrwl/js:tsc",
                "options": {
                    "transformers": [
                        {
                            "name": "@automapper/classes/transformer-plugin",
                            "options": {
                                "modelFileNameSuffix": [".dto.ts"]
                            }
                        }
                    ]
                }
            }
        }
    }
}

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