Skip to content

NestJS dialect

@pbuilder/sdk/nestjs edits the .ts files of a NestJS project. Its handle has every op of @pbuilder/sdk/typescript plus four ops that import a class and register it in the file’s @Module decorator. All ops chain on one handle and the file is written once; see Modify for how a handle works.

import * as nestjs from "@pbuilder/sdk/nestjs";
export const run = async () => {
await nestjs
.find("src/app.module.ts")
.addModuleImport("UsersModule", "./users/users.module")
.addProvider("UsersService", "./users/users.service");
};
// before
@Module({ providers: [AppService] })
export class AppModule {}
// after
import { UsersModule } from "./users/users.module";
import { UsersService } from "./users/users.service";
@Module({ providers: [AppService, UsersService], imports: [UsersModule] })
export class AppModule {}

The handle carries every TypeScript dialect op: addImport, addDefaultImport, addNamespaceImport, addTypeImport, addSideEffectImport, addReExport, addExportAll, addFunction, addVariable, addClass, addArrayElement, addClassProperty, removeImport, removeExport and removeExportAll. An op the TypeScript dialect gains later is available here too.

Each op takes (name, from) and does two things:

  1. It adds import { name } from "from" with the same rules as the TypeScript addImport. An existing import of name from from is reused, and a name already bound some other way in the file rejects.
  2. It appends name to one array of the file’s @Module metadata. If @Module() has no argument, or the metadata has no such array, the op creates it. If the array already has an entry named name, the op leaves it as it is.
Op Appends to
addProvider(name, from) providers
addController(name, from) controllers
addModuleImport(name, from) imports, the other Nest modules this module imports
addExport(name, from) exports

addModuleImport has that name because addImport is already the inherited TypeScript op, which adds an import statement.

A new entry follows the layout of the list it joins. A one-line list stays on one line, a multi-line list gets a new line at the indentation of its last entry, and a trailing comma stays trailing. A comma inside a comment after the last entry ([A /* x, y */]) is not taken for a trailing comma. Everything outside the inserted text is byte-identical.

An op rejects, and the file is left unchanged, when:

  • the file has no class decorated with @Module, or more than one;
  • the @Module argument is not an object literal (for example @Module(METADATA));
  • the target property is not an array literal (for example providers: PROVIDERS);
  • name is not a valid identifier, or is already bound in the file some other way.

Like the TypeScript dialect, find(path) accepts any .ts path. A file without @Module fails only when one of these ops runs.

The @Module ops take a class name. For an entry such as ConfigModule.forRoot() or { provide: TOKEN, useValue: 1 }, use the inherited addArrayElement with a decorator root, and add the import yourself:

await nestjs
.find("src/app.module.ts")
.addImport("ConfigModule", "@nestjs/config")
.addArrayElement({ decorator: "Module", path: ["imports"] }, { expression: "ConfigModule.forRoot()" });
// before: @Module({ imports: [] })
// after: @Module({ imports: [ConfigModule.forRoot()] })

Use .modify(fn) for files the ops reject and for edits no op covers. The callback receives the live ts-morph SourceFile, and the module’s astLibrary export is ts-morph itself. This example makes the same edit as the addArrayElement call above, by hand:

import * as nestjs from "@pbuilder/sdk/nestjs";
await nestjs
.find("src/app.module.ts")
.addImport("ConfigModule", "@nestjs/config")
.modify((ast) => {
const imports = ast
.getClassOrThrow("AppModule")
.getDecoratorOrThrow("Module")
.getArguments()[0]!
.asKindOrThrow(nestjs.astLibrary.SyntaxKind.ObjectLiteralExpression)
.getPropertyOrThrow("imports")
.asKindOrThrow(nestjs.astLibrary.SyntaxKind.PropertyAssignment)
.getInitializerIfKindOrThrow(nestjs.astLibrary.SyntaxKind.ArrayLiteralExpression);
imports.addElement("ConfigModule.forRoot()");
});
  • Modify: the TypeScript and React dialects and how a handle works.
  • AST libraries: which library and version back each dialect.