diff --git a/npm/ng-packs/packages/generators/src/generators/change-theme/generator.ts b/npm/ng-packs/packages/generators/src/generators/change-theme/generator.ts index 14d2e8e68e..d912d11e87 100644 --- a/npm/ng-packs/packages/generators/src/generators/change-theme/generator.ts +++ b/npm/ng-packs/packages/generators/src/generators/change-theme/generator.ts @@ -4,8 +4,10 @@ import { ChangeThemeGeneratorSchema } from './schema'; import { ThemeOptionsEnum } from './theme-options.enum'; export async function changeThemeGenerator(host: Tree, schema: ChangeThemeGeneratorSchema) { + const schematicPath = schema.localPath || '@abp/ng.schematics'; + const runAngularLibrarySchematic = wrapAngularDevkitSchematic( - '@abp/ng.schematics', + schema.localPath ? `${host.root}${schematicPath}` : schematicPath, 'change-theme', ); @@ -14,8 +16,10 @@ export async function changeThemeGenerator(host: Tree, schema: ChangeThemeGenera }); return () => { - const destTheme = Object.values(ThemeOptionsEnum).find((t, i) => i + 1 === schema.name); - console.log(`✅️ theme changed to ${destTheme}`); + const destTheme = Object.values(ThemeOptionsEnum).find( + (theme, index) => index + 1 === schema.name, + ); + console.log(`✅️ Switched to Theme ${destTheme}`); }; } diff --git a/npm/ng-packs/packages/generators/src/generators/change-theme/schema.d.ts b/npm/ng-packs/packages/generators/src/generators/change-theme/schema.d.ts index 9ee931564e..50caed37f0 100644 --- a/npm/ng-packs/packages/generators/src/generators/change-theme/schema.d.ts +++ b/npm/ng-packs/packages/generators/src/generators/change-theme/schema.d.ts @@ -1,4 +1,5 @@ export interface ChangeThemeGeneratorSchema { name: number; targetOption: string; + localPath?: string; } diff --git a/npm/ng-packs/packages/generators/src/generators/change-theme/schema.json b/npm/ng-packs/packages/generators/src/generators/change-theme/schema.json index fe356e00ae..e09d479f1b 100644 --- a/npm/ng-packs/packages/generators/src/generators/change-theme/schema.json +++ b/npm/ng-packs/packages/generators/src/generators/change-theme/schema.json @@ -31,6 +31,10 @@ "$source": "argv", "index": 1 } + }, + "localPath": { + "description": "If set value schematics will work on given path", + "type": "string" } }, "required": ["name", "targetProject"] diff --git a/npm/ng-packs/packages/schematics/src/commands/change-theme/index.ts b/npm/ng-packs/packages/schematics/src/commands/change-theme/index.ts index 959a3128db..438d6ea26a 100644 --- a/npm/ng-packs/packages/schematics/src/commands/change-theme/index.ts +++ b/npm/ng-packs/packages/schematics/src/commands/change-theme/index.ts @@ -1,22 +1,41 @@ -import { Rule, SchematicsException } from '@angular-devkit/schematics'; -import { isLibrary, updateWorkspace, WorkspaceDefinition } from '../../utils'; -import { allStyles, styleMap } from './style-map'; -import { ProjectDefinition } from '@angular-devkit/core/src/workspace'; import { JsonArray, JsonValue } from '@angular-devkit/core'; +import { Rule, SchematicsException, Tree, UpdateRecorder, chain } from '@angular-devkit/schematics'; +import { ProjectDefinition } from '@angular-devkit/core/src/workspace'; +import * as ts from 'typescript'; +import { ImportDefinition, allStyles, importMap, styleMap } from './style-map'; import { ChangeThemeOptions } from './model'; +import { + addImportToModule, + Change, + InsertChange, + isLibrary, + updateWorkspace, + WorkspaceDefinition, +} from '../../utils'; import { ThemeOptionsEnum } from './theme-options.enum'; +import { + //@ts-ignore + findNodes, + getDecoratorMetadata, + getMetadataField, +} from '../../utils/angular/ast-utils'; export default function (_options: ChangeThemeOptions): Rule { - return async () => { + return async (host: Tree) => { const targetThemeName = _options.name; const selectedProject = _options.targetProject; if (!targetThemeName) { throw new SchematicsException('The theme name does not selected'); } - return updateWorkspace(storedWorkspace => { - updateProjectStyle(selectedProject, storedWorkspace, targetThemeName); - }); + return chain([ + updateWorkspace(storedWorkspace => { + updateProjectStyle(selectedProject, storedWorkspace, targetThemeName); + }), + updateWorkspace(storedWorkspace => { + updateAppModule(host, selectedProject, storedWorkspace, targetThemeName); + }), + ]); }; } @@ -48,6 +67,132 @@ function updateProjectStyle( targetOption.styles = [...newStyles, ...sanitizedStyles] as JsonArray; } +function updateAppModule( + host: Tree, + projectName: string, + workspace: WorkspaceDefinition, + targetThemeName: ThemeOptionsEnum, +) { + const selectedTheme = importMap.get(targetThemeName); + if (!selectedTheme) { + throw new SchematicsException('The theme does not found'); + } + + const project = workspace.projects.get(projectName); + const appModulePath = `${project?.sourceRoot}/app/app.module.ts`; + + const text = host.read(appModulePath); + if (!text) { + throw new SchematicsException('The app module does not found'); + } + + const sourceText = text.toString('utf-8'); + const source = ts.createSourceFile( + appModulePath, + sourceText, + ts.ScriptTarget.Latest, + true, + ts.ScriptKind.TS, + ); + + const recorder = host.beginUpdate(appModulePath); + + const impMap = Array.from(importMap.values()) + ?.filter(f => f !== importMap.get(targetThemeName)) + .reduce((acc, val) => [...acc, ...val], []); + + removeImportPath(source, recorder, impMap); + removeImportFromNgModuleMetadata(source, recorder, impMap); + + insertImports(selectedTheme, source, appModulePath, recorder); + + host.commitUpdate(recorder); + return host; +} + +function insertImports( + selectedTheme: ImportDefinition[], + source: ts.SourceFile, + appModulePath: string, + recorder: UpdateRecorder, +) { + const changes: Change[] = []; + selectedTheme.map(({ importName, path }) => + changes.push(...addImportToModule(source, appModulePath, importName, path)), + ); + + if (changes?.length > 0) { + for (const change of changes) { + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } + } + } +} + +function removeImportPath( + source: ts.SourceFile, + recorder: UpdateRecorder, + arr: ImportDefinition[], +) { + const node = findNodes(source, ts.isImportDeclaration); + for (const importMp of arr) { + const importPath = node.find(f => f.getFullText().match(importMp.path)); + + if (!importPath) { + continue; + } + + /** + * We can add comment and sign as `removed` for the see what is removed + * + * recorder.insertLeft(importPath.getStart(), '//'); + */ + recorder.remove(importPath.getStart(), importPath.getWidth() + 1); + } +} + +function removeImportFromNgModuleMetadata( + source: ts.SourceFile, + recorder: UpdateRecorder, + arr: ImportDefinition[], +) { + /** + * Brings the @NgModule({...}) content + */ + const node = getDecoratorMetadata(source, 'NgModule', '@angular/core')[0] || {}; + if (!node) { + throw new SchematicsException('The app module does not found'); + } + + /** + * Select imports array in the @NgModule({...}) content + */ + const matchingProperties = getMetadataField(node as ts.ObjectLiteralExpression, 'imports'); + const assignment = matchingProperties[0] as ts.PropertyAssignment; + const assignmentInit = assignment.initializer as ts.ArrayLiteralExpression; + const elements = assignmentInit.elements; + + if (elements?.length < 0) { + return; + } + + for (const importMp of arr) { + const willRemoveModule = elements.find(f => f.getText().includes(importMp.importName)); + + if (!willRemoveModule) { + continue; + } + + /** + * We can add comment and sign as `removed` for the see what is removed + * + * recorder.insertLeft(foundModule.getStart(), '//'); + */ + recorder.remove(willRemoveModule.getStart(), willRemoveModule.getWidth() + 1); + } +} + export function getProjectTargetOptions( project: ProjectDefinition, buildTarget: string, diff --git a/npm/ng-packs/packages/schematics/src/commands/change-theme/style-map.ts b/npm/ng-packs/packages/schematics/src/commands/change-theme/style-map.ts index 91ef14a673..99b018703b 100644 --- a/npm/ng-packs/packages/schematics/src/commands/change-theme/style-map.ts +++ b/npm/ng-packs/packages/schematics/src/commands/change-theme/style-map.ts @@ -8,6 +8,11 @@ export type StyleDefinition = } | string; +export type ImportDefinition = { + path: string; + importName: string; +}; + export const styleMap = new Map(); styleMap.set(ThemeOptionsEnum.Basic, [ @@ -241,3 +246,49 @@ styleMap.set(ThemeOptionsEnum.LeptonXLite, [ ]); // the code written by Github co-pilot. thank go-pilot. You are the best sidekick. export const allStyles = Array.from(styleMap.values()).reduce((acc, val) => [...acc, ...val], []); + +export const importMap = new Map(); + +importMap.set(ThemeOptionsEnum.Basic, [ + { + path: '@abp/ng.theme.basic', + importName: 'ThemeBasicModule.forRoot()', + }, +]); + +importMap.set(ThemeOptionsEnum.Lepton, [ + { + path: '@volo/abp.ng.theme.lepton', + importName: 'ThemeLeptonModule.forRoot()', + }, +]); + +importMap.set(ThemeOptionsEnum.LeptonXLite, [ + { + path: '@abp/ng.theme.lepton-x', + importName: 'ThemeLeptonXModule.forRoot()', + }, + { + path: '@abp/ng.theme.lepton-x/layouts', + importName: 'SideMenuLayoutModule.forRoot()', + }, + { + path: '@abp/ng.theme.lepton-x/account', + importName: 'AccountLayoutModule.forRoot()', + }, +]); + +importMap.set(ThemeOptionsEnum.LeptonX, [ + { + path: '@volosoft/abp.ng.theme.lepton-x', + importName: 'ThemeLeptonXModule.forRoot()', + }, + { + path: '@volosoft/abp.ng.theme.lepton-x/layouts', + importName: 'SideMenuLayoutModule.forRoot()', + }, + { + path: '@volosoft/abp.ng.theme.lepton-x/account', + importName: 'AccountLayoutModule.forRoot()', + }, +]);