/** * Angular CLI Library Generator * * Creates Angular libraries and secondary entry points via the Angular CLI. * Handles version detection and uses the correct strategy for each version. */ import { execSync, type ExecSyncOptions } from 'node:child_process'; import * as path from 'node:path'; import * as fs from 'node:fs'; import type { LibraryExtractionOptions } from './library-extraction.orchestrator.js'; /** * Result of library creation. */ export interface LibraryCreationResult { libraryPath: string; angularVersion: string; strategy: 'ng-generate' | 'manual'; createdEntryPoints: string[]; } /** * Creates Angular libraries via the Angular CLI. * * Angular CLI 14+ supports `ng generate library /` for * secondary entry points. Older versions require manual file creation. */ export class AngularCliLibraryGenerator { /** * Create a new Angular library for the given options. */ async create( projectRoot: string, options: LibraryExtractionOptions, ): Promise { const outputFolder = options.outputFolder ?? 'projects/'; const libraryPath = path.join( projectRoot, outputFolder, options.libraryName, ); // Skip CLI if it already exists (idempotent) if (fs.existsSync(libraryPath)) { return libraryPath; } const execOptions: ExecSyncOptions = { cwd: projectRoot, stdio: 'pipe', }; try { const prefix = options.libraryPrefix.replace(/^@[^/]+\//, '') || 'lib'; execSync( `ng generate library ${options.libraryName} --prefix=${prefix} --skip-install`, execOptions, ); } catch (err) { throw new Error( `ng generate library failed: ${err instanceof Error ? err.message : String(err)}. ` + `Ensure Angular CLI is installed (npm install -g @angular/cli) and this is an Angular workspace.`, ); } // Detect actual output folder from angular.json const detectedPath = this.detectLibraryPath( projectRoot, options.libraryName, outputFolder, ); return detectedPath; } /** * Create secondary entry points for the library. * * Angular CLI 14+ supports `ng generate library /`. * For older versions, we create the entry point structure manually. */ async createSecondaryEntryPoints( libraryPath: string, options: LibraryExtractionOptions, ): Promise { if ( !options.secondaryEntryPoints || options.secondaryEntryPoints.length === 0 ) { return []; } const projectRoot = path.dirname(path.dirname(libraryPath)); const angularVersion = this.detectAngularVersion(projectRoot); const created: string[] = []; for (const entryPoint of options.secondaryEntryPoints) { const entryPointPath = path.join(libraryPath, 'src', 'lib', entryPoint); // Skip if already exists (idempotent) if (fs.existsSync(entryPointPath)) { created.push(entryPoint); continue; } if (this.supportsCliSecondaryEntryPoints(angularVersion)) { try { execSync( `ng generate library ${options.libraryName}/${entryPoint} --skip-install`, { cwd: projectRoot, stdio: 'pipe' }, ); created.push(entryPoint); } catch { // Fall back to manual creation this.createManualEntryPoint(libraryPath, entryPoint); created.push(entryPoint); } } else { this.createManualEntryPoint(libraryPath, entryPoint); created.push(entryPoint); } } return created; } // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- /** * Detect Angular CLI version from package.json. */ detectAngularVersion(projectRoot: string): string { try { const pkgPath = path.join(projectRoot, 'package.json'); const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) as { dependencies?: Record; devDependencies?: Record; }; const angularCore = pkg.dependencies?.['@angular/core'] ?? pkg.devDependencies?.['@angular/core'] ?? ''; // Strip semver prefix (^, ~) return angularCore.replace(/^[\^~]/, '').split('.')[0] ?? '0'; } catch { return '0'; } } /** * Angular CLI 14+ supports `ng generate library /`. */ private supportsCliSecondaryEntryPoints(version: string): boolean { const major = Number.parseInt(version, 10); return major >= 14; } /** * Create a secondary entry point manually for older Angular versions. * Structure: * projects//src/lib// * index.ts * ng-package.json */ private createManualEntryPoint( libraryPath: string, entryPoint: string, ): void { const entryDir = path.join(libraryPath, 'src', 'lib', entryPoint); fs.mkdirSync(entryDir, { recursive: true }); // index.ts const indexPath = path.join(entryDir, 'index.ts'); if (!fs.existsSync(indexPath)) { fs.writeFileSync(indexPath, `// ${entryPoint} secondary entry point\n`); } // ng-package.json const ngPackagePath = path.join(entryDir, 'ng-package.json'); if (!fs.existsSync(ngPackagePath)) { fs.writeFileSync( ngPackagePath, JSON.stringify( { $schema: 'ng-packagr/ng-package.schema.json', lib: { entryFile: 'index.ts' }, }, null, 2, ) + '\n', ); } } private detectLibraryPath( projectRoot: string, libraryName: string, defaultOutputFolder: string, ): string { // Try to read the path from angular.json try { const angularJsonPath = path.join(projectRoot, 'angular.json'); const angularJson = JSON.parse( fs.readFileSync(angularJsonPath, 'utf-8'), ) as { projects?: Record; }; const projectEntry = angularJson.projects?.[libraryName]; if (projectEntry?.root) { return path.join(projectRoot, projectEntry.root); } } catch { // angular.json not found or malformed } return path.join(projectRoot, defaultOutputFolder, libraryName); } }