Skip to content

Refactoring schematics for standalone migration #22645

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 27 commits into
base: rel-9.2
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
61eb7c7
Refactor schematics:create-lib command to align with the standalone t…
erdemcaygor Apr 14, 2025
ebaf0be
schematics standalone utilities
erdemcaygor Apr 15, 2025
a9e92e2
added new template definition
erdemcaygor Apr 15, 2025
cc99f14
schematics standalone updates
erdemcaygor Apr 15, 2025
45f0d38
schematics standalone updates
erdemcaygor Apr 15, 2025
a95283e
refactoring
erdemcaygor Apr 16, 2025
e05175b
refactoring
erdemcaygor Apr 16, 2025
9e36de5
refactoring
erdemcaygor Apr 16, 2025
ef98505
theme remove from import on standalone app
erdemcaygor Apr 16, 2025
26b6c84
change theme command refactoring
erdemcaygor Apr 16, 2025
7a9394a
change theme command refactoring
erdemcaygor Apr 16, 2025
331c2be
format file func added
erdemcaygor Apr 16, 2025
c7718ec
create-lib schema updated
erdemcaygor Apr 17, 2025
56e7b83
refactoring
erdemcaygor Apr 17, 2025
80e7f73
custom standalone util functions added
erdemcaygor Apr 17, 2025
b8df00a
refactoring
erdemcaygor Apr 17, 2025
c908989
refactoring
erdemcaygor Apr 18, 2025
4f0d460
refactoring
erdemcaygor Apr 18, 2025
9d332e4
refactoring
erdemcaygor Apr 21, 2025
4331675
refactoring
erdemcaygor Apr 21, 2025
8612fb4
clean commas rule added
erdemcaygor Apr 21, 2025
f282fe3
refactoring
erdemcaygor Apr 21, 2025
d4cc6dc
docs update
erdemcaygor Apr 21, 2025
4ea2855
template type enum added
erdemcaygor Apr 21, 2025
d0f3090
standalone template component name updated
erdemcaygor Apr 22, 2025
65063e5
standalone router variable updated
erdemcaygor Apr 22, 2025
b8996b9
comment removed
erdemcaygor Apr 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
233 changes: 152 additions & 81 deletions npm/ng-packs/packages/schematics/src/commands/change-theme/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,20 @@ import * as ts from 'typescript';
import { allStyles, importMap, styleMap } from './style-map';
import { ChangeThemeOptions } from './model';
import {
Change,
createDefaultPath,
InsertChange,
addRootImport,
addRootProvider,
getAppModulePath,
isLibrary,
isStandaloneApp,
updateWorkspace,
WorkspaceDefinition,
getAppConfigPath,
cleanEmptyExprFromModule,
cleanEmptyExprFromProviders,
} from '../../utils';
import { ThemeOptionsEnum } from './theme-options.enum';
import {
addImportToModule,
addProviderToModule,
findNodes,
getDecoratorMetadata,
getMetadataField,
} from '../../utils/angular/ast-utils';
import { findNodes, getDecoratorMetadata, getMetadataField } from '../../utils/angular/ast-utils';
import { getMainFilePath } from '../../utils/angular/standalone/util';

export default function (_options: ChangeThemeOptions): Rule {
return async () => {
Expand Down Expand Up @@ -68,46 +67,59 @@ function updateProjectStyle(

function updateAppModule(selectedProject: string, targetThemeName: ThemeOptionsEnum): Rule {
return async (host: Tree) => {
const appModulePath = (await createDefaultPath(host, selectedProject)) + '/app.module.ts';
const mainFilePath = await getMainFilePath(host, selectedProject);
const isStandalone = isStandaloneApp(host, mainFilePath);
const appModulePath = isStandalone
? getAppConfigPath(host, mainFilePath)
: getAppModulePath(host, mainFilePath);

return chain([
removeImportPath(appModulePath, targetThemeName),
removeImportFromNgModuleMetadata(appModulePath, targetThemeName),
removeProviderFromNgModuleMetadata(appModulePath, targetThemeName),
insertImports(appModulePath, targetThemeName),
insertProviders(appModulePath, targetThemeName),
...(!isStandalone ? [removeImportFromNgModuleMetadata(appModulePath, targetThemeName)] : []),
isStandalone
? removeImportsFromStandaloneProviders(appModulePath, targetThemeName)
: removeProviderFromNgModuleMetadata(appModulePath, targetThemeName),
insertImports(selectedProject, targetThemeName),
insertProviders(selectedProject, targetThemeName),
formatFile(appModulePath),
cleanEmptyExpressions(appModulePath, isStandalone),
]);
};
}

export function removeImportPath(appModulePath: string, selectedTheme: ThemeOptionsEnum): Rule {
export function removeImportPath(filePath: string, selectedTheme: ThemeOptionsEnum): Rule {
return (host: Tree) => {
const recorder = host.beginUpdate(appModulePath);
const source = createSourceFile(host, appModulePath);
const buffer = host.read(filePath);
if (!buffer) return host;

const sourceText = buffer.toString('utf-8');
const source = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true);
const recorder = host.beginUpdate(filePath);

const impMap = getImportPaths(selectedTheme, true);

const nodes = findNodes(source, ts.isImportDeclaration);

const filteredNodes = nodes.filter(node =>
impMap.some(({ path, importName }) => {
const sourceModule = node.getFullText();
const moduleName = importName.split('.')[0];
const filteredNodes = nodes.filter(node => {
const importPath = (node.moduleSpecifier as ts.StringLiteral).text;
const namedBindings = node.importClause?.namedBindings;

if (path && sourceModule.match(path)) {
return true;
}
return impMap.some(({ path, importName }) => {
const symbol = importName.split('.')[0];
const matchesPath = !!path && importPath === path;

return !!(moduleName && sourceModule.match(moduleName));
}),
);
const matchesSymbol =
!!namedBindings &&
ts.isNamedImports(namedBindings) &&
namedBindings.elements.some(e => e.name.text === symbol);

if (filteredNodes?.length < 1) {
return;
}
return matchesPath || matchesSymbol;
});
});

filteredNodes.map(importPath =>
recorder.remove(importPath.getStart(), importPath.getWidth() + 1),
);
for (const node of filteredNodes) {
recorder.remove(node.getStart(), node.getWidth());
}

host.commitUpdate(recorder);
return host;
Expand Down Expand Up @@ -153,6 +165,53 @@ export function removeImportFromNgModuleMetadata(
};
}

export function removeImportsFromStandaloneProviders(
mainPath: string,
selectedTheme: ThemeOptionsEnum,
): Rule {
return (host: Tree) => {
const buffer = host.read(mainPath);
if (!buffer) return host;

const sourceText = buffer.toString('utf-8');
const source = ts.createSourceFile(mainPath, sourceText, ts.ScriptTarget.Latest, true);
const recorder = host.beginUpdate(mainPath);

const impMap = getImportPaths(selectedTheme, true);
const callExpressions = findNodes(source, ts.isCallExpression);

for (const expr of callExpressions) {
const exprText = expr.getText();

const match = impMap.find(({ importName, provider }) => {
const moduleSymbol = importName?.split('.')[0];
return (
(moduleSymbol && exprText.includes(moduleSymbol)) ||
(provider && exprText.includes(provider))
);
});

if (match) {
const start = expr.getFullStart();
const end = expr.getEnd();
const nextChar = sourceText.slice(end, end + 1);
const prevChar = sourceText.slice(start - 1, start);

if (nextChar === ',') {
recorder.remove(start, end - start + 1);
} else if (prevChar === ',') {
recorder.remove(start - 1, end - start + 1);
} else {
recorder.remove(start, end - start);
}
}
}

host.commitUpdate(recorder);
return host;
};
}

export function removeProviderFromNgModuleMetadata(
appModulePath: string,
selectedTheme: ThemeOptionsEnum,
Expand Down Expand Up @@ -192,61 +251,36 @@ export function removeProviderFromNgModuleMetadata(
};
}

export function insertImports(appModulePath: string, selectedTheme: ThemeOptionsEnum): Rule {
return (host: Tree) => {
const recorder = host.beginUpdate(appModulePath);
const source = createSourceFile(host, appModulePath);
export function insertImports(projectName: string, selectedTheme: ThemeOptionsEnum): Rule {
return addRootImport(projectName, code => {
const selected = importMap.get(selectedTheme);
if (!selected || selected.length === 0) return code.code``;

if (!selected) {
return host;
}

const changes: Change[] = [];

selected.map(({ importName, path }) =>
changes.push(...addImportToModule(source, appModulePath, importName, path)),
);
const expressions: string[] = [];

if (changes.length > 0) {
for (const change of changes) {
if (change instanceof InsertChange) {
recorder.insertLeft(change.order, change.toAdd);
}
}
for (const { importName, path, expression } of selected) {
const imported = code.external(importName, path);
expressions.push(expression ?? imported); // default fallback
}
host.commitUpdate(recorder);
return host;
};

return code.code`${expressions.join(',\n')}`;
});
}

export function insertProviders(appModulePath: string, selectedTheme: ThemeOptionsEnum): Rule {
return (host: Tree) => {
const recorder = host.beginUpdate(appModulePath);
const source = createSourceFile(host, appModulePath);
export function insertProviders(projectName: string, selectedTheme: ThemeOptionsEnum): Rule {
return addRootProvider(projectName, code => {
const selected = importMap.get(selectedTheme);
if (!selected || selected.length === 0) return code.code``;

if (!selected) {
return host;
}

const changes: Change[] = [];

selected.map(({ path, provider }) => {
if (provider) {
changes.push(...addProviderToModule(source, appModulePath, provider + '()', path));
}
});

for (const change of changes) {
if (change instanceof InsertChange) {
recorder.insertLeft(change.order, change.toAdd);
}
}
const providers = selected
.filter(s => !!s.provider)
.map(({ provider, path }) => {
const symbol = code.external(provider!, path);
return `${symbol}()`;
});

host.commitUpdate(recorder);
return host;
};
return code.code`${providers}`;
});
}

export function createSourceFile(host: Tree, appModulePath: string): ts.SourceFile {
Expand All @@ -271,7 +305,7 @@ export function createSourceFile(host: Tree, appModulePath: string): ts.SourceFi
* @param selectedTheme The selected theme
* @param getAll If true, returns all import paths
*/
export function getImportPaths(selectedTheme: ThemeOptionsEnum, getAll: boolean = false) {
export function getImportPaths(selectedTheme: ThemeOptionsEnum, getAll = false) {
if (getAll) {
return Array.from(importMap.values()).reduce((acc, val) => [...acc, ...val], []);
}
Expand Down Expand Up @@ -316,3 +350,40 @@ export const styleCompareFn = (item1: string | object, item2: string | object) =

return o1.bundleName && o2.bundleName && o1.bundleName == o2.bundleName;
};

export const formatFile = (filePath: string): Rule => {
return (tree: Tree) => {
const buffer = tree.read(filePath);
if (!buffer) return tree;

const source = ts.createSourceFile(filePath, buffer.toString(), ts.ScriptTarget.Latest, true);
const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed });
const formatted = printer.printFile(source);

tree.overwrite(filePath, formatted);
return tree;
};
};

export function cleanEmptyExpressions(modulePath: string, isStandalone: boolean): Rule {
return (host: Tree) => {
const buffer = host.read(modulePath);
if (!buffer) throw new SchematicsException(`Cannot read ${modulePath}`);

const source = ts.createSourceFile(
modulePath,
buffer.toString('utf-8'),
ts.ScriptTarget.Latest,
true,
);
const recorder = host.beginUpdate(modulePath);

if (isStandalone) {
cleanEmptyExprFromProviders(source, recorder);
} else {
cleanEmptyExprFromModule(source, recorder);
}
host.commitUpdate(recorder);
return host;
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export type ImportDefinition = {
path: string;
importName: string;
provider?: string;
expression?: string;
};

export const styleMap = new Map<ThemeOptionsEnum, StyleDefinition[]>();
Expand Down Expand Up @@ -284,25 +285,30 @@ importMap.set(ThemeOptionsEnum.Lepton, [
importMap.set(ThemeOptionsEnum.LeptonXLite, [
{
path: '@abp/ng.theme.lepton-x',
importName: 'ThemeLeptonXModule.forRoot()',
importName: 'ThemeLeptonXModule',
expression: 'ThemeLeptonXModule.forRoot()',
},
{
path: '@abp/ng.theme.lepton-x/layouts',
importName: 'SideMenuLayoutModule.forRoot()',
importName: 'SideMenuLayoutModule',
expression: 'SideMenuLayoutModule.forRoot()',
},
{
path: '@abp/ng.theme.lepton-x/account',
importName: 'AccountLayoutModule.forRoot()',
importName: 'AccountLayoutModule',
expression: 'AccountLayoutModule.forRoot()',
},
]);

importMap.set(ThemeOptionsEnum.LeptonX, [
{
path: '@volosoft/abp.ng.theme.lepton-x',
importName: 'ThemeLeptonXModule.forRoot()',
importName: 'ThemeLeptonXModule',
expression: 'ThemeLeptonXModule.forRoot()',
},
{
path: '@volosoft/abp.ng.theme.lepton-x/layouts',
importName: 'SideMenuLayoutModule.forRoot()',
importName: 'SideMenuLayoutModule',
expression: 'SideMenuLayoutModule.forRoot()',
},
]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"extends": "../../.eslintrc.json",
"ignorePatterns": [
"!**/*"
],
"overrides": [
{
"files": [
"*.ts"
],
"parserOptions": {
"project": [
"projects/<%= kebab(libraryName) %>/tsconfig.lib.json",
"projects/<%= kebab(libraryName) %>/tsconfig.spec.json"
],
"createDefaultProgram": true
},
"rules": {
"@angular-eslint/directive-selector": [
"error",
{
"type": "attribute",
"prefix": "lib",
"style": "camelCase"
}
],
"@angular-eslint/component-selector": [
"error",
{
"type": "element",
"prefix": "lib",
"style": "kebab-case"
}
]
}
},
{
"files": [
"*.html"
],
"rules": {}
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json",
"dest": "../../dist/<%= kebab(libraryName) %>/config",
"lib": {
"entryFile": "src/public-api.ts"
}
}
Loading