Standalone migration plugin for the Angular Modernization Platform.
This package migrates Angular components, directives, and pipes from NgModule-based architecture to the standalone architecture introduced in Angular 14+. It automates the transformation including adding standalone: true, resolving template dependencies, and updating the imports array.
The plugin (StandalonePlugin, name @angular-modernizer/plugin-standalone) contributes one transform rule, StandaloneMigrationRule (ID standalone:migrate-to-standalone), a thin rule that delegates to StandaloneMigrationOrchestrator.
This plugin follows the API-driven plugin pattern. The orchestrator takes no constructor dependencies; everything comes in via TransformContext:
// Constructor injection - tight coupling, avoid this
class OldOrchestrator {
constructor(private project: Project) {}
}
// Context-driven - use this
class StandaloneMigrationOrchestrator {
constructor() {} // No injected dependencies
async run(context: TransformContext<PublicApi>): Promise<void> {
const { project, api } = context;
// All capabilities come from PublicApi
}
}
The orchestrator uses these tools from PublicApi:
analysis.symbolLocator): builds a project-wide symbol map of components, directives and pipes.analysis.templateAnalyzer): extracts all usages from templates (elements, directives, pipes) without filtering or heuristics.transformation.ngModuleManager): finds the NgModule declaring a component and reads its imports.transformation.importManager): adds import statements without introducing duplicates.It also creates a TemplateUsageAnalyzer from @angular-modernizer/api to keep only the NgModule imports the template uses.
*ngIf, *ngFor)templateUrl) from diskinterface StandaloneMigrationConfig {
/**
* Auto-add CommonModule when common directives are detected.
* Default: true
*/
autoAddCommonModule?: boolean;
/**
* Auto-add FormsModule when forms directives are detected.
* Default: true. Read, but not applied by the current migration.
*/
autoAddFormsModule?: boolean;
/**
* Preserve existing imports array if standalone is already true.
* Default: false (overwrite). Read, but not applied by the current migration.
*/
preserveExistingImports?: boolean;
}
The options are read from context.config.
import { Kernel, RealFileSystemAdapter } from '@angular-modernizer/core';
import { StandalonePlugin } from '@angular-modernizer/plugin-standalone';
import { createPublicApi } from '@angular-modernizer/api';
import { ContextFactory } from '@angular-modernizer/plugin-system';
import path from 'node:path';
import { glob } from 'glob';
const kernel = new Kernel({
tsConfigPath: path.join(projectPath, 'tsconfig.json'),
fileSystem: new RealFileSystemAdapter(),
plugins: [new StandalonePlugin()],
});
await kernel.initialize();
const project = kernel.getProject();
const api = createPublicApi(project);
const plugin = kernel.getPlugin('@angular-modernizer/plugin-standalone')!;
const transformRules = plugin.getTransformRules();
const rule = transformRules[0]; // StandaloneMigrationRule
const files = await glob('src/**/*.{ts,tsx}', {
ignore: ['**/node_modules/**', '**/dist/**', '**/*.spec.ts'],
});
for (const filePath of files) {
const sourceFile = project.addSourceFileAtPath(filePath);
const context = ContextFactory.createTransformContext({
sourceFile,
project,
api,
config: {
autoAddCommonModule: true,
},
});
const result = await rule.transform(context);
if (result.modified) {
await sourceFile.save();
console.log('Transformed:', filePath);
}
}
import { StandaloneMigrationOrchestrator } from '@angular-modernizer/plugin-standalone';
import { ContextFactory } from '@angular-modernizer/plugin-system';
import { createPublicApi } from '@angular-modernizer/api';
import { Project } from 'ts-morph';
const project = new Project();
const sourceFile = project.addSourceFileAtPath('my-component.ts');
const api = createPublicApi(project);
const orchestrator = new StandaloneMigrationOrchestrator();
const context = ContextFactory.createTransformContext({
sourceFile,
project,
api,
config: {},
});
await orchestrator.run(context);
packages/plugin-standalone/
src/
orchestrators/
standalone-migration-orchestrator.ts
rules/
standalone-migration-rule.ts
standalone-plugin.ts
index.ts
__tests__/
standalone-migration-orchestrator.test.ts
standalone-migration.integration.test.ts
standalone-plugin.test.ts
orchestrator-selector-map.test.ts
integration/
fixtures/
package.json
tsconfig.json
jest.config.js
README.md
pnpm --filter @angular-modernizer/plugin-standalone test
pnpm --filter @angular-modernizer/plugin-standalone test -- --watch
pnpm --filter @angular-modernizer/plugin-standalone build
Test coverage validates:
The orchestrator builds a Map<selector, className> lazily on first use and reuses it for all template analyses within a run. Custom decorators such as @RegisteredComponent are included via the two-tier NgModuleManager scan.
The migration only copies NgModule imports that are actually used in the component template, via TemplateUsageAnalyzer (Tier 1: regex ~2ms, Tier 2: Angular Compiler ~15ms, Tier 3: conservative include-all fallback). Do not copy NgModule imports directly.
When updating existing @Component properties, use the remove-and-add pattern to avoid edge cases with replaceWithText():
const prop = objLiteral.getProperty('standalone');
if (prop) {
prop.remove();
}
objLiteral.addPropertyAssignment({ name: 'standalone', initializer: 'true' });
@angular-modernizer/api - Public API with analysis and transformation tools@angular-modernizer/core - Kernel and infrastructure@angular-modernizer/plugin-system - Plugin contracts and contextsts-morph - TypeScript AST manipulation@angular/compiler, lru-cache
Standalone migration plugin for the Angular Modernizer.
Migrates Angular components, directives and pipes to standalone. StandalonePlugin is the entry point for the kernel and registers StandaloneMigrationRule (
standalone:migrate-to-standalone), which delegates to StandaloneMigrationOrchestrator. StandaloneMigrationConfig lists the rule options.