ng-upgrade-orchestrator
Version:
Enterprise-grade Angular Multi-Version Upgrade Orchestrator with automatic npm installation, comprehensive dependency management, and seamless integration of all 9 official Angular migrations. Safely migrate Angular applications across multiple major vers
604 lines • 25.2 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.IntelligentMergeEngine = void 0;
const fs = __importStar(require("fs-extra"));
const path = __importStar(require("path"));
// Simple deep merge utility - fallback for ts-deepmerge
function deepmerge(target, source, customMergers) {
if (typeof source !== 'object' || source === null) {
return source;
}
if (typeof target !== 'object' || target === null) {
return source;
}
const result = { ...target };
for (const key in source) {
if (source.hasOwnProperty(key)) {
if (customMergers && customMergers[key]) {
result[key] = customMergers[key](target[key], source[key]);
}
else if (typeof source[key] === 'object' && source[key] !== null && !Array.isArray(source[key])) {
result[key] = deepmerge(target[key] || {}, source[key]);
}
else {
result[key] = source[key];
}
}
}
return result;
}
const ts_morph_1 = require("ts-morph");
/**
* Intelligent merge engine that combines ts-deepmerge for configurations
* and custom logic for TypeScript code merging
*/
class IntelligentMergeEngine {
project;
progressReporter;
constructor(projectPath, progressReporter) {
const tsConfigPath = path.join(projectPath, 'tsconfig.json');
const tsConfigExists = fs.existsSync(tsConfigPath);
this.project = new ts_morph_1.Project({
...(tsConfigExists ? { tsConfigFilePath: tsConfigPath } : {
compilerOptions: {
target: ts_morph_1.ScriptTarget.ES2020,
module: ts_morph_1.ModuleKind.ES2020,
moduleResolution: ts_morph_1.ModuleResolutionKind.NodeNext,
strict: false,
esModuleInterop: true,
skipLibCheck: true
}
})
});
this.progressReporter = progressReporter;
}
/**
* Merge package.json configurations intelligently
*/
async mergePackageJson(packageJsonPath, migrationUpdates, options = this.getDefaultOptions()) {
try {
const userPackageJson = await fs.readJson(packageJsonPath);
const backup = options.createBackups ? await this.createBackup(packageJsonPath) : undefined;
// Use ts-deepmerge for deep merging with custom merge functions
const mergedPackageJson = deepmerge(userPackageJson, migrationUpdates, {
// Custom merge for dependencies - preserve user versions unless migration requires specific version
dependencies: (userDeps, migrationDeps) => {
return this.mergeDependencies(userDeps, migrationDeps, options);
},
devDependencies: (userDeps, migrationDeps) => {
return this.mergeDependencies(userDeps, migrationDeps, options);
},
scripts: (userScripts, migrationScripts) => {
return this.mergeScripts(userScripts, migrationScripts, options);
}
});
await fs.writeJson(packageJsonPath, mergedPackageJson, { spaces: 2 });
this.progressReporter?.success('✓ package.json merged successfully');
return {
success: true,
conflicts: [],
warnings: [],
merged: true,
backupPath: backup
};
}
catch (error) {
return {
success: false,
conflicts: [],
warnings: [`Failed to merge package.json: ${error instanceof Error ? error.message : String(error)}`],
merged: false
};
}
}
/**
* Merge angular.json configurations
*/
async mergeAngularJson(angularJsonPath, migrationUpdates, options = this.getDefaultOptions()) {
try {
const userAngularJson = await fs.readJson(angularJsonPath);
const backup = options.createBackups ? await this.createBackup(angularJsonPath) : undefined;
// Intelligent merge for angular.json with special handling for architect configs
const mergedAngularJson = deepmerge(userAngularJson, migrationUpdates, {
// Custom merge for architect configurations
projects: (userProjects, migrationProjects) => {
return this.mergeProjects(userProjects, migrationProjects, options);
}
});
await fs.writeJson(angularJsonPath, mergedAngularJson, { spaces: 2 });
this.progressReporter?.success('✓ angular.json merged successfully');
return {
success: true,
conflicts: [],
warnings: [],
merged: true,
backupPath: backup
};
}
catch (error) {
return {
success: false,
conflicts: [],
warnings: [`Failed to merge angular.json: ${error instanceof Error ? error.message : String(error)}`],
merged: false
};
}
}
/**
* Merge TypeScript configuration files (tsconfig.json)
*/
async mergeTsConfig(tsConfigPath, migrationUpdates, options = this.getDefaultOptions()) {
try {
const userTsConfig = await fs.readJson(tsConfigPath);
const backup = options.createBackups ? await this.createBackup(tsConfigPath) : undefined;
// Deep merge with special handling for compiler options
const mergedTsConfig = deepmerge(userTsConfig, migrationUpdates, {
compilerOptions: (userOptions, migrationOptions) => {
return this.mergeCompilerOptions(userOptions, migrationOptions, options);
}
});
await fs.writeJson(tsConfigPath, mergedTsConfig, { spaces: 2 });
this.progressReporter?.success('✓ tsconfig.json merged successfully');
return {
success: true,
conflicts: [],
warnings: [],
merged: true,
backupPath: backup
};
}
catch (error) {
return {
success: false,
conflicts: [],
warnings: [`Failed to merge tsconfig.json: ${error instanceof Error ? error.message : String(error)}`],
merged: false
};
}
}
/**
* Merge TypeScript source files with AST-based merging
*/
async mergeTypeScriptFile(filePath, migrationChanges, options = this.getDefaultOptions()) {
try {
const backup = options.createBackups ? await this.createBackup(filePath) : undefined;
const sourceFile = this.project.addSourceFileAtPath(filePath);
const conflicts = [];
// Merge imports
if (migrationChanges.imports) {
this.mergeImports(sourceFile, migrationChanges.imports, conflicts, options);
}
// Merge class decorators
if (migrationChanges.decorators) {
this.mergeDecorators(sourceFile, migrationChanges.decorators, conflicts, options);
}
// Merge class members (properties, methods)
if (migrationChanges.classMembers) {
this.mergeClassMembers(sourceFile, migrationChanges.classMembers, conflicts, options);
}
// Merge providers and configurations
if (migrationChanges.providers) {
this.mergeProviders(sourceFile, migrationChanges.providers, conflicts, options);
}
await sourceFile.save();
this.progressReporter?.success(`✓ ${path.basename(filePath)} merged successfully`);
return {
success: true,
conflicts,
warnings: [],
merged: true,
backupPath: backup
};
}
catch (error) {
return {
success: false,
conflicts: [],
warnings: [`Failed to merge ${filePath}: ${error instanceof Error ? error.message : String(error)}`],
merged: false
};
}
}
/**
* Merge template files (.html)
*/
async mergeTemplateFile(templatePath, migrationChanges, options = this.getDefaultOptions()) {
try {
const userTemplate = await fs.readFile(templatePath, 'utf-8');
const backup = options.createBackups ? await this.createBackup(templatePath) : undefined;
let mergedTemplate = userTemplate;
const conflicts = [];
// Merge control flow changes (*ngIf to @if)
if (migrationChanges.controlFlow) {
const result = this.mergeControlFlow(mergedTemplate, migrationChanges.controlFlow, options);
mergedTemplate = result.template;
conflicts.push(...result.conflicts);
}
// Merge directive changes
if (migrationChanges.directives) {
const result = this.mergeDirectives(mergedTemplate, migrationChanges.directives, options);
mergedTemplate = result.template;
conflicts.push(...result.conflicts);
}
await fs.writeFile(templatePath, mergedTemplate);
this.progressReporter?.success(`✓ ${path.basename(templatePath)} template merged successfully`);
return {
success: true,
conflicts,
warnings: [],
merged: true,
backupPath: backup
};
}
catch (error) {
return {
success: false,
conflicts: [],
warnings: [`Failed to merge template ${templatePath}: ${error instanceof Error ? error.message : String(error)}`],
merged: false
};
}
}
/**
* Custom dependency merging logic
*/
mergeDependencies(userDeps, migrationDeps, options) {
if (!userDeps && !migrationDeps)
return {};
if (!userDeps)
return migrationDeps;
if (!migrationDeps)
return userDeps;
const merged = { ...userDeps };
Object.entries(migrationDeps).forEach(([pkg, version]) => {
if (options.preferUserConfiguration && merged[pkg]) {
// Keep user version unless it's a critical Angular dependency
if (this.isCriticalAngularDependency(pkg)) {
merged[pkg] = version;
}
}
else {
merged[pkg] = version;
}
});
return merged;
}
/**
* Merge npm scripts intelligently
*/
mergeScripts(userScripts, migrationScripts, options) {
if (!userScripts && !migrationScripts)
return {};
if (!userScripts)
return migrationScripts;
if (!migrationScripts)
return userScripts;
const merged = { ...userScripts };
Object.entries(migrationScripts).forEach(([scriptName, scriptValue]) => {
if (options.preferUserConfiguration && merged[scriptName]) {
// Keep user script unless it's a build/test script that needs updating
if (this.isCriticalScript(scriptName)) {
merged[scriptName] = scriptValue;
}
}
else {
merged[scriptName] = scriptValue;
}
});
return merged;
}
/**
* Merge Angular projects configuration
*/
mergeProjects(userProjects, migrationProjects, options) {
if (!userProjects)
return migrationProjects;
if (!migrationProjects)
return userProjects;
const merged = { ...userProjects };
Object.entries(migrationProjects).forEach(([projectName, projectConfig]) => {
if (merged[projectName]) {
// Deep merge project configurations
merged[projectName] = deepmerge(merged[projectName], projectConfig);
}
else {
merged[projectName] = projectConfig;
}
});
return merged;
}
/**
* Merge compiler options with special handling
*/
mergeCompilerOptions(userOptions, migrationOptions, options) {
if (!userOptions)
return migrationOptions;
if (!migrationOptions)
return userOptions;
const merged = { ...userOptions };
// Special handling for certain compiler options
Object.entries(migrationOptions).forEach(([option, value]) => {
if (this.isStrictOption(option) && options.preferUserConfiguration) {
// Don't override user's strict settings unless they explicitly want migration
if (options.mergeStrategy === 'aggressive') {
merged[option] = value;
}
}
else {
merged[option] = value;
}
});
return merged;
}
/**
* Merge imports in TypeScript files
*/
mergeImports(sourceFile, newImports, conflicts, options) {
newImports.forEach(newImport => {
const existingImport = sourceFile.getImportDeclarations()
.find(imp => imp.getModuleSpecifierValue() === newImport.moduleSpecifier);
if (existingImport) {
// Merge named imports
const existingNamedImports = existingImport.getNamedImports().map(ni => ni.getName());
const newNamedImports = newImport.namedImports || [];
const allImports = [...new Set([...existingNamedImports, ...newNamedImports])];
if (allImports.length > existingNamedImports.length) {
// Update existing import with merged imports
existingImport.removeNamedImports();
existingImport.addNamedImports(allImports);
}
}
else {
// Add new import
sourceFile.addImportDeclaration({
moduleSpecifier: newImport.moduleSpecifier,
namedImports: newImport.namedImports
});
}
});
}
/**
* Merge decorators on classes
*/
mergeDecorators(sourceFile, decoratorChanges, conflicts, options) {
decoratorChanges.forEach(change => {
const classDecl = sourceFile.getClass(change.className);
if (!classDecl)
return;
const existingDecorator = classDecl.getDecorator(change.decoratorName);
if (existingDecorator) {
// Merge decorator arguments
if (change.mergeArguments && options.preferUserConfiguration) {
const existingArgs = existingDecorator.getArguments();
if (existingArgs.length > 0) {
// Parse and merge object arguments
try {
const existingConfig = this.parseDecoratorArgument(existingArgs[0].getText());
const newConfig = change.newConfiguration;
const mergedConfig = deepmerge(existingConfig, newConfig);
existingDecorator.remove();
classDecl.addDecorator({
name: change.decoratorName,
arguments: [JSON.stringify(mergedConfig, null, 2)]
});
}
catch (error) {
// If parsing fails, create conflict
conflicts.push({
file: sourceFile.getFilePath(),
type: 'code',
section: `${change.className}.${change.decoratorName}`,
userValue: existingDecorator.getText(),
migrationValue: change.newConfiguration,
resolution: options.conflictResolution === 'user' ? 'user' : 'migration'
});
}
}
}
}
else {
// Add new decorator
classDecl.addDecorator({
name: change.decoratorName,
arguments: [JSON.stringify(change.newConfiguration, null, 2)]
});
}
});
}
/**
* Merge class members (properties and methods)
*/
mergeClassMembers(sourceFile, memberChanges, conflicts, options) {
memberChanges.forEach(change => {
const classDecl = sourceFile.getClass(change.className);
if (!classDecl)
return;
if (change.type === 'property') {
const existingProperty = classDecl.getProperty(change.name);
if (!existingProperty && !options.preferUserConfiguration) {
classDecl.addProperty(change.propertyStructure);
}
}
else if (change.type === 'method') {
const existingMethod = classDecl.getMethod(change.name);
if (existingMethod && options.preserveUserCode) {
// Create conflict - user has custom implementation
conflicts.push({
file: sourceFile.getFilePath(),
type: 'code',
section: `${change.className}.${change.name}()`,
userValue: existingMethod.getText(),
migrationValue: change.methodStructure,
resolution: 'user' // Preserve user code by default
});
}
else if (!existingMethod) {
classDecl.addMethod(change.methodStructure);
}
}
});
}
/**
* Merge providers and dependency injection configurations
*/
mergeProviders(sourceFile, providerChanges, conflicts, options) {
// Implementation for merging providers in main.ts, app.config.ts, etc.
providerChanges.forEach(change => {
// Find bootstrapApplication or similar provider arrays
const callExpressions = sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression);
callExpressions.forEach(callExpr => {
if (callExpr.getExpression().getText() === 'bootstrapApplication') {
// Merge providers in bootstrapApplication
const args = callExpr.getArguments();
if (args.length > 1) {
// Second argument contains providers
this.mergeProviderArray(args[1], change.providers, options);
}
}
});
});
}
/**
* Merge control flow syntax in templates
*/
mergeControlFlow(template, controlFlowChanges, options) {
let mergedTemplate = template;
const conflicts = [];
// Only migrate simple cases if user prefers to keep their code
if (options.preserveUserCode) {
// Check for complex *ngIf expressions that should be preserved
const complexNgIfPattern = /\*ngIf="[^"]*(\|\||&&|\?)[^"]*"/g;
const complexMatches = template.match(complexNgIfPattern);
if (complexMatches) {
// Create conflicts for complex expressions
complexMatches.forEach(match => {
conflicts.push({
file: 'template',
type: 'template',
section: 'control-flow',
userValue: match,
migrationValue: this.convertToNewControlFlow(match),
resolution: 'user' // Preserve complex user logic
});
});
}
// Only migrate simple cases
const simpleNgIfPattern = /\*ngIf="([a-zA-Z_$][a-zA-Z0-9_$]*)"/g;
mergedTemplate = mergedTemplate.replace(simpleNgIfPattern, '@if ($1) {');
}
return { template: mergedTemplate, conflicts };
}
/**
* Merge directive changes in templates
*/
mergeDirectives(template, directiveChanges, options) {
let mergedTemplate = template;
const conflicts = [];
// Apply directive changes while preserving user customizations
directiveChanges.forEach((change) => {
if (options.preserveUserCode && this.hasCustomDirectiveUsage(template, change.directive)) {
conflicts.push({
file: 'template',
type: 'template',
section: change.directive,
userValue: this.extractDirectiveUsage(template, change.directive),
migrationValue: change.newUsage,
resolution: 'user'
});
}
else {
mergedTemplate = mergedTemplate.replace(change.pattern, change.replacement);
}
});
return { template: mergedTemplate, conflicts };
}
// Helper methods
async createBackup(filePath) {
const backupPath = `${filePath}.backup-${Date.now()}`;
await fs.copy(filePath, backupPath);
return backupPath;
}
isCriticalAngularDependency(packageName) {
return packageName.startsWith('@angular/') ||
packageName === 'typescript' ||
packageName === 'zone.js';
}
isCriticalScript(scriptName) {
return ['build', 'test', 'lint', 'start'].includes(scriptName);
}
isStrictOption(option) {
return option.includes('strict') || option === 'noImplicitAny' || option === 'noImplicitReturns';
}
parseDecoratorArgument(argText) {
try {
return JSON.parse(argText);
}
catch {
return {};
}
}
mergeProviderArray(providerArg, newProviders, options) {
// Implementation for merging provider arrays
// This would involve AST manipulation to add new providers while preserving existing ones
}
convertToNewControlFlow(oldSyntax) {
// Convert *ngIf="complex expression" to @if (complex expression) { }
const match = oldSyntax.match(/\*ngIf="([^"]+)"/);
return match ? `@if (${match[1]}) { }` : oldSyntax;
}
hasCustomDirectiveUsage(template, directive) {
// Check if directive has custom attributes or complex usage
const pattern = new RegExp(`${directive}[^>]*\\[[^\\]]+\\]`, 'g');
return pattern.test(template);
}
extractDirectiveUsage(template, directive) {
const pattern = new RegExp(`<[^>]*${directive}[^>]*>`, 'g');
const matches = template.match(pattern);
return matches ? matches[0] : '';
}
getDefaultOptions() {
return {
preserveUserCode: true,
preserveComments: true,
preferUserConfiguration: true,
createBackups: true,
mergeStrategy: 'conservative',
conflictResolution: 'user'
};
}
}
exports.IntelligentMergeEngine = IntelligentMergeEngine;
//# sourceMappingURL=IntelligentMergeEngine.js.map