Amplication plugins allow developers to extend and customize the code generation process to fit specific application requirements. By creating plugins, developers can modify generated code, introduce new functionalities, and streamline development workflows. This guide provides detailed explanations of essential plugin development techniques, along with practical code examples, to help you build effective Amplication plugins.

Reading Resource Information and Properties

Plugins often need to access resource metadata, such as entity definitions, properties, and settings. This information is essential for making informed modifications to the generated code. The context object provides access to all relevant resource data, enabling plugins to dynamically adapt based on project configurations.

Retrieving resource properties.
const resourceInfo = context.resourceInfo;

//Check if resource info is available - this should always be available
if (!resourceInfo) {
  throw new Error("Resource info is not available");
}

//Get the name of the resource
const resourceName = resourceInfo.name;

//resource catalog properties
const resourceCatalogProperties = (resourceInfo.properties || {}) as Record<
  string,
  string
>;

//Get the value of a property with the key DOMAIN
const domain = resourceCatalogProperties["DOMAIN"];

//Resource blueprint properties
const resourceSetting = (context.resourceSettings?.properties || {}) as Record<
  string,
  string
>;

//Get the value of a property with the key REGION
const region = resourceSetting["REGION"];

Copy the content of a folder and replace placeholders

Amplication plugins can generate files dynamically by leveraging static template files and replacing placeholders with project-specific values. This approach ensures consistency and reduces manual code duplication.

The functions importStaticFilesWithReplacements and importStaticFiles from context.utils allow developers to import entire directories of template files into the generated code. Placeholders within these files can be replaced with dynamic values, making it easy to customize the generated output.

context.utils.importStaticFiles
context.utils.importStaticFilesWithReplacements
Import Static Files With Replacements
import { IFile, blueprintTypes } from "@amplication/code-gen-types";
import { pascalCase } from "pascal-case";
import { resolve } from "path";
import { CodeBlock } from "@amplication/csharp-ast";

//convert the solution file to a AST so it can be used on other plugins as AST
export async function copyStaticFiles(
  context: blueprintTypes.DsgContext
): Promise<void> {
  const params = {} as Record<string, string>;

  //prepare some placeholders for the static files
  params.SERVICE_DISPLAY_NAME = context.resourceInfo?.name || "Service Name";
  params.SERVICE_NAME = pascalCase(params.SERVICE_DISPLAY_NAME);

  const resourceInfo = context.resourceInfo;

  if (!resourceInfo) {
    throw new Error("Resource info is not available");
  }

  //resource catalog properties
  const resourceCatalogProperties = (resourceInfo.properties || {}) as Record<
    string,
    string
  >;

  //Resource blueprint properties
  const resourceSetting = (context.resourceSettings?.properties ||
    {}) as Record<string, string>;

  //Add all catalog and resource settings to the placeholders
  //replacement is done on all files and paths in a format {{key}}
  const placeholders = {
    ...params,
    ...resourceCatalogProperties,
    ...resourceSetting,
  };

  //Add the service name to the placeholders
  //replacement is done on all files and paths as a simple string replacement
  const stringReplacements = {
    TemplateServiceName: params.SERVICE_NAME,
  };

  // read the static files from the static folder
  const staticPath = resolve(__dirname, "./static");
  const files = await context.utils.importStaticFilesWithReplacements(
    staticPath,
    ".",
    placeholders,
    stringReplacements
  );

  //convert all files to CodeBlock and add them to the context
  for (const file of files.getAll()) {
    const codeBlock: IFile<CodeBlock> = {
      path: file.path,
      code: new CodeBlock({
        code: file.code,
      }),
    };
    context.files.set(codeBlock);
  }
}

Resources in Amplication can be linked to other related resources. Accessing and managing these relationships is essential for generating code that takes into account dependencies between different resources.

For example, when generating deployment files for a Kubernetes cluster, a plugin might need to retrieve information from all related services to correctly configure the deployment specifications. By accessing related resources, plugins can ensure that generated files reflect the entire system architecture and include all necessary dependencies.

Retrieving related resources
import { blueprintTypes, DSGResourceData } from "@amplication/code-gen-types";

// Relation keys for the related resources based on the blueprint relation configuration
export enum EnumRelationKey {
  ProjectMetadata = "PROJECT_METADATA",
  Deployment = "DEPLOYMENT",
  Service = "SERVICE",
}

export function getRelatedResources(
  context: blueprintTypes.DsgContext,
  relationKey: EnumRelationKey,
): DSGResourceData[] {
  const relation = context.relations?.find(
    (relation) => relation.relationKey === relationKey,
  );

  if (!relation) {
    context.logger.error(
      `Related resource not found for relation key: ${relationKey}`,
    );
    return [];
  }

  const relatedResources = context.otherResources?.filter(
    (resource) =>
      resource.resourceInfo &&
      relation.relatedResources.includes(resource.resourceInfo.id),
  );

  if (!relatedResources) {
    context.logger.error(
      `Related resources not found for relation key: ${relationKey}`,
    );
    return [];
  }

  return relatedResources;
}

Reading Plugin Settings

Plugins can define configurable settings that influence their behavior. These settings allow users to customize how the plugin functions without modifying its core code.

By reading and applying plugin settings, developers can create more flexible and reusable plugins that adapt to different project requirements.

Read Plugin Settings
import { blueprintTypes } from "@amplication/code-gen-types";
import defaultSettings from "../.amplicationrc.json";

export const PLUGIN_ID = "my-plugin-id";

export interface Settings {
  param1: string;
  param2: string;
  array: string[];
}

export const getPluginSettings = (
  context: blueprintTypes.DsgContext,
): Settings => {
  const pluginInstallations = context.pluginInstallations;

  const plugin = pluginInstallations.find(
    (plugin) => plugin.pluginId === PLUGIN_ID,
  );

  const userSettings = plugin?.settings ?? {};

  const settings: Settings = {
    ...defaultSettings.settings,
    ...userSettings,
  };

  return settings;
};

Using the FileMap class to manage files

The FileMap class is the primary interface for managing files within a plugin. It allows plugin developers to add, edit, or remove files from the generated code. Any modifications made through FileMap directly impact the final generated output.

The FileMap is also a shared resource across different events and plugins. Files that were added in previous events or by other plugins are available in the map and can be read or altered. This enables collaboration between multiple plugins and ensures consistency in the generated project structure.

To make any changes to the generated code, plugin developers must use FileMap, ensuring a structured and maintainable approach to file management.

FileMap
FileMap<T> implements IFileMap<T>
Get a file from the context and update it

  const path = `./src/${serviceName}.sln`;

  //get the file from the context
  const slnFile = context.files.get(path);

  if (!slnFile) {
    throw new Error(`File ${path} not found`);
  }

  const slnFileContent = slnFile.code.toString();
  
  //parse the solution file into an AST
  const solution = new Solution();
  solution.parse(slnFileContent);

  slnFile.code = solution;

  //overwrite the existing file with the updated AST
  context.files.set(slnFile);