From 61eb7c7576e6252b369e70e6d4eba6217c12b95a Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Mon, 14 Apr 2025 18:48:13 +0300 Subject: [PATCH 01/27] Refactor schematics:create-lib command to align with the standalone template structure. --- .../src/commands/change-theme/index.ts | 15 +++- .../.eslintrc.json.template | 44 ++++++++++ .../config/ng-package.json.template | 7 ++ .../config/src/enums/index.ts.template | 1 + .../config/src/enums/route-names.ts.template | 3 + .../config/src/providers/index.ts.template | 1 + .../src/providers/route.provider.ts.template | 25 ++++++ .../config/src/public-api.ts.template | 2 + .../karma.conf.js.template | 44 ++++++++++ .../ng-package.json.template | 7 ++ .../package.json.template | 11 +++ .../__libraryName@kebab__-routing.ts.template | 9 ++ ..._libraryName@kebab__.component.ts.template | 11 +++ .../src/lib/index.ts.template | 1 + .../src/public-api.ts.template | 4 + .../src/test.ts.template | 26 ++++++ .../tsconfig.lib.json.template | 20 +++++ .../tsconfig.lib.prod.json.template | 10 +++ .../tsconfig.spec.json.template | 17 ++++ .../ng-package.json.template | 6 ++ ...@kebab__-__libraryName@kebab__.ts.template | 7 ++ .../src/lib/index.ts.template | 1 + .../src/public-api.ts.template | 1 + .../src/commands/create-lib/index.ts | 88 ++++++++++++++++--- .../create-lib/models/generate-lib-schema.ts | 4 + .../src/commands/create-lib/schema.json | 11 ++- .../schematics/src/utils/angular/ast-utils.ts | 9 +- .../schematics/src/utils/workspace.ts | 11 +++ 28 files changed, 377 insertions(+), 19 deletions(-) create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/.eslintrc.json.template create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/ng-package.json.template create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/enums/index.ts.template create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/enums/route-names.ts.template create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/providers/index.ts.template create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/providers/route.provider.ts.template create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/public-api.ts.template create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/karma.conf.js.template create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/ng-package.json.template create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/package.json.template create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__-routing.ts.template create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__.component.ts.template create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/index.ts.template create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/public-api.ts.template create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/test.ts.template create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.json.template create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.prod.json.template create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.spec.json.template create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-secondary-entrypoint-standalone/__libraryName@kebab__/ng-package.json.template create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-secondary-entrypoint-standalone/__libraryName@kebab__/src/lib/__target@kebab__-__libraryName@kebab__.ts.template create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-secondary-entrypoint-standalone/__libraryName@kebab__/src/lib/index.ts.template create mode 100644 npm/ng-packs/packages/schematics/src/commands/create-lib/files-secondary-entrypoint-standalone/__libraryName@kebab__/src/public-api.ts.template 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 de0c75c2efc..56e80a0762c 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 @@ -5,6 +5,7 @@ import * as ts from 'typescript'; import { allStyles, importMap, styleMap } from './style-map'; import { ChangeThemeOptions } from './model'; import { + applicationHasStandaloneTemplate, Change, createDefaultPath, InsertChange, @@ -68,13 +69,15 @@ function updateProjectStyle( function updateAppModule(selectedProject: string, targetThemeName: ThemeOptionsEnum): Rule { return async (host: Tree) => { - const appModulePath = (await createDefaultPath(host, selectedProject)) + '/app.module.ts'; + const isStandalone = applicationHasStandaloneTemplate(host, selectedProject); + const defaultPath = await createDefaultPath(host, selectedProject); + const appModulePath = defaultPath + `${isStandalone ? '/app.component.ts' : '/app.module.ts'}`; return chain([ removeImportPath(appModulePath, targetThemeName), removeImportFromNgModuleMetadata(appModulePath, targetThemeName), removeProviderFromNgModuleMetadata(appModulePath, targetThemeName), - insertImports(appModulePath, targetThemeName), + insertImports(appModulePath, targetThemeName, isStandalone), insertProviders(appModulePath, targetThemeName), ]); }; @@ -192,7 +195,11 @@ export function removeProviderFromNgModuleMetadata( }; } -export function insertImports(appModulePath: string, selectedTheme: ThemeOptionsEnum): Rule { +export function insertImports( + appModulePath: string, + selectedTheme: ThemeOptionsEnum, + isStandalone = false, +): Rule { return (host: Tree) => { const recorder = host.beginUpdate(appModulePath); const source = createSourceFile(host, appModulePath); @@ -205,7 +212,7 @@ export function insertImports(appModulePath: string, selectedTheme: ThemeOptions const changes: Change[] = []; selected.map(({ importName, path }) => - changes.push(...addImportToModule(source, appModulePath, importName, path)), + changes.push(...addImportToModule(source, appModulePath, importName, path, isStandalone)), ); if (changes.length > 0) { diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/.eslintrc.json.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/.eslintrc.json.template new file mode 100644 index 00000000000..de84cad019f --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/.eslintrc.json.template @@ -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": {} + } + ] +} diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/ng-package.json.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/ng-package.json.template new file mode 100644 index 00000000000..6cf8e96f647 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/ng-package.json.template @@ -0,0 +1,7 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json", + "dest": "../../dist/<%= kebab(libraryName) %>/config", + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/enums/index.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/enums/index.ts.template new file mode 100644 index 00000000000..3bda94b0784 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/enums/index.ts.template @@ -0,0 +1 @@ +export * from './route-names'; diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/enums/route-names.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/enums/route-names.ts.template new file mode 100644 index 00000000000..3bbb75be523 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/enums/route-names.ts.template @@ -0,0 +1,3 @@ +export const enum e<%= pascal(libraryName) %>RouteNames { + <%= pascal(libraryName) %> = '<%= pascal(libraryName) %>', +} diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/providers/index.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/providers/index.ts.template new file mode 100644 index 00000000000..fe08efba8c2 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/providers/index.ts.template @@ -0,0 +1 @@ +export * from './route.provider'; diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/providers/route.provider.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/providers/route.provider.ts.template new file mode 100644 index 00000000000..91b2d7545cc --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/providers/route.provider.ts.template @@ -0,0 +1,25 @@ +import { APP_INITIALIZER, Provider } from '@angular/core'; +import { eLayoutType, RoutesService } from '@abp/ng.core'; +import { e<%= pascal(libraryName) %>RouteNames } from '../enums/route-names'; +import { makeEnvironmentProviders, provideAppInitializer, inject } from '@angular/core'; + +export function configureRoutes() { + const routes = inject(RoutesService); + routes.add([ + { + path: '/<%= kebab(libraryName) %>', + name: e<%= pascal(libraryName) %>RouteNames.<%= pascal(libraryName) %>, + iconClass: 'fas fa-book', + layout: eLayoutType.application, + order: 3, + }, + ]); +} + +export function provide<%= pascal(libraryName) %>Config() { + return makeEnvironmentProviders([ + provideAppInitializer(() => { + configureRoutes(); + }), + ]); +} diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/public-api.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/public-api.ts.template new file mode 100644 index 00000000000..0003cebbeff --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/public-api.ts.template @@ -0,0 +1,2 @@ +export * from './enums'; +export * from './providers'; diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/karma.conf.js.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/karma.conf.js.template new file mode 100644 index 00000000000..e181d4088d1 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/karma.conf.js.template @@ -0,0 +1,44 @@ +// Karma configuration file, see link for more information +// https://karma-runner.github.io/1.0/config/configuration-file.html + +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-chrome-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, '../../coverage/<%= kebab(libraryName) %>'), + subdir: '.', + reporters: [ + { type: 'html' }, + { type: 'text-summary' } + ] + }, + reporters: ['progress', 'kjhtml'], + port: 9876, + colors: true, + logLevel: config.LOG_INFO, + autoWatch: true, + browsers: ['Chrome'], + singleRun: false, + restartOnFileChange: true + }); +}; diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/ng-package.json.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/ng-package.json.template new file mode 100644 index 00000000000..fd8e55d798d --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/ng-package.json.template @@ -0,0 +1,7 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/<%= kebab(libraryName) %>", + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/package.json.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/package.json.template new file mode 100644 index 00000000000..c4d36a85760 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/package.json.template @@ -0,0 +1,11 @@ +{ + "name": "@<%= kebab(libraryName) %>/<%= kebab(libraryName) %>", + "version": "0.0.1", + "peerDependencies": { + "@abp/ng.core": "<%= abpVersion %>", + "@abp/ng.theme.shared": "<%= abpVersion %>" + }, + "dependencies": { + "tslib": "^2.1.0" + } +} diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__-routing.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__-routing.ts.template new file mode 100644 index 00000000000..fba42c78824 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__-routing.ts.template @@ -0,0 +1,9 @@ +import { Routes } from '@angular/router'; + +export const <%= pascal(libraryName) %>Routes: Routes = [ + { + path: '', + loadComponent: () => + import('./<%= kebab(libraryName) %>.component').then(m => m.<%= pascal(libraryName) %>HomeComponent), + }, +]; diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__.component.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__.component.ts.template new file mode 100644 index 00000000000..78dae13b85f --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__.component.ts.template @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; +import {CoreModule} from "@abp/ng.core"; +import {ThemeSharedModule} from "@abp/ng.theme.shared"; + +@Component({ + standalone: true, + selector: '<%= camel(libraryName) %>-home', + template: `

Lazy Loaded Test Component

`, + imports: [CoreModule, ThemeSharedModule], +}) +export class <%= pascal(libraryName) %>HomeComponent {} diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/index.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/index.ts.template new file mode 100644 index 00000000000..aee9705b6fa --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/index.ts.template @@ -0,0 +1 @@ +export * from './<%= kebab(libraryName) %>-routing'; diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/public-api.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/public-api.ts.template new file mode 100644 index 00000000000..5fa8ee704c6 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/public-api.ts.template @@ -0,0 +1,4 @@ +/* + * Public API Surface of my-project-name + */ +export * from './lib'; diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/test.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/test.ts.template new file mode 100644 index 00000000000..52e55168ebc --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/test.ts.template @@ -0,0 +1,26 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js'; +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context(path: string, deep?: boolean, filter?: RegExp): { + keys(): string[]; + (id: string): T; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting() +); +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.json.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.json.template new file mode 100644 index 00000000000..5b574d313d7 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.json.template @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/lib", + "target": "es2020", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [], + "lib": [ + "dom", + "es2018" + ] + }, + "exclude": [ + "src/test.ts", + "**/*.spec.ts" + ] +} diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.prod.json.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.prod.json.template new file mode 100644 index 00000000000..06de549e107 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.prod.json.template @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.spec.json.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.spec.json.template new file mode 100644 index 00000000000..715dd0a5d2a --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.spec.json.template @@ -0,0 +1,17 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "files": [ + "src/test.ts" + ], + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-secondary-entrypoint-standalone/__libraryName@kebab__/ng-package.json.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-secondary-entrypoint-standalone/__libraryName@kebab__/ng-package.json.template new file mode 100644 index 00000000000..e09fb3fd037 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-secondary-entrypoint-standalone/__libraryName@kebab__/ng-package.json.template @@ -0,0 +1,6 @@ +{ + "$schema": "../../../node_modules/ng-packagr/ng-entrypoint.schema.json", + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-secondary-entrypoint-standalone/__libraryName@kebab__/src/lib/__target@kebab__-__libraryName@kebab__.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-secondary-entrypoint-standalone/__libraryName@kebab__/src/lib/__target@kebab__-__libraryName@kebab__.ts.template new file mode 100644 index 00000000000..a6b4121de5a --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-secondary-entrypoint-standalone/__libraryName@kebab__/src/lib/__target@kebab__-__libraryName@kebab__.ts.template @@ -0,0 +1,7 @@ +import { Provider } from '@angular/core'; + +export function provide<%= pascal(target) %><%= pascal(libraryName) %>(): Provider[] { + return [ + // Add your providers here + ]; +} diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-secondary-entrypoint-standalone/__libraryName@kebab__/src/lib/index.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-secondary-entrypoint-standalone/__libraryName@kebab__/src/lib/index.ts.template new file mode 100644 index 00000000000..b8bd7952292 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-secondary-entrypoint-standalone/__libraryName@kebab__/src/lib/index.ts.template @@ -0,0 +1 @@ +export * from './<%= kebab(target) %>-<%= kebab(libraryName) %>'; diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-secondary-entrypoint-standalone/__libraryName@kebab__/src/public-api.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-secondary-entrypoint-standalone/__libraryName@kebab__/src/public-api.ts.template new file mode 100644 index 00000000000..11aece60c49 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-secondary-entrypoint-standalone/__libraryName@kebab__/src/public-api.ts.template @@ -0,0 +1 @@ +export * from './lib/index'; diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts index f3538458800..78a7d2e904b 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts @@ -68,10 +68,18 @@ function createLibrary(options: GenerateLibSchema): Rule { const target = await resolveProject(tree, options.packageName, null); if (!target || options.override) { if (options.isModuleTemplate) { - return createLibFromModuleTemplate(tree, options); + if (options.isStandaloneTemplate) { + return createLibFromModuleStandaloneTemplate(tree, options); + } else { + return createLibFromModuleTemplate(tree, options); + } } if (options.isSecondaryEntrypoint) { - return createLibSecondaryEntry(tree, options); + if (options.isStandaloneTemplate) { + return createLibSecondaryEntryWithStandaloneTemplate(tree, options); + } else { + return createLibSecondaryEntry(tree, options); + } } } else { throw new SchematicsException( @@ -109,14 +117,32 @@ async function createLibFromModuleTemplate(tree: Tree, options: GenerateLibSchem }), move(normalize(packagesDir)), ]), - addLibToWorkspaceIfNotExist(options.packageName, packagesDir), + addLibToWorkspaceIfNotExist(options, packagesDir), ]); } -export function addLibToWorkspaceIfNotExist(name: string, packagesDir: string): Rule { +async function createLibFromModuleStandaloneTemplate(tree: Tree, options: GenerateLibSchema) { + const packagesDir = await resolvePackagesDirFromAngularJson(tree); + const packageJson = JSON.parse(tree.read('./package.json')!.toString()); + const abpVersion = packageJson.dependencies['@abp/ng.core']; + + return chain([ + applyWithOverwrite(url('./files-package-standalone'), [ + applyTemplates({ + ...cases, + libraryName: options.packageName, + abpVersion, + }), + move(normalize(packagesDir)), + ]), + addLibToWorkspaceIfNotExist(options, packagesDir), + ]); +} + +export function addLibToWorkspaceIfNotExist(options: GenerateLibSchema, packagesDir: string): Rule { return async (tree: Tree) => { const workspace = await getWorkspace(tree); - const packageName = kebab(name); + const packageName = kebab(options.packageName); const isProjectExist = workspace.projects.has(packageName); const projectRoot = join(normalize(packagesDir), packageName); @@ -130,8 +156,8 @@ export function addLibToWorkspaceIfNotExist(name: string, packagesDir: string): : noop(), addLibToWorkspaceFile(projectRoot, packageName), updateTsConfig(packageName, pathImportLib), - importConfigModuleToDefaultProjectAppModule(workspace, packageName), - addRoutingToAppRoutingModule(workspace, packageName), + importConfigModuleToDefaultProjectAppModule(workspace, packageName, options), + addRoutingToAppRoutingModule(workspace, packageName, options), ]); }; } @@ -169,9 +195,30 @@ export async function createLibSecondaryEntry(tree: Tree, options: GenerateLibSc ]); } +export async function createLibSecondaryEntryWithStandaloneTemplate( + tree: Tree, + options: GenerateLibSchema, +) { + const targetLib = await resolveProject(tree, options.target); + const packageName = `${kebab(targetLib.name)}/${kebab(options.packageName)}`; + const importPath = `${targetLib.definition.root}/${kebab(options.packageName)}`; + return chain([ + applyWithOverwrite(url('./files-secondary-entrypoint-standalone'), [ + applyTemplates({ + ...cases, + libraryName: options.packageName, + target: targetLib.name, + }), + move(normalize(targetLib.definition.root)), + updateTsConfig(packageName, importPath), + ]), + ]); +} + export function importConfigModuleToDefaultProjectAppModule( workspace: WorkspaceDefinition, packageName: string, + options: GenerateLibSchema, ) { return (tree: Tree) => { const projectName = readWorkspaceSchema(tree).defaultProject || getFirstApplication(tree).name!; @@ -182,17 +229,25 @@ export function importConfigModuleToDefaultProjectAppModule( return; } const appModuleContent = appModule.toString(); - if (appModuleContent.includes(`${camel(packageName)}ConfigModule`)) { + if ( + appModuleContent.includes( + options.isStandaloneTemplate + ? `provide${pascal(packageName)}Config` + : `${camel(packageName)}ConfigModule`, + ) + ) { return; } - const forRootStatement = `${pascal(packageName)}ConfigModule.forRoot()`; + const rootConfigStatement = options.isStandaloneTemplate + ? `provide${pascal(packageName)}Config()` + : `${pascal(packageName)}ConfigModule.forRoot()`; const text = tree.read(appModulePath); if (!text) { return; } const sourceText = text.toString(); - if (sourceText.includes(forRootStatement)) { + if (sourceText.includes(rootConfigStatement)) { return; } const source = ts.createSourceFile(appModulePath, sourceText, ts.ScriptTarget.Latest, true); @@ -200,8 +255,9 @@ export function importConfigModuleToDefaultProjectAppModule( const changes = addImportToModule( source, appModulePath, - forRootStatement, + rootConfigStatement, `${kebab(packageName)}/config`, + options.isStandaloneTemplate, ); const recorder = tree.beginUpdate(appModulePath); for (const change of changes) { @@ -215,7 +271,11 @@ export function importConfigModuleToDefaultProjectAppModule( }; } -export function addRoutingToAppRoutingModule(workspace: WorkspaceDefinition, packageName: string) { +export function addRoutingToAppRoutingModule( + workspace: WorkspaceDefinition, + packageName: string, + options: GenerateLibSchema, +) { return (tree: Tree) => { const projectName = readWorkspaceSchema(tree).defaultProject || getFirstApplication(tree).name!; const project = workspace.projects.get(projectName); @@ -237,7 +297,9 @@ export function addRoutingToAppRoutingModule(workspace: WorkspaceDefinition, pac true, ); const importPath = `${kebab(packageName)}`; - const importStatement = `() => import('${importPath}').then(m => m.${moduleName}.forLazy())`; + const importStatement = options.isStandaloneTemplate + ? `() => import('${importPath}').then(m => m.${pascal(packageName)}Routes)` + : `() => import('${importPath}').then(m => m.${moduleName}.forLazy())`; const routeDefinition = `{ path: '${kebab(packageName)}', loadChildren: ${importStatement} }`; const change = addRouteDeclarationToModule(source, `${kebab(packageName)}`, routeDefinition); diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/models/generate-lib-schema.ts b/npm/ng-packs/packages/schematics/src/commands/create-lib/models/generate-lib-schema.ts index f3c6e5b97fa..a6d32e1da08 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/models/generate-lib-schema.ts +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/models/generate-lib-schema.ts @@ -8,6 +8,10 @@ export interface GenerateLibSchema { * İs the package a library or a library module */ isSecondaryEntrypoint: boolean; + /** + * İs the package has standalone template + */ + isStandaloneTemplate: boolean; isModuleTemplate: boolean; diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/schema.json b/npm/ng-packs/packages/schematics/src/commands/create-lib/schema.json index 19df1a54273..19de7420aee 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/schema.json +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/schema.json @@ -31,12 +31,21 @@ }, "x-prompt": "Is module template?" }, + "isStandaloneTemplate": { + "description": "Is standalone template", + "type": "boolean", + "$default": { + "$source": "argv", + "index": 3 + }, + "x-prompt": "Is standalone template?" + }, "override": { "description": "Override existing files", "type": "boolean", "$default": { "$source": "argv", - "index": 3 + "index": 4 }, "x-prompt": "Override existing files?" } diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/ast-utils.ts b/npm/ng-packs/packages/schematics/src/utils/angular/ast-utils.ts index d435b90c85b..97eaa2e6306 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/ast-utils.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/ast-utils.ts @@ -493,8 +493,15 @@ export function addImportToModule( modulePath: string, classifiedName: string, importPath: string, + isStandalone = false, ): Change[] { - return addSymbolToNgModuleMetadata(source, modulePath, 'imports', classifiedName, importPath); + return addSymbolToNgModuleMetadata( + source, + modulePath, + isStandalone ? 'providers' : 'imports', + classifiedName, + importPath, + ); } /** diff --git a/npm/ng-packs/packages/schematics/src/utils/workspace.ts b/npm/ng-packs/packages/schematics/src/utils/workspace.ts index ed53e482e80..6218bc038e0 100644 --- a/npm/ng-packs/packages/schematics/src/utils/workspace.ts +++ b/npm/ng-packs/packages/schematics/src/utils/workspace.ts @@ -13,6 +13,17 @@ export function isLibrary(project: workspaces.ProjectDefinition): boolean { return project.extensions['projectType'] === ProjectType.Library; } +export function applicationHasStandaloneTemplate(tree: Tree, selectedProject?: string): boolean { + const workspace = readWorkspaceSchema(tree); + const project = workspace.projects[selectedProject ?? 0]; + + const mainPath = project.sourceRoot + '/main.ts'; + const mainSource = readFileInTree(tree, mainPath); + const mainContent = mainSource.toString(); + + return mainContent.includes('bootstrapComponent'); +} + export function readEnvironment(tree: Tree, project: workspaces.ProjectDefinition) { if (isLibrary(project)) return undefined; From ebaf0be8f9d9fb759217a25fd77c82b31f5a9c1a Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Tue, 15 Apr 2025 11:45:34 +0300 Subject: [PATCH 02/27] schematics standalone utilities --- .../schematics/src/utils/angular/eol.ts | 25 ++ .../utils/angular/standalone/app_component.ts | 148 ++++++++++ .../utils/angular/standalone/app_config.ts | 127 +++++++++ .../utils/angular/standalone/code_block.ts | 115 ++++++++ .../src/utils/angular/standalone/index.ts | 10 + .../src/utils/angular/standalone/rules.ts | 258 ++++++++++++++++++ .../src/utils/angular/standalone/util.ts | 171 ++++++++++++ 7 files changed, 854 insertions(+) create mode 100644 npm/ng-packs/packages/schematics/src/utils/angular/eol.ts create mode 100644 npm/ng-packs/packages/schematics/src/utils/angular/standalone/app_component.ts create mode 100644 npm/ng-packs/packages/schematics/src/utils/angular/standalone/app_config.ts create mode 100644 npm/ng-packs/packages/schematics/src/utils/angular/standalone/code_block.ts create mode 100644 npm/ng-packs/packages/schematics/src/utils/angular/standalone/index.ts create mode 100644 npm/ng-packs/packages/schematics/src/utils/angular/standalone/rules.ts create mode 100644 npm/ng-packs/packages/schematics/src/utils/angular/standalone/util.ts diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/eol.ts b/npm/ng-packs/packages/schematics/src/utils/angular/eol.ts new file mode 100644 index 00000000000..5fdae3f5753 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/utils/angular/eol.ts @@ -0,0 +1,25 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { EOL } from 'node:os'; + +const CRLF = '\r\n'; +const LF = '\n'; + +export function getEOL(content: string): string { + const newlines = content.match(/(?:\r?\n)/g); + + if (newlines?.length) { + const crlf = newlines.filter(l => l === CRLF).length; + const lf = newlines.length - crlf; + + return crlf > lf ? CRLF : LF; + } + + return EOL; +} diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/standalone/app_component.ts b/npm/ng-packs/packages/schematics/src/utils/angular/standalone/app_component.ts new file mode 100644 index 00000000000..f617e0baaa9 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/utils/angular/standalone/app_component.ts @@ -0,0 +1,148 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { SchematicsException, Tree } from '@angular-devkit/schematics'; +import * as ts from 'typescript'; +import { getDecoratorMetadata, getMetadataField } from '../ast-utils'; +import { findBootstrapModuleCall, getAppModulePath } from '../ng-ast-utils'; +import { findBootstrapApplicationCall, getSourceFile } from './util'; + +/** Data resolved for a bootstrapped component. */ +interface BootstrappedComponentData { + /** Original name of the component class. */ + componentName: string; + + /** Path under which the component was imported in the main entrypoint. */ + componentImportPathInSameFile: string; + + /** Original name of the NgModule being bootstrapped, null if the app isn't module-based. */ + moduleName: string | null; + + /** + * Path under which the module was imported in the main entrypoint, + * null if the app isn't module-based. + */ + moduleImportPathInSameFile: string | null; +} + +/** + * Finds the original name and path relative to the `main.ts` of the bootrstrapped app component. + * @param tree File tree in which to look for the component. + * @param mainFilePath Path of the `main` file. + */ +export function resolveBootstrappedComponentData( + tree: Tree, + mainFilePath: string, +): BootstrappedComponentData | null { + // First try to resolve for a standalone app. + try { + const call = findBootstrapApplicationCall(tree, mainFilePath); + + if (call.arguments.length > 0 && ts.isIdentifier(call.arguments[0])) { + const resolved = resolveIdentifier(call.arguments[0]); + + if (resolved) { + return { + componentName: resolved.name, + componentImportPathInSameFile: resolved.path, + moduleName: null, + moduleImportPathInSameFile: null, + }; + } + } + } catch (e) { + // `findBootstrapApplicationCall` will throw if it can't find the `bootrstrapApplication` call. + // Catch so we can continue to the fallback logic. + if (!(e instanceof SchematicsException)) { + throw e; + } + } + + // Otherwise fall back to resolving an NgModule-based app. + return resolveNgModuleBasedData(tree, mainFilePath); +} + +/** Resolves the bootstrap data for a NgModule-based app. */ +function resolveNgModuleBasedData( + tree: Tree, + mainFilePath: string, +): BootstrappedComponentData | null { + const appModulePath = getAppModulePath(tree, mainFilePath); + const appModuleFile = getSourceFile(tree, appModulePath); + const metadataNodes = getDecoratorMetadata(appModuleFile, 'NgModule', '@angular/core'); + + for (const node of metadataNodes) { + if (!ts.isObjectLiteralExpression(node)) { + continue; + } + + const bootstrapProp = getMetadataField(node, 'bootstrap').find(prop => { + return ( + ts.isArrayLiteralExpression(prop.initializer) && + prop.initializer.elements.length > 0 && + ts.isIdentifier(prop.initializer.elements[0]) + ); + }); + + const componentIdentifier = (bootstrapProp?.initializer as ts.ArrayLiteralExpression) + .elements[0] as ts.Identifier | undefined; + const componentResult = componentIdentifier ? resolveIdentifier(componentIdentifier) : null; + const bootstrapCall = findBootstrapModuleCall(tree, mainFilePath); + + if ( + componentResult && + bootstrapCall && + bootstrapCall.arguments.length > 0 && + ts.isIdentifier(bootstrapCall.arguments[0]) + ) { + const moduleResult = resolveIdentifier(bootstrapCall.arguments[0]); + + if (moduleResult) { + return { + componentName: componentResult.name, + componentImportPathInSameFile: componentResult.path, + moduleName: moduleResult.name, + moduleImportPathInSameFile: moduleResult.path, + }; + } + } + } + + return null; +} + +/** Resolves an identifier to its original name and path that it was imported from. */ +function resolveIdentifier(identifier: ts.Identifier): { name: string; path: string } | null { + const sourceFile = identifier.getSourceFile(); + + // Try to resolve the import path by looking at the top-level named imports of the file. + for (const node of sourceFile.statements) { + if ( + !ts.isImportDeclaration(node) || + !ts.isStringLiteral(node.moduleSpecifier) || + !node.importClause || + !node.importClause.namedBindings || + !ts.isNamedImports(node.importClause.namedBindings) + ) { + continue; + } + + for (const element of node.importClause.namedBindings.elements) { + if (element.name.text === identifier.text) { + return { + // Note that we use `propertyName` if available, because it contains + // the real name in the case where the import is aliased. + name: (element.propertyName || element.name).text, + path: node.moduleSpecifier.text, + }; + } + } + } + + return null; +} diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/standalone/app_config.ts b/npm/ng-packs/packages/schematics/src/utils/angular/standalone/app_config.ts new file mode 100644 index 00000000000..817f7b680ac --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/utils/angular/standalone/app_config.ts @@ -0,0 +1,127 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { Tree } from '@angular-devkit/schematics'; +import { dirname, join } from 'node:path'; +import * as ts from 'typescript'; +import { getSourceFile } from './util'; + +/** App config that was resolved to its source node. */ +export interface ResolvedAppConfig { + /** Tree-relative path of the file containing the app config. */ + filePath: string; + + /** Node defining the app config. */ + node: ts.ObjectLiteralExpression; +} + +/** + * Resolves the node that defines the app config from a bootstrap call. + * @param bootstrapCall Call for which to resolve the config. + * @param tree File tree of the project. + * @param filePath File path of the bootstrap call. + */ +export function findAppConfig( + bootstrapCall: ts.CallExpression, + tree: Tree, + filePath: string, +): ResolvedAppConfig | null { + if (bootstrapCall.arguments.length > 1) { + const config = bootstrapCall.arguments[1]; + + if (ts.isObjectLiteralExpression(config)) { + return { filePath, node: config }; + } + + if (ts.isIdentifier(config)) { + return resolveAppConfigFromIdentifier(config, tree, filePath); + } + } + + return null; +} + +/** + * Resolves the app config from an identifier referring to it. + * @param identifier Identifier referring to the app config. + * @param tree File tree of the project. + * @param bootstapFilePath Path of the bootstrap call. + */ +function resolveAppConfigFromIdentifier( + identifier: ts.Identifier, + tree: Tree, + bootstapFilePath: string, +): ResolvedAppConfig | null { + const sourceFile = identifier.getSourceFile(); + + for (const node of sourceFile.statements) { + // Only look at relative imports. This will break if the app uses a path + // mapping to refer to the import, but in order to resolve those, we would + // need knowledge about the entire program. + if ( + !ts.isImportDeclaration(node) || + !node.importClause?.namedBindings || + !ts.isNamedImports(node.importClause.namedBindings) || + !ts.isStringLiteralLike(node.moduleSpecifier) || + !node.moduleSpecifier.text.startsWith('.') + ) { + continue; + } + + for (const specifier of node.importClause.namedBindings.elements) { + if (specifier.name.text !== identifier.text) { + continue; + } + + // Look for a variable with the imported name in the file. Note that ideally we would use + // the type checker to resolve this, but we can't because these utilities are set up to + // operate on individual files, not the entire program. + const filePath = join(dirname(bootstapFilePath), node.moduleSpecifier.text + '.ts'); + const importedSourceFile = getSourceFile(tree, filePath); + const resolvedVariable = findAppConfigFromVariableName( + importedSourceFile, + (specifier.propertyName || specifier.name).text, + ); + + if (resolvedVariable) { + return { filePath, node: resolvedVariable }; + } + } + } + + const variableInSameFile = findAppConfigFromVariableName(sourceFile, identifier.text); + + return variableInSameFile ? { filePath: bootstapFilePath, node: variableInSameFile } : null; +} + +/** + * Finds an app config within the top-level variables of a file. + * @param sourceFile File in which to search for the config. + * @param variableName Name of the variable containing the config. + */ +function findAppConfigFromVariableName( + sourceFile: ts.SourceFile, + variableName: string, +): ts.ObjectLiteralExpression | null { + for (const node of sourceFile.statements) { + if (ts.isVariableStatement(node)) { + for (const decl of node.declarationList.declarations) { + if ( + ts.isIdentifier(decl.name) && + decl.name.text === variableName && + decl.initializer && + ts.isObjectLiteralExpression(decl.initializer) + ) { + return decl.initializer; + } + } + } + } + + return null; +} diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/standalone/code_block.ts b/npm/ng-packs/packages/schematics/src/utils/angular/standalone/code_block.ts new file mode 100644 index 00000000000..a572f3b6c9b --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/utils/angular/standalone/code_block.ts @@ -0,0 +1,115 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { Rule, Tree } from '@angular-devkit/schematics'; +import * as ts from 'typescript'; +import { hasTopLevelIdentifier, insertImport } from '../ast-utils'; +import { applyToUpdateRecorder } from '../change'; + +/** Generated code that hasn't been interpolated yet. */ +export interface PendingCode { + /** Code that will be inserted. */ + expression: string; + + /** Imports that need to be added to the file in which the code is inserted. */ + imports: PendingImports; +} + +/** Map keeping track of imports and aliases under which they're referred to in an expression. */ +type PendingImports = Map>; + +/** Counter used to generate unique IDs. */ +let uniqueIdCounter = 0; + +/** + * Callback invoked by a Rule that produces the code + * that needs to be inserted somewhere in the app. + */ +export type CodeBlockCallback = (block: CodeBlock) => PendingCode; + +/** + * Utility class used to generate blocks of code that + * can be inserted by the devkit into a user's app. + */ +export class CodeBlock { + private _imports: PendingImports = new Map>(); + + // Note: the methods here are defined as arrow function so that they can be destructured by + // consumers without losing their context. This makes the API more concise. + + /** Function used to tag a code block in order to produce a `PendingCode` object. */ + code = (strings: TemplateStringsArray, ...params: unknown[]): PendingCode => { + return { + expression: strings.map((part, index) => part + (params[index] || '')).join(''), + imports: this._imports, + }; + }; + + /** + * Used inside of a code block to mark external symbols and which module they should be imported + * from. When the code is inserted, the required import statements will be produced automatically. + * @param symbolName Name of the external symbol. + * @param moduleName Module from which the symbol should be imported. + */ + external = (symbolName: string, moduleName: string): string => { + if (!this._imports.has(moduleName)) { + this._imports.set(moduleName, new Map()); + } + + const symbolsPerModule = this._imports.get(moduleName) as Map; + + if (!symbolsPerModule.has(symbolName)) { + symbolsPerModule.set(symbolName, `@@__SCHEMATIC_PLACEHOLDER_${uniqueIdCounter++}__@@`); + } + + return symbolsPerModule.get(symbolName) as string; + }; + + /** + * Produces the necessary rules to transform a `PendingCode` object into valid code. + * @param initialCode Code pending transformed. + * @param filePath Path of the file in which the code will be inserted. + */ + static transformPendingCode(initialCode: PendingCode, filePath: string) { + const code = { ...initialCode }; + const rules: Rule[] = []; + + code.imports.forEach((symbols, moduleName) => { + symbols.forEach((placeholder, symbolName) => { + rules.push((tree: Tree) => { + const recorder = tree.beginUpdate(filePath); + const sourceFile = ts.createSourceFile( + filePath, + tree.readText(filePath), + ts.ScriptTarget.Latest, + true, + ); + + // Note that this could still technically clash if there's a top-level symbol called + // `${symbolName}_alias`, however this is unlikely. We can revisit this if it becomes + // a problem. + const alias = hasTopLevelIdentifier(sourceFile, symbolName, moduleName) + ? symbolName + '_alias' + : undefined; + + code.expression = code.expression.replace( + new RegExp(placeholder, 'g'), + alias || symbolName, + ); + + applyToUpdateRecorder(recorder, [ + insertImport(sourceFile, filePath, symbolName, moduleName, false, alias), + ]); + tree.commitUpdate(recorder); + }); + }); + }); + + return { code, rules }; + } +} diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/standalone/index.ts b/npm/ng-packs/packages/schematics/src/utils/angular/standalone/index.ts new file mode 100644 index 00000000000..b522156c491 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/utils/angular/standalone/index.ts @@ -0,0 +1,10 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +export { addRootImport, addRootProvider } from './rules'; +export type { PendingCode, CodeBlockCallback, CodeBlock } from './code_block'; diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/standalone/rules.ts b/npm/ng-packs/packages/schematics/src/utils/angular/standalone/rules.ts new file mode 100644 index 00000000000..79bc5ea8e46 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/utils/angular/standalone/rules.ts @@ -0,0 +1,258 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { tags } from '@angular-devkit/core'; +import { Rule, SchematicsException, Tree, chain } from '@angular-devkit/schematics'; +import * as ts from 'typescript'; +import { addSymbolToNgModuleMetadata, insertAfterLastOccurrence } from '../ast-utils'; +import { InsertChange } from '../change'; +import { getAppModulePath, isStandaloneApp } from '../ng-ast-utils'; +import { ResolvedAppConfig, findAppConfig } from './app_config'; +import { CodeBlock, CodeBlockCallback, PendingCode } from './code_block'; +import { + applyChangesToFile, + findBootstrapApplicationCall, + findProvidersLiteral, + getMainFilePath, + getSourceFile, + isMergeAppConfigCall, +} from './util'; + +/** + * Adds an import to the root of the project. + * @param project Name of the project to which to add the import. + * @param callback Function that generates the code block which should be inserted. + * @example + * + * ```ts + * import { Rule } from '@angular-devkit/schematics'; + * import { addRootImport } from '@schematics/angular/utility'; + * + * export default function(): Rule { + * return addRootImport('default', ({code, external}) => { + * return code`${external('MyModule', '@my/module')}.forRoot({})`; + * }); + * } + * ``` + */ +export function addRootImport(project: string, callback: CodeBlockCallback): Rule { + return getRootInsertionRule(project, callback, 'imports', { + name: 'importProvidersFrom', + module: '@angular/core', + }); +} + +/** + * Adds a provider to the root of the project. + * @param project Name of the project to which to add the import. + * @param callback Function that generates the code block which should be inserted. + * @example + * + * ```ts + * import { Rule } from '@angular-devkit/schematics'; + * import { addRootProvider } from '@schematics/angular/utility'; + * + * export default function(): Rule { + * return addRootProvider('default', ({code, external}) => { + * return code`${external('provideLibrary', '@my/library')}({})`; + * }); + * } + * ``` + */ +export function addRootProvider(project: string, callback: CodeBlockCallback): Rule { + return getRootInsertionRule(project, callback, 'providers'); +} + +/** + * Creates a rule that inserts code at the root of either a standalone or NgModule-based project. + * @param project Name of the project into which to inser tthe code. + * @param callback Function that generates the code block which should be inserted. + * @param ngModuleField Field of the root NgModule into which the code should be inserted, if the + * app is based on NgModule + * @param standaloneWrapperFunction Function with which to wrap the code if the app is standalone. + */ +function getRootInsertionRule( + project: string, + callback: CodeBlockCallback, + ngModuleField: string, + standaloneWrapperFunction?: { name: string; module: string }, +): Rule { + return async host => { + const mainFilePath = await getMainFilePath(host, project); + const codeBlock = new CodeBlock(); + + if (isStandaloneApp(host, mainFilePath)) { + return tree => + addProviderToStandaloneBootstrap( + tree, + callback(codeBlock), + mainFilePath, + standaloneWrapperFunction, + ); + } + + const modulePath = getAppModulePath(host, mainFilePath); + const pendingCode = CodeBlock.transformPendingCode(callback(codeBlock), modulePath); + + return chain([ + ...pendingCode.rules, + tree => { + const changes = addSymbolToNgModuleMetadata( + getSourceFile(tree, modulePath), + modulePath, + ngModuleField, + pendingCode.code.expression, + // Explicitly set the import path to null since we deal with imports here separately. + null, + ); + + applyChangesToFile(tree, modulePath, changes); + }, + ]); + }; +} + +/** + * Adds a provider to the root of a standalone project. + * @param host Tree of the root rule. + * @param pendingCode Code that should be inserted. + * @param mainFilePath Path to the project's main file. + * @param wrapperFunction Optional function with which to wrap the provider. + */ +function addProviderToStandaloneBootstrap( + host: Tree, + pendingCode: PendingCode, + mainFilePath: string, + wrapperFunction?: { name: string; module: string }, +): Rule { + const bootstrapCall = findBootstrapApplicationCall(host, mainFilePath); + const fileToEdit = findAppConfig(bootstrapCall, host, mainFilePath)?.filePath || mainFilePath; + const { code, rules } = CodeBlock.transformPendingCode(pendingCode, fileToEdit); + + return chain([ + ...rules, + () => { + let wrapped: PendingCode; + let additionalRules: Rule[]; + + if (wrapperFunction) { + const block = new CodeBlock(); + const result = CodeBlock.transformPendingCode( + block.code`${block.external(wrapperFunction.name, wrapperFunction.module)}(${ + code.expression + })`, + fileToEdit, + ); + + wrapped = result.code; + additionalRules = result.rules; + } else { + wrapped = code; + additionalRules = []; + } + + return chain([ + ...additionalRules, + tree => insertStandaloneRootProvider(tree, mainFilePath, wrapped.expression), + ]); + }, + ]); +} + +/** + * Inserts a string expression into the root of a standalone project. + * @param tree File tree used to modify the project. + * @param mainFilePath Path to the main file of the project. + * @param expression Code expression to be inserted. + */ +function insertStandaloneRootProvider(tree: Tree, mainFilePath: string, expression: string): void { + const bootstrapCall = findBootstrapApplicationCall(tree, mainFilePath); + const appConfig = findAppConfig(bootstrapCall, tree, mainFilePath); + + if (bootstrapCall.arguments.length === 0) { + throw new SchematicsException( + `Cannot add provider to invalid bootstrapApplication call in ${ + bootstrapCall.getSourceFile().fileName + }`, + ); + } + + if (appConfig) { + addProvidersExpressionToAppConfig(tree, appConfig, expression); + + return; + } + + const newAppConfig = `, {\n${tags.indentBy(2)`providers: [${expression}]`}\n}`; + let targetCall: ts.CallExpression; + + if (bootstrapCall.arguments.length === 1) { + targetCall = bootstrapCall; + } else if (isMergeAppConfigCall(bootstrapCall.arguments[1])) { + targetCall = bootstrapCall.arguments[1]; + } else { + throw new SchematicsException( + `Cannot statically analyze bootstrapApplication call in ${ + bootstrapCall.getSourceFile().fileName + }`, + ); + } + + applyChangesToFile(tree, mainFilePath, [ + insertAfterLastOccurrence( + targetCall.arguments, + newAppConfig, + mainFilePath, + targetCall.getEnd() - 1, + ), + ]); +} + +/** + * Adds a string expression to an app config object. + * @param tree File tree used to modify the project. + * @param appConfig Resolved configuration object of the project. + * @param expression Code expression to be inserted. + */ +function addProvidersExpressionToAppConfig( + tree: Tree, + appConfig: ResolvedAppConfig, + expression: string, +): void { + const { node, filePath } = appConfig; + const configProps = node.properties; + const providersLiteral = findProvidersLiteral(node); + + // If there's a `providers` property, we can add the provider + // to it, otherwise we need to declare it ourselves. + if (providersLiteral) { + applyChangesToFile(tree, filePath, [ + insertAfterLastOccurrence( + providersLiteral.elements, + (providersLiteral.elements.length === 0 ? '' : ', ') + expression, + filePath, + providersLiteral.getStart() + 1, + ), + ]); + } else { + const prop = tags.indentBy(2)`providers: [${expression}]`; + let toInsert: string; + let insertPosition: number; + + if (configProps.length === 0) { + toInsert = '\n' + prop + '\n'; + insertPosition = node.getEnd() - 1; + } else { + const hasTrailingComma = configProps.hasTrailingComma; + toInsert = (hasTrailingComma ? '' : ',') + '\n' + prop; + insertPosition = configProps[configProps.length - 1].getEnd() + (hasTrailingComma ? 1 : 0); + } + + applyChangesToFile(tree, filePath, [new InsertChange(filePath, insertPosition, toInsert)]); + } +} diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/standalone/util.ts b/npm/ng-packs/packages/schematics/src/utils/angular/standalone/util.ts new file mode 100644 index 00000000000..0bb1419a631 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/utils/angular/standalone/util.ts @@ -0,0 +1,171 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.dev/license + */ + +import { SchematicsException, Tree } from '@angular-devkit/schematics'; +import * as ts from 'typescript'; +import { Change, applyToUpdateRecorder } from '../change'; +import { targetBuildNotFoundError } from '../project-targets'; +import { getWorkspace } from '../workspace'; +import { Builders } from '../workspace-models'; + +/** + * Finds the main file of a project. + * @param tree File tree for the project. + * @param projectName Name of the project in which to search. + */ +export async function getMainFilePath(tree: Tree, projectName: string): Promise { + const workspace = await getWorkspace(tree); + const project = workspace.projects.get(projectName); + const buildTarget = project?.targets.get('build'); + + if (!buildTarget) { + throw targetBuildNotFoundError(); + } + + const options = buildTarget.options as Record; + + return buildTarget.builder === Builders.Application || + buildTarget.builder === Builders.BuildApplication + ? options.browser + : options.main; +} + +/** + * Gets a TypeScript source file at a specific path. + * @param tree File tree of a project. + * @param path Path to the file. + */ +export function getSourceFile(tree: Tree, path: string): ts.SourceFile { + const content = tree.readText(path); + const source = ts.createSourceFile(path, content, ts.ScriptTarget.Latest, true); + + return source; +} + +/** Finds the call to `bootstrapApplication` within a file. */ +export function findBootstrapApplicationCall(tree: Tree, mainFilePath: string): ts.CallExpression { + const sourceFile = getSourceFile(tree, mainFilePath); + const localName = findImportLocalName( + sourceFile, + 'bootstrapApplication', + '@angular/platform-browser', + ); + + if (localName) { + let result: ts.CallExpression | null = null; + + sourceFile.forEachChild(function walk(node) { + if ( + ts.isCallExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text === localName + ) { + result = node; + } + + if (!result) { + node.forEachChild(walk); + } + }); + + if (result) { + return result; + } + } + + throw new SchematicsException(`Could not find bootstrapApplication call in ${mainFilePath}`); +} + +/** + * Finds the local name of an imported symbol. Could be the symbol name itself or its alias. + * @param sourceFile File within which to search for the import. + * @param name Actual name of the import, not its local alias. + * @param moduleName Name of the module from which the symbol is imported. + */ +function findImportLocalName( + sourceFile: ts.SourceFile, + name: string, + moduleName: string, +): string | null { + for (const node of sourceFile.statements) { + // Only look for top-level imports. + if ( + !ts.isImportDeclaration(node) || + !ts.isStringLiteral(node.moduleSpecifier) || + node.moduleSpecifier.text !== moduleName + ) { + continue; + } + + // Filter out imports that don't have the right shape. + if ( + !node.importClause || + !node.importClause.namedBindings || + !ts.isNamedImports(node.importClause.namedBindings) + ) { + continue; + } + + // Look through the elements of the declaration for the specific import. + for (const element of node.importClause.namedBindings.elements) { + if ((element.propertyName || element.name).text === name) { + // The local name is always in `name`. + return element.name.text; + } + } + } + + return null; +} + +/** + * Applies a set of changes to a file. + * @param tree File tree of the project. + * @param path Path to the file that is being changed. + * @param changes Changes that should be applied to the file. + */ +export function applyChangesToFile(tree: Tree, path: string, changes: Change[]) { + if (changes.length > 0) { + const recorder = tree.beginUpdate(path); + applyToUpdateRecorder(recorder, changes); + tree.commitUpdate(recorder); + } +} + +/** Checks whether a node is a call to `mergeApplicationConfig`. */ +export function isMergeAppConfigCall(node: ts.Node): node is ts.CallExpression { + if (!ts.isCallExpression(node)) { + return false; + } + + const localName = findImportLocalName( + node.getSourceFile(), + 'mergeApplicationConfig', + '@angular/core', + ); + + return !!localName && ts.isIdentifier(node.expression) && node.expression.text === localName; +} + +/** Finds the `providers` array literal within an application config. */ +export function findProvidersLiteral( + config: ts.ObjectLiteralExpression, +): ts.ArrayLiteralExpression | null { + for (const prop of config.properties) { + if ( + ts.isPropertyAssignment(prop) && + ts.isIdentifier(prop.name) && + prop.name.text === 'providers' && + ts.isArrayLiteralExpression(prop.initializer) + ) { + return prop.initializer; + } + } + + return null; +} From a9e92e2e4bae7bc01835ecdfedbf9ca74aa5ce20 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Tue, 15 Apr 2025 13:42:50 +0300 Subject: [PATCH 03/27] added new template definition --- npm/ng-packs/scripts/build-schematics.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/npm/ng-packs/scripts/build-schematics.ts b/npm/ng-packs/scripts/build-schematics.ts index 6690b272b3d..0699eb05676 100644 --- a/npm/ng-packs/scripts/build-schematics.ts +++ b/npm/ng-packs/scripts/build-schematics.ts @@ -23,10 +23,15 @@ const FILES_TO_COPY_AFTER_BUILD: (FileCopy | string)[] = [ { src: 'src/commands/create-lib/schema.json', dest: 'commands/create-lib/schema.json' }, { src: 'src/commands/change-theme/schema.json', dest: 'commands/change-theme/schema.json' }, { src: 'src/commands/create-lib/files-package', dest: 'commands/create-lib/files-package' }, + { src: 'src/commands/create-lib/files-package-standalone', dest: 'commands/create-lib/files-package-standalone' }, { src: 'src/commands/create-lib/files-secondary-entrypoint', dest: 'commands/create-lib/files-secondary-entrypoint', }, + { + src: 'src/commands/create-lib/files-secondary-entrypoint-standalone', + dest: 'commands/create-lib/files-secondary-entrypoint-standalone', + }, { src: 'src/commands/proxy-add/schema.json', dest: 'commands/proxy-add/schema.json' }, { src: 'src/commands/proxy-index/schema.json', dest: 'commands/proxy-index/schema.json' }, { src: 'src/commands/proxy-refresh/schema.json', dest: 'commands/proxy-refresh/schema.json' }, From cc99f14a0a52fb30105127658f3537e6fddda957 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Wed, 16 Apr 2025 00:16:31 +0300 Subject: [PATCH 04/27] schematics standalone updates --- .../src/commands/change-theme/index.ts | 48 +++--- .../src/commands/create-lib/index.ts | 93 ++++++++--- .../angular/add-declaration-to-ng-module.ts | 11 +- .../schematics/src/utils/angular/ast-utils.ts | 150 ++++++++++-------- .../schematics/src/utils/angular/change.ts | 20 ++- .../src/utils/angular/dependencies.ts | 2 +- .../src/utils/angular/dependency.ts | 10 +- .../src/utils/angular/find-module.ts | 12 +- .../src/utils/angular/generate-from-files.ts | 22 ++- .../schematics/src/utils/angular/index.ts | 1 + .../schematics/src/utils/angular/json-file.ts | 18 ++- .../src/utils/angular/ng-ast-utils.ts | 24 ++- .../src/utils/angular/parse-name.ts | 2 +- .../schematics/src/utils/angular/paths.ts | 12 +- .../src/utils/angular/project-targets.ts | 12 +- .../src/utils/angular/validation.ts | 5 +- .../src/utils/angular/workspace-models.ts | 20 ++- .../schematics/src/utils/angular/workspace.ts | 12 +- .../schematics/src/utils/workspace.ts | 14 +- 19 files changed, 315 insertions(+), 173 deletions(-) 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 56e80a0762c..a2f55cf2bba 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 @@ -5,17 +5,18 @@ import * as ts from 'typescript'; import { allStyles, importMap, styleMap } from './style-map'; import { ChangeThemeOptions } from './model'; import { - applicationHasStandaloneTemplate, + addRootImport, Change, createDefaultPath, + getWorkspace, InsertChange, isLibrary, + isStandaloneApp, updateWorkspace, WorkspaceDefinition, } from '../../utils'; import { ThemeOptionsEnum } from './theme-options.enum'; import { - addImportToModule, addProviderToModule, findNodes, getDecoratorMetadata, @@ -69,16 +70,20 @@ function updateProjectStyle( function updateAppModule(selectedProject: string, targetThemeName: ThemeOptionsEnum): Rule { return async (host: Tree) => { - const isStandalone = applicationHasStandaloneTemplate(host, selectedProject); + const workspace = await getWorkspace(host); + const project = workspace.projects.get(selectedProject); + const sourceRoot = project?.sourceRoot || 'src'; + const isStandalone = isStandaloneApp(host, `${sourceRoot}/main.ts`); + console.log('isStandalone --->>>>>', isStandalone); const defaultPath = await createDefaultPath(host, selectedProject); - const appModulePath = defaultPath + `${isStandalone ? '/app.component.ts' : '/app.module.ts'}`; + const appModulePath = + defaultPath + `${isStandalone ? `${sourceRoot}/main.ts` : '/app.module.ts'}`; return chain([ removeImportPath(appModulePath, targetThemeName), removeImportFromNgModuleMetadata(appModulePath, targetThemeName), removeProviderFromNgModuleMetadata(appModulePath, targetThemeName), - insertImports(appModulePath, targetThemeName, isStandalone), - insertProviders(appModulePath, targetThemeName), + insertImports(selectedProject, targetThemeName), ]); }; } @@ -195,35 +200,36 @@ export function removeProviderFromNgModuleMetadata( }; } -export function insertImports( - appModulePath: string, - selectedTheme: ThemeOptionsEnum, - isStandalone = false, -): Rule { +export function insertImports(projectName: string, selectedTheme: ThemeOptionsEnum): Rule { return (host: Tree) => { - const recorder = host.beginUpdate(appModulePath); - const source = createSourceFile(host, appModulePath); const selected = importMap.get(selectedTheme); if (!selected) { return host; } - const changes: Change[] = []; + const rules: Rule[] = []; - selected.map(({ importName, path }) => - changes.push(...addImportToModule(source, appModulePath, importName, path, isStandalone)), - ); + selected.map(({ importName, path }) => { + rules.push( + addRootImport(projectName, code => { + const configFn = code.external(importName, path); + console.log('configFn --->>>', configFn); + return code.code`${configFn}`; + }), + ); + }); - if (changes.length > 0) { + /* if (changes.length > 0) { for (const change of changes) { if (change instanceof InsertChange) { recorder.insertLeft(change.order, change.toAdd); } } } - host.commitUpdate(recorder); - return host; + host.commitUpdate(recorder);*/ + console.log(rules); + return chain(rules); }; } @@ -278,7 +284,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], []); } diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts index 78a7d2e904b..44d181a175e 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts @@ -12,19 +12,19 @@ import * as ts from 'typescript'; import { join, normalize } from '@angular-devkit/core'; import { - addImportToModule, + addRootImport, + addRootProvider, addRouteDeclarationToModule, applyWithOverwrite, - camel, getFirstApplication, getWorkspace, InsertChange, interpolate, isLibrary, + isStandaloneApp, JSONFile, kebab, pascal, - readWorkspaceSchema, resolveProject, updateWorkspace, } from '../../utils'; @@ -221,15 +221,44 @@ export function importConfigModuleToDefaultProjectAppModule( options: GenerateLibSchema, ) { return (tree: Tree) => { - const projectName = readWorkspaceSchema(tree).defaultProject || getFirstApplication(tree).name!; + // const projectName = readWorkspaceSchema(tree).defaultProject || getFirstApplication(tree).name!; + const projectName = getFirstApplication(tree).name!; const project = workspace.projects.get(projectName); - const appModulePath = `${project?.sourceRoot}/app/app.module.ts`; - const appModule = tree.read(appModulePath); - if (!appModule) { - return; + const sourceRoot = project?.sourceRoot || 'src'; + + const isSourceStandalone = isStandaloneApp(tree, `${sourceRoot}/main.ts`); + console.log('isStandalone --->>>>', isSourceStandalone); + const rules: Rule[] = []; + + if (options.isStandaloneTemplate) { + rules.push( + addRootProvider(projectName, code => { + const configFn = code.external( + `provide${pascal(packageName)}Config`, + `${kebab(packageName)}/config`, + ); + return code.code`${configFn}()`; + }), + ); + } else { + rules.push( + addRootImport(projectName, code => { + const configFn = code.external( + `${pascal(packageName)}ConfigModule`, + `${kebab(packageName)}/config`, + ); + return code.code`${configFn}()`; + }), + ); } - const appModuleContent = appModule.toString(); - if ( + + // const appModulePath = `${project?.sourceRoot}/app/app.module.ts`; + // const appModule = tree.read(appModulePath); + // if (!appModule) { + // return; + // } + // const appModuleContent = appModule.toString(); + /* if ( appModuleContent.includes( options.isStandaloneTemplate ? `provide${pascal(packageName)}Config` @@ -237,9 +266,9 @@ export function importConfigModuleToDefaultProjectAppModule( ) ) { return; - } + }*/ - const rootConfigStatement = options.isStandaloneTemplate + /* const rootConfigStatement = options.isStandaloneTemplate ? `provide${pascal(packageName)}Config()` : `${pascal(packageName)}ConfigModule.forRoot()`; const text = tree.read(appModulePath); @@ -257,7 +286,6 @@ export function importConfigModuleToDefaultProjectAppModule( appModulePath, rootConfigStatement, `${kebab(packageName)}/config`, - options.isStandaloneTemplate, ); const recorder = tree.beginUpdate(appModulePath); for (const change of changes) { @@ -265,9 +293,9 @@ export function importConfigModuleToDefaultProjectAppModule( recorder.insertLeft(change.pos, change.toAdd); } } - tree.commitUpdate(recorder); + tree.commitUpdate(recorder);*/ - return; + return chain(rules); }; } @@ -275,17 +303,37 @@ export function addRoutingToAppRoutingModule( workspace: WorkspaceDefinition, packageName: string, options: GenerateLibSchema, -) { +): Rule { return (tree: Tree) => { - const projectName = readWorkspaceSchema(tree).defaultProject || getFirstApplication(tree).name!; + // const projectName = readWorkspaceSchema(tree).defaultProject || getFirstApplication(tree).name!; + const projectName = getFirstApplication(tree).name!; const project = workspace.projects.get(projectName); + const sourceRoot = project?.sourceRoot || 'src'; + + const mainPath = `${sourceRoot}/main.ts`; + const isSourceStandalone = isStandaloneApp(tree, mainPath); + const pascalName = pascal(packageName); + const routePath = `${kebab(packageName)}`; + + if (isSourceStandalone) { + return addRootProvider(projectName, code => { + const routeExpr = options.isStandaloneTemplate + ? `() => import('${routePath}/routes').then(m => m.${pascalName}Routes)` + : `() => import('${routePath}').then(m => m.${moduleName}.forLazy())`; + + return code.code`provideRouter([ + { path: '${routePath}', loadChildren: ${routeExpr} } + ])`; + }); + } + const appRoutingModulePath = `${project?.sourceRoot}/app/app-routing.module.ts`; const appRoutingModule = tree.read(appRoutingModulePath); if (!appRoutingModule) { return; } const appRoutingModuleContent = appRoutingModule.toString(); - const moduleName = `${pascal(packageName)}Module`; + const moduleName = `${pascalName}Module`; if (appRoutingModuleContent.includes(moduleName)) { return; } @@ -296,12 +344,11 @@ export function addRoutingToAppRoutingModule( ts.ScriptTarget.Latest, true, ); - const importPath = `${kebab(packageName)}`; const importStatement = options.isStandaloneTemplate - ? `() => import('${importPath}').then(m => m.${pascal(packageName)}Routes)` - : `() => import('${importPath}').then(m => m.${moduleName}.forLazy())`; - const routeDefinition = `{ path: '${kebab(packageName)}', loadChildren: ${importStatement} }`; - const change = addRouteDeclarationToModule(source, `${kebab(packageName)}`, routeDefinition); + ? `() => import('${routePath}').then(m => m.${pascalName}Routes)` + : `() => import('${routePath}').then(m => m.${moduleName}.forLazy())`; + const routeDefinition = `{ path: '${routePath}', loadChildren: ${importStatement} }`; + const change = addRouteDeclarationToModule(source, routePath, routeDefinition); const recorder = tree.beginUpdate(appRoutingModulePath); if (change instanceof InsertChange) { diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/add-declaration-to-ng-module.ts b/npm/ng-packs/packages/schematics/src/utils/angular/add-declaration-to-ng-module.ts index f62e3922fd6..6e5dc266b7c 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/add-declaration-to-ng-module.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/add-declaration-to-ng-module.ts @@ -3,7 +3,7 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import { Rule, Tree, strings } from '@angular-devkit/schematics'; @@ -19,6 +19,7 @@ export interface DeclarationToNgModuleOptions { flat?: boolean; export?: boolean; type: string; + typeSeparator?: '.' | '-'; skipImport?: boolean; standalone?: boolean; } @@ -30,6 +31,8 @@ export function addDeclarationToNgModule(options: DeclarationToNgModuleOptions): return host; } + const typeSeparator = options.typeSeparator ?? '.'; + const sourceText = host.readText(modulePath); const source = ts.createSourceFile(modulePath, sourceText, ts.ScriptTarget.Latest, true); @@ -37,11 +40,11 @@ export function addDeclarationToNgModule(options: DeclarationToNgModuleOptions): `/${options.path}/` + (options.flat ? '' : strings.dasherize(options.name) + '/') + strings.dasherize(options.name) + - (options.type ? '.' : '') + - strings.dasherize(options.type); + (options.type ? typeSeparator + strings.dasherize(options.type) : ''); const importPath = buildRelativePath(modulePath, filePath); - const classifiedName = strings.classify(options.name) + strings.classify(options.type); + const classifiedName = + strings.classify(options.name) + (options.type ? strings.classify(options.type) : ''); const changes = addDeclarationToModule(source, modulePath, classifiedName, importPath); if (options.export) { diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/ast-utils.ts b/npm/ng-packs/packages/schematics/src/utils/angular/ast-utils.ts index 97eaa2e6306..2683e20149b 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/ast-utils.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/ast-utils.ts @@ -3,20 +3,22 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import { tags } from '@angular-devkit/core'; import * as ts from 'typescript'; import { Change, InsertChange, NoopChange } from './change'; +import { getEOL } from './eol'; /** * Add Import `import { symbolName } from fileName` if the import doesn't exit * already. Assumes fileToEdit can be resolved and accessed. - * @param fileToEdit (file we want to add import to) - * @param symbolName (item to import) - * @param fileName (path to the file) - * @param isDefault (if true, import follows style for importing default exports) + * @param fileToEdit File we want to add import to. + * @param symbolName Item to import. + * @param fileName Path to the file. + * @param isDefault If true, import follows style for importing default exports. + * @param alias Alias that the symbol should be inserted under. * @return Change */ export function insertImport( @@ -25,46 +27,40 @@ export function insertImport( symbolName: string, fileName: string, isDefault = false, + alias?: string, ): Change { const rootNode = source; - const allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration); + const allImports = findNodes(rootNode, ts.isImportDeclaration); + const importExpression = alias ? `${symbolName} as ${alias}` : symbolName; // get nodes that map to import statements from the file fileName const relevantImports = allImports.filter(node => { - // StringLiteral of the ImportDeclaration is the import file (fileName in this case). - const importFiles = node - .getChildren() - .filter(ts.isStringLiteral) - .map(n => n.text); - - return importFiles.filter(file => file === fileName).length === 1; + return ts.isStringLiteralLike(node.moduleSpecifier) && node.moduleSpecifier.text === fileName; }); if (relevantImports.length > 0) { - let importsAsterisk = false; - // imports from import file - const imports: ts.Node[] = []; - relevantImports.forEach(n => { - Array.prototype.push.apply(imports, findNodes(n, ts.SyntaxKind.Identifier)); - if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) { - importsAsterisk = true; - } + const hasNamespaceImport = relevantImports.some(node => { + return node.importClause?.namedBindings?.kind === ts.SyntaxKind.NamespaceImport; }); // if imports * from fileName, don't add symbolName - if (importsAsterisk) { + if (hasNamespaceImport) { return new NoopChange(); } - const importTextNodes = imports.filter(n => (n as ts.Identifier).text === symbolName); + const imports = relevantImports.flatMap(node => { + return node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings) + ? node.importClause.namedBindings.elements + : []; + }); // insert import if it's not there - if (importTextNodes.length === 0) { + if (!imports.some(node => (node.propertyName || node.name).text === symbolName)) { const fallbackPos = findNodes(relevantImports[0], ts.SyntaxKind.CloseBraceToken)[0].getStart() || findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].getStart(); - return insertAfterLastOccurrence(imports, `, ${symbolName}`, fileToEdit, fallbackPos); + return insertAfterLastOccurrence(imports, `, ${importExpression}`, fileToEdit, fallbackPos); } return new NoopChange(); @@ -78,12 +74,13 @@ export function insertImport( } const open = isDefault ? '' : '{ '; const close = isDefault ? '' : ' }'; + const eol = getEOL(rootNode.getText()); // if there are no imports or 'use strict' statement, insert import at beginning of file const insertAtBeginning = allImports.length === 0 && useStrict.length === 0; - const separator = insertAtBeginning ? '' : ';\n'; + const separator = insertAtBeginning ? '' : `;${eol}`; const toInsert = - `${separator}import ${open}${symbolName}${close}` + - ` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`; + `${separator}import ${open}${importExpression}${close}` + + ` from '${fileName}'${insertAtBeginning ? `;${eol}` : ''}`; return insertAfterLastOccurrence( allImports, @@ -222,7 +219,7 @@ function nodesByPosition(first: ts.Node, second: ts.Node): number { * @throw Error if toInsert is first occurence but fall back is not set */ export function insertAfterLastOccurrence( - nodes: ts.Node[], + nodes: ts.Node[] | ts.NodeArray, toInsert: string, file: string, fallbackPos: number, @@ -345,7 +342,7 @@ export function getDecoratorMetadata( export function getMetadataField( node: ts.ObjectLiteralExpression, metadataField: string, -): ts.ObjectLiteralElement[] { +): ts.PropertyAssignment[] { return ( node.properties .filter(ts.isPropertyAssignment) @@ -399,12 +396,7 @@ export function addSymbolToNgModuleMetadata( if (importPath !== null) { return [ new InsertChange(ngModulePath, position, toInsert), - insertImport( - source, - ngModulePath, - symbolName.replace(/\..*$/, '').replace(/\(\)/, ''), - importPath, - ), + insertImport(source, ngModulePath, symbolName.replace(/\..*$/, ''), importPath), ]; } else { return [new InsertChange(ngModulePath, position, toInsert)]; @@ -420,7 +412,7 @@ export function addSymbolToNgModuleMetadata( return []; } - let expresssion: ts.Expression | ts.ArrayLiteralExpression; + let expression: ts.Expression | ts.ArrayLiteralExpression; const assignmentInit = assignment.initializer; const elements = assignmentInit.elements; @@ -430,20 +422,20 @@ export function addSymbolToNgModuleMetadata( return []; } - expresssion = elements[elements.length - 1]; + expression = elements[elements.length - 1]; } else { - expresssion = assignmentInit; + expression = assignmentInit; } let toInsert: string; - let position = expresssion.getEnd(); - if (ts.isArrayLiteralExpression(expresssion)) { + let position = expression.getEnd(); + if (ts.isArrayLiteralExpression(expression)) { // We found the field but it's empty. Insert it just before the `]`. position--; toInsert = `\n${tags.indentBy(4)`${symbolName}`}\n `; } else { // Get the indentation of the last element, if any. - const text = expresssion.getFullText(source); + const text = expression.getFullText(source); const matches = text.match(/^(\r?\n)(\s*)/); if (matches) { toInsert = `,${matches[1]}${tags.indentBy(matches[2].length)`${symbolName}`}`; @@ -451,15 +443,11 @@ export function addSymbolToNgModuleMetadata( toInsert = `, ${symbolName}`; } } + if (importPath !== null) { return [ new InsertChange(ngModulePath, position, toInsert), - insertImport( - source, - ngModulePath, - symbolName.replace(/\..*$/, '').replace(/\(\)/, ''), - importPath, - ), + insertImport(source, ngModulePath, symbolName.replace(/\..*$/, ''), importPath), ]; } @@ -493,15 +481,8 @@ export function addImportToModule( modulePath: string, classifiedName: string, importPath: string, - isStandalone = false, ): Change[] { - return addSymbolToNgModuleMetadata( - source, - modulePath, - isStandalone ? 'providers' : 'imports', - classifiedName, - importPath, - ); + return addSymbolToNgModuleMetadata(source, modulePath, 'imports', classifiedName, importPath); } /** @@ -579,13 +560,9 @@ export function getRouterModuleDeclaration(source: ts.SourceFile): ts.Expression } const matchingProperties = getMetadataField(node, 'imports'); - if (!matchingProperties) { - return; - } - - const assignment = matchingProperties[0] as ts.PropertyAssignment; + const assignment = matchingProperties[0]; - if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) { + if (!assignment || assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) { return; } @@ -685,3 +662,52 @@ export function addRouteDeclarationToModule( return new InsertChange(fileToAdd, insertPos, route); } + +/** Asserts if the specified node is a named declaration (e.g. class, interface). */ +function isNamedNode( + node: ts.Node & { name?: ts.Node }, +): node is ts.Node & { name: ts.Identifier } { + return !!node.name && ts.isIdentifier(node.name); +} + +/** + * Determines if a SourceFile has a top-level declaration whose name matches a specific symbol. + * Can be used to avoid conflicts when inserting new imports into a file. + * @param sourceFile File in which to search. + * @param symbolName Name of the symbol to search for. + * @param skipModule Path of the module that the symbol may have been imported from. Used to + * avoid false positives where the same symbol we're looking for may have been imported. + */ +export function hasTopLevelIdentifier( + sourceFile: ts.SourceFile, + symbolName: string, + skipModule: string | null = null, +): boolean { + for (const node of sourceFile.statements) { + if (isNamedNode(node) && node.name.text === symbolName) { + return true; + } + + if ( + ts.isVariableStatement(node) && + node.declarationList.declarations.some(decl => { + return isNamedNode(decl) && decl.name.text === symbolName; + }) + ) { + return true; + } + + if ( + ts.isImportDeclaration(node) && + ts.isStringLiteralLike(node.moduleSpecifier) && + node.moduleSpecifier.text !== skipModule && + node.importClause?.namedBindings && + ts.isNamedImports(node.importClause.namedBindings) && + node.importClause.namedBindings.elements.some(el => el.name.text === symbolName) + ) { + return true; + } + } + + return false; +} diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/change.ts b/npm/ng-packs/packages/schematics/src/utils/angular/change.ts index 08df56a6ddf..62e3ab659a3 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/change.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/change.ts @@ -3,7 +3,7 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import { UpdateRecorder } from '@angular-devkit/schematics'; @@ -47,7 +47,11 @@ export class InsertChange implements Change { order: number; description: string; - constructor(public path: string, public pos: number, public toAdd: string) { + constructor( + public path: string, + public pos: number, + public toAdd: string, + ) { if (pos < 0) { throw new Error('Negative positions are invalid'); } @@ -59,7 +63,7 @@ export class InsertChange implements Change { * This method does not insert spaces if there is none in the original string. */ apply(host: Host) { - return host.read(this.path).then((content) => { + return host.read(this.path).then(content => { const prefix = content.substring(0, this.pos); const suffix = content.substring(this.pos); @@ -75,7 +79,11 @@ export class RemoveChange implements Change { order: number; description: string; - constructor(public path: string, private pos: number, public toRemove: string) { + constructor( + public path: string, + private pos: number, + public toRemove: string, + ) { if (pos < 0) { throw new Error('Negative positions are invalid'); } @@ -84,7 +92,7 @@ export class RemoveChange implements Change { } apply(host: Host): Promise { - return host.read(this.path).then((content) => { + return host.read(this.path).then(content => { const prefix = content.substring(0, this.pos); const suffix = content.substring(this.pos + this.toRemove.length); @@ -115,7 +123,7 @@ export class ReplaceChange implements Change { } apply(host: Host): Promise { - return host.read(this.path).then((content) => { + return host.read(this.path).then(content => { const prefix = content.substring(0, this.pos); const suffix = content.substring(this.pos + this.oldText.length); const text = content.substring(this.pos, this.pos + this.oldText.length); diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/dependencies.ts b/npm/ng-packs/packages/schematics/src/utils/angular/dependencies.ts index c9aa6171910..06c4f38653b 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/dependencies.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/dependencies.ts @@ -3,7 +3,7 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import { Tree } from '@angular-devkit/schematics'; diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/dependency.ts b/npm/ng-packs/packages/schematics/src/utils/angular/dependency.ts index 3522b59042f..2f3501c33a8 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/dependency.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/dependency.ts @@ -3,12 +3,12 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import { Rule, SchematicContext } from '@angular-devkit/schematics'; import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; -import * as path from 'path'; +import * as path from 'node:path'; const installTasks = new WeakMap>(); @@ -41,11 +41,13 @@ export enum InstallBehavior { * which may install the dependency. */ None, + /** * Automatically determine the need to schedule a {@link NodePackageInstallTask} based on * previous usage of the {@link addDependency} within the schematic. */ Auto, + /** * Always schedule a {@link NodePackageInstallTask} when the rule is executed. */ @@ -62,6 +64,7 @@ export enum ExistingBehavior { * The dependency will not be added or otherwise changed if it already exists. */ Skip, + /** * The dependency's existing specifier will be replaced with the specifier provided in the * {@link addDependency} call. A warning will also be shown during schematic execution to @@ -95,17 +98,20 @@ export function addDependency( * dependency will be added. Defaults to {@link DependencyType.Default} (`dependencies`). */ type?: DependencyType; + /** * The path of the package manifest file (`package.json`) that will be modified. * Defaults to `/package.json`. */ packageJsonPath?: string; + /** * The dependency installation behavior to use to determine whether a * {@link NodePackageInstallTask} should be scheduled after adding the dependency. * Defaults to {@link InstallBehavior.Auto}. */ install?: InstallBehavior; + /** * The behavior to use when the dependency already exists within the `package.json`. * Defaults to {@link ExistingBehavior.Replace}. diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/find-module.ts b/npm/ng-packs/packages/schematics/src/utils/angular/find-module.ts index 112269d5e5d..085592abaa7 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/find-module.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/find-module.ts @@ -3,7 +3,7 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import { NormalizedRoot, Path, dirname, join, normalize, relative } from '@angular-devkit/core'; @@ -54,12 +54,12 @@ export function findModuleFromOptions(host: Tree, options: ModuleOptions): Path const candidatesDirs = [...candidateSet].sort((a, b) => b.length - a.length); for (const c of candidatesDirs) { - const candidateFiles = ['', `${moduleBaseName}.ts`, `${moduleBaseName}${moduleExt}`].map( - (x) => join(c, x), + const candidateFiles = ['', `${moduleBaseName}.ts`, `${moduleBaseName}${moduleExt}`].map(x => + join(c, x), ); for (const sc of candidateFiles) { - if (host.exists(sc)) { + if (host.exists(sc) && host.readText(sc).includes('@NgModule')) { return normalize(sc); } } @@ -85,8 +85,8 @@ export function findModule( let foundRoutingModule = false; while (dir) { - const allMatches = dir.subfiles.filter((p) => p.endsWith(moduleExt)); - const filteredMatches = allMatches.filter((p) => !p.endsWith(routingModuleExt)); + const allMatches = dir.subfiles.filter(p => p.endsWith(moduleExt)); + const filteredMatches = allMatches.filter(p => !p.endsWith(routingModuleExt)); foundRoutingModule = foundRoutingModule || allMatches.length !== filteredMatches.length; diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/generate-from-files.ts b/npm/ng-packs/packages/schematics/src/utils/angular/generate-from-files.ts index 169c6c389ab..946c4119d38 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/generate-from-files.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/generate-from-files.ts @@ -3,16 +3,18 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import { + FileOperator, Rule, Tree, apply, applyTemplates, chain, filter, + forEach, mergeWith, move, noop, @@ -30,6 +32,8 @@ export interface GenerateFromFilesOptions { prefix?: string; project: string; skipTests?: boolean; + templateFilesDirectory?: string; + type?: string; } export function generateFromFiles( @@ -41,19 +45,33 @@ export function generateFromFiles( options.prefix ??= ''; options.flat ??= true; + // Schematic templates require a defined type value + options.type ??= ''; + const parsedPath = parseName(options.path, options.name); options.name = parsedPath.name; options.path = parsedPath.path; validateClassName(strings.classify(options.name)); - const templateSource = apply(url('./files'), [ + const templateFilesDirectory = options.templateFilesDirectory ?? './files'; + const templateSource = apply(url(templateFilesDirectory), [ options.skipTests ? filter(path => !path.endsWith('.spec.ts.template')) : noop(), applyTemplates({ ...strings, ...options, ...extraTemplateValues, }), + !options.type + ? forEach((file => { + return file.path.includes('..') + ? { + content: file.content, + path: file.path.replace('..', '.'), + } + : file; + }) as FileOperator) + : noop(), move(parsedPath.path + (options.flat ? '' : '/' + strings.dasherize(options.name))), ]); diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/index.ts b/npm/ng-packs/packages/schematics/src/utils/angular/index.ts index abf2992d90e..2c1bfd086e3 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/index.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/index.ts @@ -11,3 +11,4 @@ export * from './project-targets'; export * from './validation'; export * from './workspace'; export * from './workspace-models'; +export * from './standalone'; diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/json-file.ts b/npm/ng-packs/packages/schematics/src/utils/angular/json-file.ts index 6bb532416f2..26a06e16a62 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/json-file.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/json-file.ts @@ -3,7 +3,7 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import { JsonValue } from '@angular-devkit/core'; @@ -18,16 +18,22 @@ import { parseTree, printParseErrorCode, } from 'jsonc-parser'; +import { getEOL } from './eol'; export type InsertionIndex = (properties: string[]) => number; export type JSONPath = (string | number)[]; -/** @internal */ +/** @private */ export class JSONFile { content: string; + private eol: string; - constructor(private readonly host: Tree, private readonly path: string) { + constructor( + private readonly host: Tree, + private readonly path: string, + ) { this.content = this.host.readText(this.path); + this.eol = getEOL(this.content); } private _jsonAst: Node | undefined; @@ -73,15 +79,17 @@ export class JSONFile { let getInsertionIndex: InsertionIndex | undefined; if (insertInOrder === undefined) { const property = jsonPath.slice(-1)[0]; - getInsertionIndex = (properties) => - [...properties, property].sort().findIndex((p) => p === property); + getInsertionIndex = properties => + [...properties, property].sort().findIndex(p => p === property); } else if (insertInOrder !== false) { getInsertionIndex = insertInOrder; } const edits = modify(this.content, jsonPath, value, { getInsertionIndex, + formattingOptions: { + eol: this.eol, insertSpaces: true, tabSize: 2, }, diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/ng-ast-utils.ts b/npm/ng-packs/packages/schematics/src/utils/angular/ng-ast-utils.ts index 75962f31d71..b4eb1fd0563 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/ng-ast-utils.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/ng-ast-utils.ts @@ -3,14 +3,14 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -import { normalize } from '@angular-devkit/core'; import { SchematicsException, Tree } from '@angular-devkit/schematics'; -import { dirname } from 'path'; +import { dirname, join } from 'node:path/posix'; import * as ts from 'typescript'; import { findNode, getSourceNodes } from './ast-utils'; +import { findBootstrapApplicationCall } from './standalone/util'; export function findBootstrapModuleCall(host: Tree, mainPath: string): ts.CallExpression | null { const mainText = host.readText(mainPath); @@ -46,7 +46,7 @@ export function findBootstrapModuleCall(host: Tree, mainPath: string): ts.CallEx return bootstrapCall; } -export function findBootstrapModulePath(host: Tree, mainPath: string): string { +function findBootstrapModulePath(host: Tree, mainPath: string): string { const bootstrapCall = findBootstrapModuleCall(host, mainPath); if (!bootstrapCall) { throw new SchematicsException('Bootstrap call not found'); @@ -74,7 +74,21 @@ export function findBootstrapModulePath(host: Tree, mainPath: string): string { export function getAppModulePath(host: Tree, mainPath: string): string { const moduleRelativePath = findBootstrapModulePath(host, mainPath); const mainDir = dirname(mainPath); - const modulePath = normalize(`/${mainDir}/${moduleRelativePath}.ts`); + const modulePath = join(mainDir, `${moduleRelativePath}.ts`); return modulePath; } + +export function isStandaloneApp(host: Tree, mainPath: string): boolean { + try { + findBootstrapApplicationCall(host, mainPath); + + return true; + } catch (error) { + if (error instanceof SchematicsException) { + return false; + } + + throw error; + } +} diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/parse-name.ts b/npm/ng-packs/packages/schematics/src/utils/angular/parse-name.ts index 01227198c90..37a019e940d 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/parse-name.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/parse-name.ts @@ -3,7 +3,7 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import { Path, basename, dirname, join, normalize } from '@angular-devkit/core'; diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/paths.ts b/npm/ng-packs/packages/schematics/src/utils/angular/paths.ts index dffa9e841d0..28420188028 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/paths.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/paths.ts @@ -3,17 +3,15 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ -import { normalize, split } from '@angular-devkit/core'; +import { join, relative } from 'node:path/posix'; export function relativePathToWorkspaceRoot(projectRoot: string | undefined): string { - const normalizedPath = split(normalize(projectRoot || '')); - - if (normalizedPath.length === 0 || !normalizedPath[0]) { + if (!projectRoot) { return '.'; - } else { - return normalizedPath.map(() => '..').join('/'); } + + return relative(join('/', projectRoot), '/') || '.'; } diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/project-targets.ts b/npm/ng-packs/packages/schematics/src/utils/angular/project-targets.ts index 7f4b7ba8c64..8897a3ddab6 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/project-targets.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/project-targets.ts @@ -3,11 +3,21 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import { SchematicsException } from '@angular-devkit/schematics'; +import { ProjectDefinition } from './workspace'; +import { Builders } from './workspace-models'; export function targetBuildNotFoundError(): SchematicsException { return new SchematicsException(`Project target "build" not found.`); } + +export function isUsingApplicationBuilder(project: ProjectDefinition): boolean { + const buildBuilder = project.targets.get('build')?.builder; + const isUsingApplicationBuilder = + buildBuilder === Builders.Application || buildBuilder === Builders.BuildApplication; + + return isUsingApplicationBuilder; +} diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/validation.ts b/npm/ng-packs/packages/schematics/src/utils/angular/validation.ts index 619fe8e924b..8b380d1b826 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/validation.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/validation.ts @@ -3,14 +3,15 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import { SchematicsException } from '@angular-devkit/schematics'; // Must start with a letter, and must contain only alphanumeric characters or dashes. // When adding a dash the segment after the dash must also start with a letter. -export const htmlSelectorRe = /^[a-zA-Z][.0-9a-zA-Z]*(:?-[a-zA-Z][.0-9a-zA-Z]*)*$/; +export const htmlSelectorRe = + /^[a-zA-Z][.0-9a-zA-Z]*((:?-[0-9]+)*|(:?-[a-zA-Z][.0-9a-zA-Z]*(:?-[0-9]+)*)*)$/; // See: https://github.com/tc39/proposal-regexp-unicode-property-escapes/blob/fe6d07fad74cd0192d154966baa1e95e7cda78a1/README.md#other-examples const ecmaIdentifierNameRegExp = /^(?:[$_\p{ID_Start}])(?:[$_\u200C\u200D\p{ID_Continue}])*$/u; diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/workspace-models.ts b/npm/ng-packs/packages/schematics/src/utils/angular/workspace-models.ts index fb7f18c3f7c..34c329b470d 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/workspace-models.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/workspace-models.ts @@ -3,7 +3,7 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ export enum ProjectType { @@ -18,16 +18,24 @@ export enum ProjectType { * `angular.json` workspace file. */ export enum Builders { + Application = '@angular-devkit/build-angular:application', AppShell = '@angular-devkit/build-angular:app-shell', Server = '@angular-devkit/build-angular:server', Browser = '@angular-devkit/build-angular:browser', + SsrDevServer = '@angular-devkit/build-angular:ssr-dev-server', + Prerender = '@angular-devkit/build-angular:prerender', + BrowserEsbuild = '@angular-devkit/build-angular:browser-esbuild', Karma = '@angular-devkit/build-angular:karma', + BuildKarma = '@angular/build:karma', TsLint = '@angular-devkit/build-angular:tslint', - DeprecatedNgPackagr = '@angular-devkit/build-ng-packagr:build', NgPackagr = '@angular-devkit/build-angular:ng-packagr', + BuildNgPackagr = '@angular/build:ng-packagr', DevServer = '@angular-devkit/build-angular:dev-server', + BuildDevServer = '@angular/build:dev-server', ExtractI18n = '@angular-devkit/build-angular:extract-i18n', - Protractor = '@angular-devkit/build-angular:protractor', + BuildExtractI18n = '@angular/build:extract-i18n', + Protractor = '@angular-devkit/build-angular:private-protractor', + BuildApplication = '@angular/build:application', } export interface FileReplacements { @@ -70,8 +78,9 @@ export interface BrowserBuilderOptions extends BrowserBuilderBaseOptions { } export interface ServeBuilderOptions { - browserTarget: string; + buildTarget: string; } + export interface LibraryBuilderOptions { tsConfig: string; project: string; @@ -138,11 +147,9 @@ export type E2EBuilderTarget = BuilderTarget; interface WorkspaceCLISchema { warnings?: Record; schematicCollections?: string[]; - defaultCollection?: string; } export interface WorkspaceSchema { version: 1; - defaultProject?: string; cli?: WorkspaceCLISchema; projects: { [key: string]: WorkspaceProject; @@ -165,6 +172,7 @@ export interface WorkspaceProject; + /** * Tool options. */ diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/workspace.ts b/npm/ng-packs/packages/schematics/src/utils/angular/workspace.ts index 410c079e2fa..b831458edf4 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/workspace.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/workspace.ts @@ -3,7 +3,7 @@ * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license + * found in the LICENSE file at https://angular.dev/license */ import { json, workspaces } from '@angular-devkit/core'; @@ -20,7 +20,7 @@ export type TargetDefinition = workspaces.TargetDefinition; /** * A {@link workspaces.WorkspaceHost} backed by a Schematics {@link Tree} instance. */ -class TreeWorkspaceHost implements workspaces.WorkspaceHost { +export class TreeWorkspaceHost implements workspaces.WorkspaceHost { constructor(private readonly tree: Tree) {} async readFile(path: string): Promise { @@ -58,14 +58,12 @@ class TreeWorkspaceHost implements workspaces.WorkspaceHost { export function updateWorkspace( updater: (workspace: WorkspaceDefinition) => void | Rule | PromiseLike, ): Rule { - return async (tree: Tree) => { - const host = new TreeWorkspaceHost(tree); - - const { workspace } = await workspaces.readWorkspace(DEFAULT_WORKSPACE_PATH, host); + return async (host: Tree) => { + const workspace = await getWorkspace(host); const result = await updater(workspace); - await workspaces.writeWorkspace(workspace, host); + await workspaces.writeWorkspace(workspace, new TreeWorkspaceHost(host)); return result || noop; }; diff --git a/npm/ng-packs/packages/schematics/src/utils/workspace.ts b/npm/ng-packs/packages/schematics/src/utils/workspace.ts index 6218bc038e0..9c71d318642 100644 --- a/npm/ng-packs/packages/schematics/src/utils/workspace.ts +++ b/npm/ng-packs/packages/schematics/src/utils/workspace.ts @@ -13,17 +13,6 @@ export function isLibrary(project: workspaces.ProjectDefinition): boolean { return project.extensions['projectType'] === ProjectType.Library; } -export function applicationHasStandaloneTemplate(tree: Tree, selectedProject?: string): boolean { - const workspace = readWorkspaceSchema(tree); - const project = workspace.projects[selectedProject ?? 0]; - - const mainPath = project.sourceRoot + '/main.ts'; - const mainSource = readFileInTree(tree, mainPath); - const mainContent = mainSource.toString(); - - return mainContent.includes('bootstrapComponent'); -} - export function readEnvironment(tree: Tree, project: workspaces.ProjectDefinition) { if (isLibrary(project)) return undefined; @@ -63,7 +52,8 @@ export async function resolveProject( // @typescript-eslint/no-explicit-any notFoundValue: T = NOT_FOUND_VALUE as unknown as any, ): Promise { - name = name || readWorkspaceSchema(tree).defaultProject || getFirstApplication(tree).name!; + // name = name || readWorkspaceSchema(tree).defaultProject || getFirstApplication(tree).name!; + name = name || getFirstApplication(tree).name!; const workspace = await getWorkspace(tree); let definition: Project['definition'] | undefined; From 45f0d38904815e8e831f3e8c19628153ece2149e Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Wed, 16 Apr 2025 01:57:09 +0300 Subject: [PATCH 05/27] schematics standalone updates --- .../src/commands/change-theme/index.ts | 69 +++++++------------ .../src/commands/change-theme/style-map.ts | 16 +++-- 2 files changed, 37 insertions(+), 48 deletions(-) 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 a2f55cf2bba..077d2298630 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 @@ -6,22 +6,16 @@ import { allStyles, importMap, styleMap } from './style-map'; import { ChangeThemeOptions } from './model'; import { addRootImport, - Change, + addRootProvider, createDefaultPath, getWorkspace, - InsertChange, isLibrary, isStandaloneApp, updateWorkspace, WorkspaceDefinition, } from '../../utils'; import { ThemeOptionsEnum } from './theme-options.enum'; -import { - addProviderToModule, - findNodes, - getDecoratorMetadata, - getMetadataField, -} from '../../utils/angular/ast-utils'; +import { findNodes, getDecoratorMetadata, getMetadataField } from '../../utils/angular/ast-utils'; export default function (_options: ChangeThemeOptions): Rule { return async () => { @@ -84,6 +78,7 @@ function updateAppModule(selectedProject: string, targetThemeName: ThemeOptionsE removeImportFromNgModuleMetadata(appModulePath, targetThemeName), removeProviderFromNgModuleMetadata(appModulePath, targetThemeName), insertImports(selectedProject, targetThemeName), + insertProviders(selectedProject, targetThemeName), ]); }; } @@ -201,64 +196,52 @@ export function removeProviderFromNgModuleMetadata( } export function insertImports(projectName: string, selectedTheme: ThemeOptionsEnum): Rule { - return (host: Tree) => { + return addRootImport(projectName, code => { const selected = importMap.get(selectedTheme); + if (!selected || selected.length === 0) return code.code``; - if (!selected) { - return host; - } + const expressions: string[] = []; - const rules: Rule[] = []; - - selected.map(({ importName, path }) => { - rules.push( - addRootImport(projectName, code => { - const configFn = code.external(importName, path); - console.log('configFn --->>>', configFn); - return code.code`${configFn}`; - }), - ); - }); - - /* 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);*/ - console.log(rules); - return chain(rules); - }; + + return code.code`${expressions.join(',\n')}`; + }); } -export function insertProviders(appModulePath: string, selectedTheme: ThemeOptionsEnum): Rule { +export function insertProviders(projectName: string, selectedTheme: ThemeOptionsEnum): Rule { return (host: Tree) => { - const recorder = host.beginUpdate(appModulePath); - const source = createSourceFile(host, appModulePath); + // const recorder = host.beginUpdate(appModulePath); + // const source = createSourceFile(host, appModulePath); const selected = importMap.get(selectedTheme); if (!selected) { return host; } - const changes: Change[] = []; + const rules: Rule[] = []; selected.map(({ path, provider }) => { if (provider) { - changes.push(...addProviderToModule(source, appModulePath, provider + '()', path)); + rules.push( + addRootProvider(projectName, code => { + const configFn = code.external(provider, path); + return code.code`${configFn}()`; + }), + ); } }); - for (const change of changes) { + /* for (const change of changes) { if (change instanceof InsertChange) { recorder.insertLeft(change.order, change.toAdd); } - } + }*/ - host.commitUpdate(recorder); - return host; + // host.commitUpdate(recorder); + return chain(rules); }; } 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 7dc9bf8053f..7c589f92128 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 @@ -12,6 +12,7 @@ export type ImportDefinition = { path: string; importName: string; provider?: string; + expression?: string; }; export const styleMap = new Map(); @@ -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()', }, ]); From a95283eb502f779842aea4bd1a07a65c1bdee128 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Wed, 16 Apr 2025 12:25:35 +0300 Subject: [PATCH 06/27] refactoring --- .../src/commands/change-theme/index.ts | 37 ++++++------------- 1 file changed, 11 insertions(+), 26 deletions(-) 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 077d2298630..4a482a01d8b 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 @@ -212,37 +212,22 @@ export function insertImports(projectName: string, selectedTheme: ThemeOptionsEn } export function insertProviders(projectName: string, selectedTheme: ThemeOptionsEnum): Rule { - return (host: Tree) => { - // const recorder = host.beginUpdate(appModulePath); - // const source = createSourceFile(host, appModulePath); + return addRootProvider(projectName, code => { const selected = importMap.get(selectedTheme); + if (!selected || selected.length === 0) return code.code``; - if (!selected) { - return host; - } - - const rules: Rule[] = []; - - selected.map(({ path, provider }) => { - if (provider) { - rules.push( - addRootProvider(projectName, code => { - const configFn = code.external(provider, path); - return code.code`${configFn}()`; - }), - ); - } - }); + const expressions: string[] = []; - /* for (const change of changes) { - if (change instanceof InsertChange) { - recorder.insertLeft(change.order, change.toAdd); + for (const { path, provider } of selected) { + if (!provider) { + continue; } - }*/ + const imported = code.external(provider, path); + expressions.push(`${imported}()`); + } - // host.commitUpdate(recorder); - return chain(rules); - }; + return code.code`${expressions.join(',\n')}`; + }); } export function createSourceFile(host: Tree, appModulePath: string): ts.SourceFile { From e05175b4a9b78792b10022bd147806f95e744634 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Wed, 16 Apr 2025 12:53:52 +0300 Subject: [PATCH 07/27] refactoring --- .../src/commands/change-theme/index.ts | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) 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 4a482a01d8b..22b8ccf2ba1 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 @@ -85,32 +85,37 @@ function updateAppModule(selectedProject: string, targetThemeName: ThemeOptionsE export function removeImportPath(appModulePath: string, selectedTheme: ThemeOptionsEnum): Rule { return (host: Tree) => { + const buffer = host.read(appModulePath); + if (!buffer) return host; + + const sourceText = buffer.toString('utf-8'); + const source = ts.createSourceFile(appModulePath, sourceText, ts.ScriptTarget.Latest, true); const recorder = host.beginUpdate(appModulePath); - const source = createSourceFile(host, appModulePath); + 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]; // remove .forRoot() - return !!(moduleName && sourceModule.match(moduleName)); - }), - ); + const matchesPath = path && importPath === path; + 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; From 9e36de5aecf3d8a7d91eb66981827e752b9a68e4 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Wed, 16 Apr 2025 15:51:33 +0300 Subject: [PATCH 08/27] refactoring --- .../src/commands/change-theme/index.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) 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 22b8ccf2ba1..9fae76efe25 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 @@ -7,8 +7,7 @@ import { ChangeThemeOptions } from './model'; import { addRootImport, addRootProvider, - createDefaultPath, - getWorkspace, + getAppModulePath, isLibrary, isStandaloneApp, updateWorkspace, @@ -16,6 +15,7 @@ import { } from '../../utils'; import { ThemeOptionsEnum } from './theme-options.enum'; import { findNodes, getDecoratorMetadata, getMetadataField } from '../../utils/angular/ast-utils'; +import { getMainFilePath } from '../../utils/angular/standalone/util'; export default function (_options: ChangeThemeOptions): Rule { return async () => { @@ -64,14 +64,12 @@ function updateProjectStyle( function updateAppModule(selectedProject: string, targetThemeName: ThemeOptionsEnum): Rule { return async (host: Tree) => { - const workspace = await getWorkspace(host); - const project = workspace.projects.get(selectedProject); - const sourceRoot = project?.sourceRoot || 'src'; - const isStandalone = isStandaloneApp(host, `${sourceRoot}/main.ts`); + const mainFilePath = await getMainFilePath(host, selectedProject); + console.log('main file path --->>>>>', mainFilePath); + const isStandalone = isStandaloneApp(host, mainFilePath); console.log('isStandalone --->>>>>', isStandalone); - const defaultPath = await createDefaultPath(host, selectedProject); - const appModulePath = - defaultPath + `${isStandalone ? `${sourceRoot}/main.ts` : '/app.module.ts'}`; + const appModulePath = isStandalone ? mainFilePath : getAppModulePath(host, mainFilePath); + console.log('app module path --->>>>>', appModulePath); return chain([ removeImportPath(appModulePath, targetThemeName), From ef9850506733de6915fd4d510c2b3d399ffb8faa Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Wed, 16 Apr 2025 16:15:03 +0300 Subject: [PATCH 09/27] theme remove from import on standalone app --- .../src/commands/change-theme/index.ts | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) 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 9fae76efe25..48e2ad10087 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 @@ -15,7 +15,8 @@ import { } from '../../utils'; import { ThemeOptionsEnum } from './theme-options.enum'; import { findNodes, getDecoratorMetadata, getMetadataField } from '../../utils/angular/ast-utils'; -import { getMainFilePath } from '../../utils/angular/standalone/util'; +import { findBootstrapApplicationCall, getMainFilePath } from '../../utils/angular/standalone/util'; +import { findAppConfig } from '../../utils/angular/standalone/app_config'; export default function (_options: ChangeThemeOptions): Rule { return async () => { @@ -65,11 +66,10 @@ function updateProjectStyle( function updateAppModule(selectedProject: string, targetThemeName: ThemeOptionsEnum): Rule { return async (host: Tree) => { const mainFilePath = await getMainFilePath(host, selectedProject); - console.log('main file path --->>>>>', mainFilePath); const isStandalone = isStandaloneApp(host, mainFilePath); - console.log('isStandalone --->>>>>', isStandalone); - const appModulePath = isStandalone ? mainFilePath : getAppModulePath(host, mainFilePath); - console.log('app module path --->>>>>', appModulePath); + const appModulePath = isStandalone + ? getAppConfigPath(host, mainFilePath) + : getAppModulePath(host, mainFilePath); return chain([ removeImportPath(appModulePath, targetThemeName), @@ -81,16 +81,16 @@ function updateAppModule(selectedProject: string, targetThemeName: ThemeOptionsE }; } -export function removeImportPath(appModulePath: string, selectedTheme: ThemeOptionsEnum): Rule { +export function removeImportPath(filePath: string, selectedTheme: ThemeOptionsEnum): Rule { return (host: Tree) => { - const buffer = host.read(appModulePath); + const buffer = host.read(filePath); if (!buffer) return host; const sourceText = buffer.toString('utf-8'); - const source = ts.createSourceFile(appModulePath, sourceText, ts.ScriptTarget.Latest, true); - const recorder = host.beginUpdate(appModulePath); + const source = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true); + const recorder = host.beginUpdate(filePath); - const impMap = getImportPaths(selectedTheme, true); + const impMap = getImportPaths(selectedTheme, true); // all theme-related imports const nodes = findNodes(source, ts.isImportDeclaration); @@ -99,9 +99,9 @@ export function removeImportPath(appModulePath: string, selectedTheme: ThemeOpti const namedBindings = node.importClause?.namedBindings; return impMap.some(({ path, importName }) => { - const symbol = importName.split('.')[0]; // remove .forRoot() + const symbol = importName.split('.')[0]; // Ex: ThemeXModule from ThemeXModule.forRoot() + const matchesPath = !!path && importPath === path; - const matchesPath = path && importPath === path; const matchesSymbol = !!namedBindings && ts.isNamedImports(namedBindings) && @@ -300,3 +300,9 @@ export const styleCompareFn = (item1: string | object, item2: string | object) = return o1.bundleName && o2.bundleName && o1.bundleName == o2.bundleName; }; + +export const getAppConfigPath = (host: Tree, mainFilePath: string): string => { + const bootstrapCall = findBootstrapApplicationCall(host, mainFilePath); + const appConfig = findAppConfig(bootstrapCall, host, mainFilePath); + return appConfig?.filePath || ''; +}; From 26b6c84ee88e32aa95e80284c8c631c0afa6b135 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Wed, 16 Apr 2025 17:23:11 +0300 Subject: [PATCH 10/27] change theme command refactoring --- .../src/commands/change-theme/index.ts | 38 ++++++++++++++++++- 1 file changed, 36 insertions(+), 2 deletions(-) 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 48e2ad10087..4f70d5ea292 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 @@ -73,8 +73,10 @@ function updateAppModule(selectedProject: string, targetThemeName: ThemeOptionsE return chain([ removeImportPath(appModulePath, targetThemeName), - removeImportFromNgModuleMetadata(appModulePath, targetThemeName), - removeProviderFromNgModuleMetadata(appModulePath, targetThemeName), + ...(!isStandalone ? [removeImportFromNgModuleMetadata(appModulePath, targetThemeName)] : []), + isStandalone + ? removeImportsFromStandaloneProviders(appModulePath, targetThemeName) + : removeProviderFromNgModuleMetadata(appModulePath, targetThemeName), insertImports(selectedProject, targetThemeName), insertProviders(selectedProject, targetThemeName), ]); @@ -159,6 +161,38 @@ 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); + console.log('remove from standalone main path --->>>>', mainPath); + const recorder = host.beginUpdate(mainPath); + + const impMap = getImportPaths(selectedTheme, true); + + const callExpressions = findNodes(source, ts.isCallExpression); + + for (const expr of callExpressions) { + const text = expr.getText(); + + const match = impMap.find(({ importName }) => text.includes(importName.split('.')[0])); + + if (match) { + recorder.remove(expr.getStart(), expr.getWidth() + 1); + } + } + + host.commitUpdate(recorder); + return host; + }; +} + export function removeProviderFromNgModuleMetadata( appModulePath: string, selectedTheme: ThemeOptionsEnum, From 7a9394ac886f520a51b422c2379877875c5f5314 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Thu, 17 Apr 2025 01:21:28 +0300 Subject: [PATCH 11/27] change theme command refactoring --- .../src/commands/change-theme/index.ts | 48 ++++++++++++------- 1 file changed, 30 insertions(+), 18 deletions(-) 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 4f70d5ea292..0340f6a647f 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 @@ -92,7 +92,7 @@ export function removeImportPath(filePath: string, selectedTheme: ThemeOptionsEn const source = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true); const recorder = host.beginUpdate(filePath); - const impMap = getImportPaths(selectedTheme, true); // all theme-related imports + const impMap = getImportPaths(selectedTheme, true); const nodes = findNodes(source, ts.isImportDeclaration); @@ -101,7 +101,7 @@ export function removeImportPath(filePath: string, selectedTheme: ThemeOptionsEn const namedBindings = node.importClause?.namedBindings; return impMap.some(({ path, importName }) => { - const symbol = importName.split('.')[0]; // Ex: ThemeXModule from ThemeXModule.forRoot() + const symbol = importName.split('.')[0]; const matchesPath = !!path && importPath === path; const matchesSymbol = @@ -171,20 +171,35 @@ export function removeImportsFromStandaloneProviders( const sourceText = buffer.toString('utf-8'); const source = ts.createSourceFile(mainPath, sourceText, ts.ScriptTarget.Latest, true); - console.log('remove from standalone main path --->>>>', mainPath); const recorder = host.beginUpdate(mainPath); const impMap = getImportPaths(selectedTheme, true); - const callExpressions = findNodes(source, ts.isCallExpression); for (const expr of callExpressions) { - const text = expr.getText(); - - const match = impMap.find(({ importName }) => text.includes(importName.split('.')[0])); + 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) { - recorder.remove(expr.getStart(), expr.getWidth() + 1); + 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); + } } } @@ -253,17 +268,14 @@ export function insertProviders(projectName: string, selectedTheme: ThemeOptions const selected = importMap.get(selectedTheme); if (!selected || selected.length === 0) return code.code``; - const expressions: string[] = []; - - for (const { path, provider } of selected) { - if (!provider) { - continue; - } - const imported = code.external(provider, path); - expressions.push(`${imported}()`); - } + const providers = selected + .filter(s => !!s.provider) + .map(({ provider, path }) => { + const symbol = code.external(provider!, path); + return `${symbol}()`; + }); - return code.code`${expressions.join(',\n')}`; + return code.code`${providers}`; }); } From 331c2bec8e72c782db09a7a4b4f2b6b631d71e8a Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Thu, 17 Apr 2025 01:45:21 +0300 Subject: [PATCH 12/27] format file func added --- .../schematics/src/commands/change-theme/index.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) 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 0340f6a647f..9ef5dd1690c 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 @@ -79,6 +79,7 @@ function updateAppModule(selectedProject: string, targetThemeName: ThemeOptionsE : removeProviderFromNgModuleMetadata(appModulePath, targetThemeName), insertImports(selectedProject, targetThemeName), insertProviders(selectedProject, targetThemeName), + formatFile(appModulePath), ]); }; } @@ -352,3 +353,17 @@ export const getAppConfigPath = (host: Tree, mainFilePath: string): string => { const appConfig = findAppConfig(bootstrapCall, host, mainFilePath); return appConfig?.filePath || ''; }; + +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; + }; +}; From c7718ec923da829510c68ca4818204a9a9c74316 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Thu, 17 Apr 2025 11:31:58 +0300 Subject: [PATCH 13/27] create-lib schema updated --- .../src/commands/create-lib/index.ts | 32 +++++++++---------- .../create-lib/models/generate-lib-schema.ts | 4 +-- .../src/commands/create-lib/schema.json | 29 +++++++---------- 3 files changed, 29 insertions(+), 36 deletions(-) diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts index 44d181a175e..f2a9f6347f8 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts @@ -67,20 +67,18 @@ function createLibrary(options: GenerateLibSchema): Rule { return async (tree: Tree) => { const target = await resolveProject(tree, options.packageName, null); if (!target || options.override) { - if (options.isModuleTemplate) { - if (options.isStandaloneTemplate) { - return createLibFromModuleStandaloneTemplate(tree, options); - } else { - return createLibFromModuleTemplate(tree, options); - } - } if (options.isSecondaryEntrypoint) { - if (options.isStandaloneTemplate) { + if (options.templateType === 'standalone') { return createLibSecondaryEntryWithStandaloneTemplate(tree, options); } else { return createLibSecondaryEntry(tree, options); } } + if (options.templateType === 'module') { + return createLibFromModuleTemplate(tree, options); + } else { + return createLibFromModuleStandaloneTemplate(tree, options); + } } else { throw new SchematicsException( interpolate(Exception.LibraryAlreadyExists, options.packageName), @@ -230,7 +228,7 @@ export function importConfigModuleToDefaultProjectAppModule( console.log('isStandalone --->>>>', isSourceStandalone); const rules: Rule[] = []; - if (options.isStandaloneTemplate) { + if (options.templateType === 'standalone') { rules.push( addRootProvider(projectName, code => { const configFn = code.external( @@ -247,7 +245,7 @@ export function importConfigModuleToDefaultProjectAppModule( `${pascal(packageName)}ConfigModule`, `${kebab(packageName)}/config`, ); - return code.code`${configFn}()`; + return code.code`${configFn}.forRoot()`; }), ); } @@ -317,9 +315,10 @@ export function addRoutingToAppRoutingModule( if (isSourceStandalone) { return addRootProvider(projectName, code => { - const routeExpr = options.isStandaloneTemplate - ? `() => import('${routePath}/routes').then(m => m.${pascalName}Routes)` - : `() => import('${routePath}').then(m => m.${moduleName}.forLazy())`; + const routeExpr = + options.templateType === 'standalone' + ? `() => import('${routePath}').then(m => m.${pascalName}Routes)` + : `() => import('${routePath}').then(m => m.${moduleName}.forLazy())`; return code.code`provideRouter([ { path: '${routePath}', loadChildren: ${routeExpr} } @@ -344,9 +343,10 @@ export function addRoutingToAppRoutingModule( ts.ScriptTarget.Latest, true, ); - const importStatement = options.isStandaloneTemplate - ? `() => import('${routePath}').then(m => m.${pascalName}Routes)` - : `() => import('${routePath}').then(m => m.${moduleName}.forLazy())`; + const importStatement = + options.templateType === 'standalone' + ? `() => import('${routePath}').then(m => m.${pascalName}Routes)` + : `() => import('${routePath}').then(m => m.${moduleName}.forLazy())`; const routeDefinition = `{ path: '${routePath}', loadChildren: ${importStatement} }`; const change = addRouteDeclarationToModule(source, routePath, routeDefinition); diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/models/generate-lib-schema.ts b/npm/ng-packs/packages/schematics/src/commands/create-lib/models/generate-lib-schema.ts index a6d32e1da08..a3a9314abe8 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/models/generate-lib-schema.ts +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/models/generate-lib-schema.ts @@ -11,9 +11,7 @@ export interface GenerateLibSchema { /** * İs the package has standalone template */ - isStandaloneTemplate: boolean; - - isModuleTemplate: boolean; + templateType: 'standalone' | 'module'; override: boolean; diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/schema.json b/npm/ng-packs/packages/schematics/src/commands/create-lib/schema.json index 19de7420aee..df9f64a4701 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/schema.json +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/schema.json @@ -22,23 +22,18 @@ }, "x-prompt": "Is secondary entrypoint?" }, - "isModuleTemplate": { - "description": "Is module template", - "type": "boolean", - "$default": { - "$source": "argv", - "index": 2 - }, - "x-prompt": "Is module template?" - }, - "isStandaloneTemplate": { - "description": "Is standalone template", - "type": "boolean", - "$default": { - "$source": "argv", - "index": 3 - }, - "x-prompt": "Is standalone template?" + "templateType": { + "type": "string", + "description": "Type of the template", + "enum": ["module", "standalone"], + "x-prompt": { + "message": "Select the type of template to generate:", + "type": "list", + "items": [ + { "value": "module", "label": "Module Template" }, + { "value": "standalone", "label": "Standalone Template" } + ] + } }, "override": { "description": "Override existing files", From 56e7b83efa33c312af5b2362e8f473de0b1de0da Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Thu, 17 Apr 2025 16:26:49 +0300 Subject: [PATCH 14/27] refactoring --- .../src/commands/create-lib/index.ts | 206 ++++++++++-------- 1 file changed, 110 insertions(+), 96 deletions(-) diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts index f2a9f6347f8..b64b3b2dbb2 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts @@ -33,6 +33,7 @@ import { addLibToWorkspaceFile } from '../../utils/angular-schematic/generate-li import * as cases from '../../utils/text'; import { Exception } from '../../enums/exception'; import { GenerateLibSchema } from './models/generate-lib-schema'; +import { getMainFilePath } from '../../utils/angular/standalone/util'; export default function (schema: GenerateLibSchema) { return async (tree: Tree) => { @@ -154,7 +155,7 @@ export function addLibToWorkspaceIfNotExist(options: GenerateLibSchema, packages : noop(), addLibToWorkspaceFile(projectRoot, packageName), updateTsConfig(packageName, pathImportLib), - importConfigModuleToDefaultProjectAppModule(workspace, packageName, options), + importConfigModuleToDefaultProjectAppModule(packageName, options), addRoutingToAppRoutingModule(workspace, packageName, options), ]); }; @@ -214,18 +215,11 @@ export async function createLibSecondaryEntryWithStandaloneTemplate( } export function importConfigModuleToDefaultProjectAppModule( - workspace: WorkspaceDefinition, packageName: string, options: GenerateLibSchema, ) { return (tree: Tree) => { - // const projectName = readWorkspaceSchema(tree).defaultProject || getFirstApplication(tree).name!; const projectName = getFirstApplication(tree).name!; - const project = workspace.projects.get(projectName); - const sourceRoot = project?.sourceRoot || 'src'; - - const isSourceStandalone = isStandaloneApp(tree, `${sourceRoot}/main.ts`); - console.log('isStandalone --->>>>', isSourceStandalone); const rules: Rule[] = []; if (options.templateType === 'standalone') { @@ -249,50 +243,6 @@ export function importConfigModuleToDefaultProjectAppModule( }), ); } - - // const appModulePath = `${project?.sourceRoot}/app/app.module.ts`; - // const appModule = tree.read(appModulePath); - // if (!appModule) { - // return; - // } - // const appModuleContent = appModule.toString(); - /* if ( - appModuleContent.includes( - options.isStandaloneTemplate - ? `provide${pascal(packageName)}Config` - : `${camel(packageName)}ConfigModule`, - ) - ) { - return; - }*/ - - /* const rootConfigStatement = options.isStandaloneTemplate - ? `provide${pascal(packageName)}Config()` - : `${pascal(packageName)}ConfigModule.forRoot()`; - const text = tree.read(appModulePath); - if (!text) { - return; - } - const sourceText = text.toString(); - if (sourceText.includes(rootConfigStatement)) { - return; - } - const source = ts.createSourceFile(appModulePath, sourceText, ts.ScriptTarget.Latest, true); - - const changes = addImportToModule( - source, - appModulePath, - rootConfigStatement, - `${kebab(packageName)}/config`, - ); - const recorder = tree.beginUpdate(appModulePath); - for (const change of changes) { - if (change instanceof InsertChange) { - recorder.insertLeft(change.pos, change.toAdd); - } - } - tree.commitUpdate(recorder);*/ - return chain(rules); }; } @@ -302,60 +252,124 @@ export function addRoutingToAppRoutingModule( packageName: string, options: GenerateLibSchema, ): Rule { - return (tree: Tree) => { - // const projectName = readWorkspaceSchema(tree).defaultProject || getFirstApplication(tree).name!; + return async (tree: Tree) => { const projectName = getFirstApplication(tree).name!; const project = workspace.projects.get(projectName); - const sourceRoot = project?.sourceRoot || 'src'; + console.log('project --->>>', project); + const mainFilePath = await getMainFilePath(tree, projectName); + console.log('main file path ---->>>>', mainFilePath); + const isSourceStandalone = isStandaloneApp(tree, mainFilePath); + console.log('isSourceStandalone ---->>>>', isSourceStandalone); - const mainPath = `${sourceRoot}/main.ts`; - const isSourceStandalone = isStandaloneApp(tree, mainPath); const pascalName = pascal(packageName); const routePath = `${kebab(packageName)}`; + const moduleName = `${pascalName}Module`; if (isSourceStandalone) { - return addRootProvider(projectName, code => { - const routeExpr = - options.templateType === 'standalone' - ? `() => import('${routePath}').then(m => m.${pascalName}Routes)` - : `() => import('${routePath}').then(m => m.${moduleName}.forLazy())`; - - return code.code`provideRouter([ - { path: '${routePath}', loadChildren: ${routeExpr} } - ])`; - }); - } + const appRoutesPath = `${project?.sourceRoot}/app/app.routes.ts`; + const buffer = tree.read(appRoutesPath); + if (!buffer) { + throw new SchematicsException(`Cannot find routes file: ${appRoutesPath}`); + } - const appRoutingModulePath = `${project?.sourceRoot}/app/app-routing.module.ts`; - const appRoutingModule = tree.read(appRoutingModulePath); - if (!appRoutingModule) { - return; - } - const appRoutingModuleContent = appRoutingModule.toString(); - const moduleName = `${pascalName}Module`; - if (appRoutingModuleContent.includes(moduleName)) { - return; - } + const content = buffer.toString(); + const source = ts.createSourceFile(appRoutesPath, content, ts.ScriptTarget.Latest, true); + const routeExpr = + options.templateType === 'standalone' + ? `() => import('${routePath}').then(m => m.${pascalName}Routes)` + : `() => import('${routePath}').then(m => m.${moduleName}.forLazy())`; + const routeToAdd = `{ path: '${routePath}', loadChildren: ${routeExpr} }`; + const change = addRouteToRoutesArray(source, 'routes', routeToAdd); - const source = ts.createSourceFile( - appRoutingModulePath, - appRoutingModuleContent, - ts.ScriptTarget.Latest, - true, - ); - const importStatement = - options.templateType === 'standalone' - ? `() => import('${routePath}').then(m => m.${pascalName}Routes)` - : `() => import('${routePath}').then(m => m.${moduleName}.forLazy())`; - const routeDefinition = `{ path: '${routePath}', loadChildren: ${importStatement} }`; - const change = addRouteDeclarationToModule(source, routePath, routeDefinition); - - const recorder = tree.beginUpdate(appRoutingModulePath); - if (change instanceof InsertChange) { - recorder.insertLeft(change.pos, change.toAdd); - } - tree.commitUpdate(recorder); + if (change instanceof InsertChange) { + const recorder = tree.beginUpdate(appRoutesPath); + recorder.insertLeft(change.pos, change.toAdd); + tree.commitUpdate(recorder); + } + } else { + const appRoutingModulePath = `${project?.sourceRoot}/app/app-routing.module.ts`; + const appRoutingModule = tree.read(appRoutingModulePath); + if (!appRoutingModule) { + return; + } + const appRoutingModuleContent = appRoutingModule.toString(); + if (appRoutingModuleContent.includes(moduleName)) { + return; + } + const source = ts.createSourceFile( + appRoutingModulePath, + appRoutingModuleContent, + ts.ScriptTarget.Latest, + true, + ); + const importStatement = + options.templateType === 'standalone' + ? `() => import('${routePath}').then(m => m.${pascalName}Routes)` + : `() => import('${routePath}').then(m => m.${moduleName}.forLazy())`; + const routeDefinition = `{ path: '${routePath}', loadChildren: ${importStatement} }`; + const change = addRouteDeclarationToModule(source, routePath, routeDefinition); + + const recorder = tree.beginUpdate(appRoutingModulePath); + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } + tree.commitUpdate(recorder); + } return; }; } + +export function addRouteToRoutesArray( + source: ts.SourceFile, + arrayName: string, + routeToAdd: string, +): InsertChange | null { + const routesVar = source.statements.find( + stmt => + ts.isVariableStatement(stmt) && + stmt.declarationList.declarations.some( + decl => + ts.isVariableDeclaration(decl) && + decl.name.getText() === arrayName && + decl.initializer !== undefined && + ts.isArrayLiteralExpression(decl.initializer), + ), + ); + + if (!routesVar || !ts.isVariableStatement(routesVar)) { + throw new Error(`Could not find routes array named "${arrayName}".`); + } + + const declaration = routesVar.declarationList.declarations.find( + decl => decl.name.getText() === arrayName, + ) as ts.VariableDeclaration; + + const arrayLiteral = declaration.initializer as ts.ArrayLiteralExpression; + + const getPathValue = (routeText: string): string | null => { + const match = routeText.match(/path:\s*['"`](.+?)['"`]/); + return match?.[1] ?? null; + }; + + const newPath = getPathValue(routeToAdd); + + const alreadyExists = arrayLiteral.elements.some(el => { + const existingPath = getPathValue(el.getText()); + return existingPath === newPath; + }); + + if (alreadyExists) { + return null; + } + + const insertPos = + arrayLiteral.elements.hasTrailingComma || arrayLiteral.elements.length === 0 + ? arrayLiteral.getEnd() - 1 + : arrayLiteral.elements[arrayLiteral.elements.length - 1].getEnd(); + + const prefix = arrayLiteral.elements.length > 0 ? ',\n ' : ' '; + const toAdd = `${prefix}${routeToAdd}`; + + return new InsertChange(source.fileName, insertPos, toAdd); +} From 80e7f7317d0250c398a248ab0a99b67231d1abf3 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Thu, 17 Apr 2025 18:23:23 +0300 Subject: [PATCH 15/27] custom standalone util functions added --- .../packages/schematics/src/utils/index.ts | 1 + .../schematics/src/utils/standalone.ts | 59 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 npm/ng-packs/packages/schematics/src/utils/standalone.ts diff --git a/npm/ng-packs/packages/schematics/src/utils/index.ts b/npm/ng-packs/packages/schematics/src/utils/index.ts index eeb12e6d9eb..733f656ecb6 100644 --- a/npm/ng-packs/packages/schematics/src/utils/index.ts +++ b/npm/ng-packs/packages/schematics/src/utils/index.ts @@ -18,3 +18,4 @@ export * from './text'; export * from './tree'; export * from './type'; export * from './workspace'; +export * from './standalone'; diff --git a/npm/ng-packs/packages/schematics/src/utils/standalone.ts b/npm/ng-packs/packages/schematics/src/utils/standalone.ts new file mode 100644 index 00000000000..2444db31854 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/utils/standalone.ts @@ -0,0 +1,59 @@ +import { Tree } from '@angular-devkit/schematics'; +import { findBootstrapApplicationCall } from './angular/standalone/util'; +import { findAppConfig } from './angular/standalone/app_config'; +import * as ts from 'typescript'; +import { normalize, Path } from '@angular-devkit/core'; +import * as path from 'path'; + +export const getAppConfigPath = (host: Tree, mainFilePath: string): string => { + const bootstrapCall = findBootstrapApplicationCall(host, mainFilePath); + const appConfig = findAppConfig(bootstrapCall, host, mainFilePath); + return appConfig?.filePath || ''; +}; + +export function findAppRoutesPath(tree: Tree, mainFilePath: string): Path | null { + const appConfigPath = getAppConfigPath(tree, mainFilePath); + if (!appConfigPath || !tree.exists(appConfigPath)) return null; + + const buffer = tree.read(appConfigPath); + if (!buffer) return null; + + const source = ts.createSourceFile( + appConfigPath, + buffer.toString('utf-8'), + ts.ScriptTarget.Latest, + true, + ); + + for (const stmt of source.statements) { + if (!ts.isImportDeclaration(stmt)) continue; + + const importClause = stmt.importClause; + if (!importClause?.namedBindings || !ts.isNamedImports(importClause.namedBindings)) continue; + + const isRoutesImport = importClause.namedBindings.elements.some( + el => el.name.getText() === 'routes', + ); + if (!isRoutesImport || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; + + let importPath = stmt.moduleSpecifier.text; + + if (!importPath.endsWith('.ts')) { + importPath += '.ts'; + } + + const configDir = path.dirname(appConfigPath); + const resolvedFsPath = path.resolve(configDir, importPath); + const workspaceRelativePath = path.relative(process.cwd(), resolvedFsPath).replace(/\\/g, '/'); + + const normalizedPath = normalize(workspaceRelativePath); + + if (!tree.exists(normalizedPath)) { + throw new Error(`Cannot find routes file: ${normalizedPath}`); + } + + return normalizedPath; + } + + return null; +} From b8df00add6fd40c24f1430bd3880626891d4d22f Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Thu, 17 Apr 2025 19:18:40 +0300 Subject: [PATCH 16/27] refactoring --- .../schematics/src/commands/change-theme/index.ts | 10 ++-------- .../schematics/src/commands/create-lib/index.ts | 7 +++---- 2 files changed, 5 insertions(+), 12 deletions(-) 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 9ef5dd1690c..937af3f1c23 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 @@ -12,11 +12,11 @@ import { isStandaloneApp, updateWorkspace, WorkspaceDefinition, + getAppConfigPath, } from '../../utils'; import { ThemeOptionsEnum } from './theme-options.enum'; import { findNodes, getDecoratorMetadata, getMetadataField } from '../../utils/angular/ast-utils'; -import { findBootstrapApplicationCall, getMainFilePath } from '../../utils/angular/standalone/util'; -import { findAppConfig } from '../../utils/angular/standalone/app_config'; +import { getMainFilePath } from '../../utils/angular/standalone/util'; export default function (_options: ChangeThemeOptions): Rule { return async () => { @@ -348,12 +348,6 @@ export const styleCompareFn = (item1: string | object, item2: string | object) = return o1.bundleName && o2.bundleName && o1.bundleName == o2.bundleName; }; -export const getAppConfigPath = (host: Tree, mainFilePath: string): string => { - const bootstrapCall = findBootstrapApplicationCall(host, mainFilePath); - const appConfig = findAppConfig(bootstrapCall, host, mainFilePath); - return appConfig?.filePath || ''; -}; - export const formatFile = (filePath: string): Rule => { return (tree: Tree) => { const buffer = tree.read(filePath); diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts index b64b3b2dbb2..0111d004d06 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts @@ -16,6 +16,7 @@ import { addRootProvider, addRouteDeclarationToModule, applyWithOverwrite, + findAppRoutesPath, getFirstApplication, getWorkspace, InsertChange, @@ -255,18 +256,16 @@ export function addRoutingToAppRoutingModule( return async (tree: Tree) => { const projectName = getFirstApplication(tree).name!; const project = workspace.projects.get(projectName); - console.log('project --->>>', project); const mainFilePath = await getMainFilePath(tree, projectName); - console.log('main file path ---->>>>', mainFilePath); const isSourceStandalone = isStandaloneApp(tree, mainFilePath); - console.log('isSourceStandalone ---->>>>', isSourceStandalone); const pascalName = pascal(packageName); const routePath = `${kebab(packageName)}`; const moduleName = `${pascalName}Module`; if (isSourceStandalone) { - const appRoutesPath = `${project?.sourceRoot}/app/app.routes.ts`; + const appRoutesPath = + findAppRoutesPath(tree, mainFilePath) || `${project?.sourceRoot}/app/app.routes.ts`; const buffer = tree.read(appRoutesPath); if (!buffer) { throw new SchematicsException(`Cannot find routes file: ${appRoutesPath}`); From c9089899965db0e27e19f0a1ef99b2159059cf33 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Fri, 18 Apr 2025 13:58:29 +0300 Subject: [PATCH 17/27] refactoring --- .../src/commands/create-lib/index.ts | 11 +++++- .../schematics/src/utils/standalone.ts | 39 ++++++++++++++++++- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts index 0111d004d06..0370640f052 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts @@ -19,6 +19,7 @@ import { findAppRoutesPath, getFirstApplication, getWorkspace, + hasProviderInStandaloneAppConfig, InsertChange, interpolate, isLibrary, @@ -219,11 +220,19 @@ export function importConfigModuleToDefaultProjectAppModule( packageName: string, options: GenerateLibSchema, ) { - return (tree: Tree) => { + return async (tree: Tree) => { const projectName = getFirstApplication(tree).name!; const rules: Rule[] = []; if (options.templateType === 'standalone') { + const providerAlreadyExists = await hasProviderInStandaloneAppConfig( + tree, + projectName, + `provide${pascal(packageName)}Config`, + ); + if (providerAlreadyExists) { + return; + } rules.push( addRootProvider(projectName, code => { const configFn = code.external( diff --git a/npm/ng-packs/packages/schematics/src/utils/standalone.ts b/npm/ng-packs/packages/schematics/src/utils/standalone.ts index 2444db31854..ad9197c2aac 100644 --- a/npm/ng-packs/packages/schematics/src/utils/standalone.ts +++ b/npm/ng-packs/packages/schematics/src/utils/standalone.ts @@ -1,5 +1,5 @@ -import { Tree } from '@angular-devkit/schematics'; -import { findBootstrapApplicationCall } from './angular/standalone/util'; +import { SchematicsException, Tree } from '@angular-devkit/schematics'; +import { findBootstrapApplicationCall, getMainFilePath } from './angular/standalone/util'; import { findAppConfig } from './angular/standalone/app_config'; import * as ts from 'typescript'; import { normalize, Path } from '@angular-devkit/core'; @@ -57,3 +57,38 @@ export function findAppRoutesPath(tree: Tree, mainFilePath: string): Path | null return null; } + +export const hasProviderInStandaloneAppConfig = async ( + host: Tree, + projectName: string, + providerName: string, +): Promise => { + const mainFilePath = await getMainFilePath(host, projectName); + const appConfigPath = getAppConfigPath(host, mainFilePath); + const buffer = host.read(appConfigPath); + + if (!buffer) { + throw new SchematicsException(`Could not read file: ${appConfigPath}`); + } + + const source = ts.createSourceFile( + appConfigPath, + buffer.toString('utf-8'), + ts.ScriptTarget.Latest, + true, + ); + const callExpressions = source.statements + .flatMap(stmt => (ts.isVariableStatement(stmt) ? stmt.declarationList.declarations : [])) + .flatMap(decl => + decl.initializer && ts.isObjectLiteralExpression(decl.initializer) + ? decl.initializer.properties + : [], + ) + .filter(ts.isPropertyAssignment) + .filter(prop => prop.name.getText() === 'providers'); + + if (callExpressions.length === 0) return false; + + const providersArray = callExpressions[0].initializer as ts.ArrayLiteralExpression; + return providersArray.elements.some(el => el.getText().includes(providerName)); +}; From 4f0d46063f5b6514c57d7e3bcff19c5423743aa0 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Fri, 18 Apr 2025 18:46:46 +0300 Subject: [PATCH 18/27] refactoring --- .../packages/schematics/src/utils/index.ts | 1 + .../schematics/src/utils/ng-module.ts | 48 +++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 npm/ng-packs/packages/schematics/src/utils/ng-module.ts diff --git a/npm/ng-packs/packages/schematics/src/utils/index.ts b/npm/ng-packs/packages/schematics/src/utils/index.ts index 733f656ecb6..06b96892fcc 100644 --- a/npm/ng-packs/packages/schematics/src/utils/index.ts +++ b/npm/ng-packs/packages/schematics/src/utils/index.ts @@ -19,3 +19,4 @@ export * from './tree'; export * from './type'; export * from './workspace'; export * from './standalone'; +export * from './ng-module'; diff --git a/npm/ng-packs/packages/schematics/src/utils/ng-module.ts b/npm/ng-packs/packages/schematics/src/utils/ng-module.ts new file mode 100644 index 00000000000..ac960cd8d0a --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/utils/ng-module.ts @@ -0,0 +1,48 @@ +import { SchematicsException, Tree } from '@angular-devkit/schematics'; +import { getMainFilePath } from './angular/standalone/util'; +import * as ts from 'typescript'; +import { getAppModulePath, getDecoratorMetadata, getMetadataField } from './angular'; +import { createSourceFile } from '../commands/change-theme/index'; + +export const hasImportInNgModule = async ( + host: Tree, + projectName: string, + metadataFn: string, + metadataName = 'imports', +): Promise => { + const mainFilePath = await getMainFilePath(host, projectName); + const appModulePath = getAppModulePath(host, mainFilePath); + const buffer = host.read(appModulePath); + + if (!buffer) { + throw new SchematicsException(`Could not read file: ${appModulePath}`); + } + + const source = createSourceFile(host, appModulePath); + + console.log('AppModule content:\n', buffer.toString('utf-8')); + + // Get the NgModule decorator metadata + const ngModuleDecorator = getDecoratorMetadata(source, 'NgModule', '@angular/core')[0]; + console.log( + 'Found NgModule decorators:', + getDecoratorMetadata(source, 'NgModule', '@angular/core'), + ); + if (!ngModuleDecorator) { + throw new SchematicsException('The app module does not found'); + } + + const matchingProperties = getMetadataField( + ngModuleDecorator as ts.ObjectLiteralExpression, + metadataName, + ); + const assignment = matchingProperties[0] as ts.PropertyAssignment; + const assignmentInit = assignment.initializer as ts.ArrayLiteralExpression; + + const elements = assignmentInit.elements; + if (!elements || elements.length < 1) { + throw new SchematicsException(`Elements could not found: ${elements}`); + } + + return elements.some(f => f.getText().match(metadataFn)); +}; From 9d332e4f6715b9d33c4f2d48d52508a93552930f Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Mon, 21 Apr 2025 11:16:10 +0300 Subject: [PATCH 19/27] refactoring --- .../src/providers/route.provider.ts.template | 19 ++++--- ... __libraryName@kebab__.routes.ts.template} | 2 +- .../src/lib/index.ts.template | 2 +- .../src/commands/create-lib/index.ts | 45 +++++++++++----- .../schematics/src/utils/ng-module.ts | 52 +++++++++++++++++++ 5 files changed, 99 insertions(+), 21 deletions(-) rename npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/{__libraryName@kebab__-routing.ts.template => __libraryName@kebab__.routes.ts.template} (77%) diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/providers/route.provider.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/providers/route.provider.ts.template index 91b2d7545cc..c890db73056 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/providers/route.provider.ts.template +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/providers/route.provider.ts.template @@ -1,7 +1,12 @@ -import { APP_INITIALIZER, Provider } from '@angular/core'; import { eLayoutType, RoutesService } from '@abp/ng.core'; import { e<%= pascal(libraryName) %>RouteNames } from '../enums/route-names'; -import { makeEnvironmentProviders, provideAppInitializer, inject } from '@angular/core'; +import { makeEnvironmentProviders, provideAppInitializer, inject, EnvironmentProviders } from '@angular/core'; + +export const <%= macro(libraryName) %>_ROUTE_PROVIDERS = [ + provideAppInitializer(() => { + configureRoutes(); + }), +]; export function configureRoutes() { const routes = inject(RoutesService); @@ -16,10 +21,10 @@ export function configureRoutes() { ]); } +export const <%= macro(libraryName) %>_PROVIDERS: EnvironmentProviders[] = [ + ...<%= macro(libraryName) %>_ROUTE_PROVIDERS, +]; + export function provide<%= pascal(libraryName) %>Config() { - return makeEnvironmentProviders([ - provideAppInitializer(() => { - configureRoutes(); - }), - ]); + return makeEnvironmentProviders(<%= macro(libraryName) %>_PROVIDERS); } diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__-routing.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__.routes.ts.template similarity index 77% rename from npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__-routing.ts.template rename to npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__.routes.ts.template index fba42c78824..4fb52f82597 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__-routing.ts.template +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__.routes.ts.template @@ -1,6 +1,6 @@ import { Routes } from '@angular/router'; -export const <%= pascal(libraryName) %>Routes: Routes = [ +export const <%= macro(libraryName) %>_ROUTES: Routes = [ { path: '', loadComponent: () => diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/index.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/index.ts.template index aee9705b6fa..b954dd6be02 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/index.ts.template +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/index.ts.template @@ -1 +1 @@ -export * from './<%= kebab(libraryName) %>-routing'; +export * from './<%= kebab(libraryName) %>.routes'; diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts index 0370640f052..4d3927f015e 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts @@ -16,9 +16,11 @@ import { addRootProvider, addRouteDeclarationToModule, applyWithOverwrite, + findAppRoutesModulePath, findAppRoutesPath, getFirstApplication, getWorkspace, + hasImportInNgModule, hasProviderInStandaloneAppConfig, InsertChange, interpolate, @@ -26,6 +28,7 @@ import { isStandaloneApp, JSONFile, kebab, + macro, pascal, resolveProject, updateWorkspace, @@ -222,17 +225,29 @@ export function importConfigModuleToDefaultProjectAppModule( ) { return async (tree: Tree) => { const projectName = getFirstApplication(tree).name!; + const mainFilePath = await getMainFilePath(tree, projectName); + const isSourceStandalone = isStandaloneApp(tree, mainFilePath); const rules: Rule[] = []; + const providerAlreadyExists = isSourceStandalone + ? await hasProviderInStandaloneAppConfig( + tree, + projectName, + `provide${pascal(packageName)}Config`, + ) + : await hasImportInNgModule( + tree, + projectName, + options.templateType === 'standalone' + ? `provide${pascal(packageName)}Config` + : `${pascal(packageName)}ConfigModule`, + options.templateType === 'standalone' ? 'providers' : 'imports', + ); + if (providerAlreadyExists) { + return; + } + if (options.templateType === 'standalone') { - const providerAlreadyExists = await hasProviderInStandaloneAppConfig( - tree, - projectName, - `provide${pascal(packageName)}Config`, - ); - if (providerAlreadyExists) { - return; - } rules.push( addRootProvider(projectName, code => { const configFn = code.external( @@ -269,6 +284,7 @@ export function addRoutingToAppRoutingModule( const isSourceStandalone = isStandaloneApp(tree, mainFilePath); const pascalName = pascal(packageName); + const macroName = macro(packageName); const routePath = `${kebab(packageName)}`; const moduleName = `${pascalName}Module`; @@ -284,7 +300,7 @@ export function addRoutingToAppRoutingModule( const source = ts.createSourceFile(appRoutesPath, content, ts.ScriptTarget.Latest, true); const routeExpr = options.templateType === 'standalone' - ? `() => import('${routePath}').then(m => m.${pascalName}Routes)` + ? `() => import('${routePath}').then(m => m.${macroName}_ROUTES)` : `() => import('${routePath}').then(m => m.${moduleName}.forLazy())`; const routeToAdd = `{ path: '${routePath}', loadChildren: ${routeExpr} }`; const change = addRouteToRoutesArray(source, 'routes', routeToAdd); @@ -295,7 +311,12 @@ export function addRoutingToAppRoutingModule( tree.commitUpdate(recorder); } } else { - const appRoutingModulePath = `${project?.sourceRoot}/app/app-routing.module.ts`; + const appRoutingModulePath = await findAppRoutesModulePath(tree, mainFilePath); + + if (!appRoutingModulePath) { + throw new SchematicsException(`Cannot find routing module: ${appRoutingModulePath}`); + } + const appRoutingModule = tree.read(appRoutingModulePath); if (!appRoutingModule) { return; @@ -313,7 +334,7 @@ export function addRoutingToAppRoutingModule( ); const importStatement = options.templateType === 'standalone' - ? `() => import('${routePath}').then(m => m.${pascalName}Routes)` + ? `() => import('${routePath}').then(m => m.${macroName}_ROUTES)` : `() => import('${routePath}').then(m => m.${moduleName}.forLazy())`; const routeDefinition = `{ path: '${routePath}', loadChildren: ${importStatement} }`; const change = addRouteDeclarationToModule(source, routePath, routeDefinition); @@ -339,7 +360,7 @@ export function addRouteToRoutesArray( stmt.declarationList.declarations.some( decl => ts.isVariableDeclaration(decl) && - decl.name.getText() === arrayName && + (decl.name.getText() === arrayName || decl.name.getText() === arrayName.toUpperCase()) && decl.initializer !== undefined && ts.isArrayLiteralExpression(decl.initializer), ), diff --git a/npm/ng-packs/packages/schematics/src/utils/ng-module.ts b/npm/ng-packs/packages/schematics/src/utils/ng-module.ts index ac960cd8d0a..30dafd427b7 100644 --- a/npm/ng-packs/packages/schematics/src/utils/ng-module.ts +++ b/npm/ng-packs/packages/schematics/src/utils/ng-module.ts @@ -3,6 +3,8 @@ import { getMainFilePath } from './angular/standalone/util'; import * as ts from 'typescript'; import { getAppModulePath, getDecoratorMetadata, getMetadataField } from './angular'; import { createSourceFile } from '../commands/change-theme/index'; +import { normalize, Path } from '@angular-devkit/core'; +import * as path from 'path'; export const hasImportInNgModule = async ( host: Tree, @@ -46,3 +48,53 @@ export const hasImportInNgModule = async ( return elements.some(f => f.getText().match(metadataFn)); }; + +export async function findAppRoutesModulePath( + tree: Tree, + mainFilePath: string, +): Promise { + const appModulePath = getAppModulePath(tree, mainFilePath); + if (!appModulePath || !tree.exists(appModulePath)) return null; + + const buffer = tree.read(appModulePath); + if (!buffer) return null; + + const source = ts.createSourceFile( + appModulePath, + buffer.toString('utf-8'), + ts.ScriptTarget.Latest, + true, + ); + + for (const stmt of source.statements) { + if (!ts.isImportDeclaration(stmt)) continue; + + const importClause = stmt.importClause; + if (!importClause?.namedBindings || !ts.isNamedImports(importClause.namedBindings)) continue; + + const isRoutesImport = importClause.namedBindings.elements.some( + el => el.name.getText() === 'AppRoutingModule', + ); + if (!isRoutesImport || !ts.isStringLiteral(stmt.moduleSpecifier)) continue; + + let importPath = stmt.moduleSpecifier.text; + + if (!importPath.endsWith('.ts')) { + importPath += '.ts'; + } + + const configDir = path.dirname(appModulePath); + const resolvedFsPath = path.resolve(configDir, importPath); + const workspaceRelativePath = path.relative(process.cwd(), resolvedFsPath).replace(/\\/g, '/'); + + const normalizedPath = normalize(workspaceRelativePath); + + if (!tree.exists(normalizedPath)) { + throw new Error(`Cannot find routes file: ${normalizedPath}`); + } + + return normalizedPath; + } + + return null; +} From 433167503c9dd99927441cd706af96aca832955f Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Mon, 21 Apr 2025 11:28:56 +0300 Subject: [PATCH 20/27] refactoring --- .../packages/schematics/src/commands/create-lib/index.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts index 4d3927f015e..01eb241dbee 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts @@ -322,7 +322,8 @@ export function addRoutingToAppRoutingModule( return; } const appRoutingModuleContent = appRoutingModule.toString(); - if (appRoutingModuleContent.includes(moduleName)) { + const routeExpr = options.templateType === 'standalone' ? `${macroName}_ROUTES` : moduleName; + if (appRoutingModuleContent.includes(routeExpr)) { return; } From 8612fb44505e8d2ca89e7fa47e4a37e7622545e4 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Mon, 21 Apr 2025 13:17:46 +0300 Subject: [PATCH 21/27] clean commas rule added --- .../src/commands/change-theme/index.ts | 71 +++++++++++++++++++ .../packages/schematics/src/utils/ast.ts | 7 ++ 2 files changed, 78 insertions(+) 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 937af3f1c23..70a8c212446 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 @@ -13,6 +13,7 @@ import { updateWorkspace, WorkspaceDefinition, getAppConfigPath, + removeEmptyElementsFromArrayLiteral, } from '../../utils'; import { ThemeOptionsEnum } from './theme-options.enum'; import { findNodes, getDecoratorMetadata, getMetadataField } from '../../utils/angular/ast-utils'; @@ -80,6 +81,7 @@ function updateAppModule(selectedProject: string, targetThemeName: ThemeOptionsE insertImports(selectedProject, targetThemeName), insertProviders(selectedProject, targetThemeName), formatFile(appModulePath), + cleanNgModuleCommasRule(appModulePath, isStandalone), ]); }; } @@ -361,3 +363,72 @@ export const formatFile = (filePath: string): Rule => { return tree; }; }; + +export function cleanNgModuleCommasRule(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); + const printer = ts.createPrinter(); + + if (isStandalone) { + const varStatements = findNodes(source, ts.isVariableStatement); + + for (const stmt of varStatements) { + const declList = stmt.declarationList; + for (const decl of declList.declarations) { + if (!decl.initializer || !ts.isObjectLiteralExpression(decl.initializer)) continue; + + const obj = decl.initializer; + + const providersProp = obj.properties.find( + prop => + ts.isPropertyAssignment(prop) && + ts.isIdentifier(prop.name) && + prop.name.text === 'providers', + ) as ts.PropertyAssignment; + + if (!providersProp || !ts.isArrayLiteralExpression(providersProp.initializer)) continue; + + const arrayLiteral = providersProp.initializer; + const cleanedArray = removeEmptyElementsFromArrayLiteral(arrayLiteral); + + recorder.remove(arrayLiteral.getStart(), arrayLiteral.getWidth()); + recorder.insertLeft( + arrayLiteral.getStart(), + printer.printNode(ts.EmitHint.Expression, cleanedArray, source), + ); + } + } + } else { + const ngModuleNode = getDecoratorMetadata(source, 'NgModule', '@angular/core')[0]; + if (!ngModuleNode) return host; + + const metadataKeys = ['imports', 'providers']; + for (const key of metadataKeys) { + const metadataField = getMetadataField(ngModuleNode as ts.ObjectLiteralExpression, key); + if (!metadataField.length) continue; + + const assignment = metadataField[0] as ts.PropertyAssignment; + const arrayLiteral = assignment.initializer as ts.ArrayLiteralExpression; + + const cleanedArray = removeEmptyElementsFromArrayLiteral(arrayLiteral); + + recorder.remove(arrayLiteral.getStart(), arrayLiteral.getWidth()); + recorder.insertLeft( + arrayLiteral.getStart(), + printer.printNode(ts.EmitHint.Expression, cleanedArray, source), + ); + } + } + host.commitUpdate(recorder); + return host; + }; +} diff --git a/npm/ng-packs/packages/schematics/src/utils/ast.ts b/npm/ng-packs/packages/schematics/src/utils/ast.ts index a1ffe5a3197..0b720e63cc1 100644 --- a/npm/ng-packs/packages/schematics/src/utils/ast.ts +++ b/npm/ng-packs/packages/schematics/src/utils/ast.ts @@ -35,3 +35,10 @@ export function isBooleanStringOrNumberLiteral( node.kind === ts.SyntaxKind.FalseKeyword ); } + +export function removeEmptyElementsFromArrayLiteral( + array: ts.ArrayLiteralExpression, +): ts.ArrayLiteralExpression { + const cleaned = array.elements.filter(el => el.kind !== ts.SyntaxKind.OmittedExpression); + return ts.factory.updateArrayLiteralExpression(array, ts.factory.createNodeArray(cleaned)); +} From f282fe373e8adb682f50155444c8c4ac2ad5a9e8 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Mon, 21 Apr 2025 15:00:11 +0300 Subject: [PATCH 22/27] refactoring --- .../src/commands/change-theme/index.ts | 57 ++----------------- .../schematics/src/utils/ng-module.ts | 26 ++++++++- .../schematics/src/utils/standalone.ts | 36 +++++++++++- 3 files changed, 66 insertions(+), 53 deletions(-) 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 70a8c212446..af516d22865 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 @@ -13,7 +13,8 @@ import { updateWorkspace, WorkspaceDefinition, getAppConfigPath, - removeEmptyElementsFromArrayLiteral, + cleanEmptyExprFromModule, + cleanEmptyExprFromProviders, } from '../../utils'; import { ThemeOptionsEnum } from './theme-options.enum'; import { findNodes, getDecoratorMetadata, getMetadataField } from '../../utils/angular/ast-utils'; @@ -81,7 +82,7 @@ function updateAppModule(selectedProject: string, targetThemeName: ThemeOptionsE insertImports(selectedProject, targetThemeName), insertProviders(selectedProject, targetThemeName), formatFile(appModulePath), - cleanNgModuleCommasRule(appModulePath, isStandalone), + cleanEmptyExpressions(appModulePath, isStandalone), ]); }; } @@ -364,7 +365,7 @@ export const formatFile = (filePath: string): Rule => { }; }; -export function cleanNgModuleCommasRule(modulePath: string, isStandalone: boolean): Rule { +export function cleanEmptyExpressions(modulePath: string, isStandalone: boolean): Rule { return (host: Tree) => { const buffer = host.read(modulePath); if (!buffer) throw new SchematicsException(`Cannot read ${modulePath}`); @@ -376,57 +377,11 @@ export function cleanNgModuleCommasRule(modulePath: string, isStandalone: boolea true, ); const recorder = host.beginUpdate(modulePath); - const printer = ts.createPrinter(); if (isStandalone) { - const varStatements = findNodes(source, ts.isVariableStatement); - - for (const stmt of varStatements) { - const declList = stmt.declarationList; - for (const decl of declList.declarations) { - if (!decl.initializer || !ts.isObjectLiteralExpression(decl.initializer)) continue; - - const obj = decl.initializer; - - const providersProp = obj.properties.find( - prop => - ts.isPropertyAssignment(prop) && - ts.isIdentifier(prop.name) && - prop.name.text === 'providers', - ) as ts.PropertyAssignment; - - if (!providersProp || !ts.isArrayLiteralExpression(providersProp.initializer)) continue; - - const arrayLiteral = providersProp.initializer; - const cleanedArray = removeEmptyElementsFromArrayLiteral(arrayLiteral); - - recorder.remove(arrayLiteral.getStart(), arrayLiteral.getWidth()); - recorder.insertLeft( - arrayLiteral.getStart(), - printer.printNode(ts.EmitHint.Expression, cleanedArray, source), - ); - } - } + cleanEmptyExprFromProviders(source, recorder); } else { - const ngModuleNode = getDecoratorMetadata(source, 'NgModule', '@angular/core')[0]; - if (!ngModuleNode) return host; - - const metadataKeys = ['imports', 'providers']; - for (const key of metadataKeys) { - const metadataField = getMetadataField(ngModuleNode as ts.ObjectLiteralExpression, key); - if (!metadataField.length) continue; - - const assignment = metadataField[0] as ts.PropertyAssignment; - const arrayLiteral = assignment.initializer as ts.ArrayLiteralExpression; - - const cleanedArray = removeEmptyElementsFromArrayLiteral(arrayLiteral); - - recorder.remove(arrayLiteral.getStart(), arrayLiteral.getWidth()); - recorder.insertLeft( - arrayLiteral.getStart(), - printer.printNode(ts.EmitHint.Expression, cleanedArray, source), - ); - } + cleanEmptyExprFromModule(source, recorder); } host.commitUpdate(recorder); return host; diff --git a/npm/ng-packs/packages/schematics/src/utils/ng-module.ts b/npm/ng-packs/packages/schematics/src/utils/ng-module.ts index 30dafd427b7..f048588d5f9 100644 --- a/npm/ng-packs/packages/schematics/src/utils/ng-module.ts +++ b/npm/ng-packs/packages/schematics/src/utils/ng-module.ts @@ -1,10 +1,11 @@ -import { SchematicsException, Tree } from '@angular-devkit/schematics'; +import { SchematicsException, Tree, UpdateRecorder } from '@angular-devkit/schematics'; import { getMainFilePath } from './angular/standalone/util'; import * as ts from 'typescript'; import { getAppModulePath, getDecoratorMetadata, getMetadataField } from './angular'; import { createSourceFile } from '../commands/change-theme/index'; import { normalize, Path } from '@angular-devkit/core'; import * as path from 'path'; +import { removeEmptyElementsFromArrayLiteral } from './ast'; export const hasImportInNgModule = async ( host: Tree, @@ -98,3 +99,26 @@ export async function findAppRoutesModulePath( return null; } + +export function cleanEmptyExprFromModule(source: ts.SourceFile, recorder: UpdateRecorder): void { + const ngModuleNode = getDecoratorMetadata(source, 'NgModule', '@angular/core')[0]; + if (!ngModuleNode) return; + + const printer = ts.createPrinter(); + const metadataKeys = ['imports', 'providers']; + for (const key of metadataKeys) { + const metadataField = getMetadataField(ngModuleNode as ts.ObjectLiteralExpression, key); + if (!metadataField.length) continue; + + const assignment = metadataField[0] as ts.PropertyAssignment; + const arrayLiteral = assignment.initializer as ts.ArrayLiteralExpression; + + const cleanedArray = removeEmptyElementsFromArrayLiteral(arrayLiteral); + + recorder.remove(arrayLiteral.getStart(), arrayLiteral.getWidth()); + recorder.insertLeft( + arrayLiteral.getStart(), + printer.printNode(ts.EmitHint.Expression, cleanedArray, source), + ); + } +} diff --git a/npm/ng-packs/packages/schematics/src/utils/standalone.ts b/npm/ng-packs/packages/schematics/src/utils/standalone.ts index ad9197c2aac..982c56875e4 100644 --- a/npm/ng-packs/packages/schematics/src/utils/standalone.ts +++ b/npm/ng-packs/packages/schematics/src/utils/standalone.ts @@ -1,9 +1,11 @@ -import { SchematicsException, Tree } from '@angular-devkit/schematics'; +import { SchematicsException, Tree, UpdateRecorder } from '@angular-devkit/schematics'; import { findBootstrapApplicationCall, getMainFilePath } from './angular/standalone/util'; import { findAppConfig } from './angular/standalone/app_config'; import * as ts from 'typescript'; import { normalize, Path } from '@angular-devkit/core'; import * as path from 'path'; +import { findNodes } from './angular'; +import { removeEmptyElementsFromArrayLiteral } from './ast'; export const getAppConfigPath = (host: Tree, mainFilePath: string): string => { const bootstrapCall = findBootstrapApplicationCall(host, mainFilePath); @@ -92,3 +94,35 @@ export const hasProviderInStandaloneAppConfig = async ( const providersArray = callExpressions[0].initializer as ts.ArrayLiteralExpression; return providersArray.elements.some(el => el.getText().includes(providerName)); }; + +export function cleanEmptyExprFromProviders(source: ts.SourceFile, recorder: UpdateRecorder): void { + const varStatements = findNodes(source, ts.isVariableStatement); + const printer = ts.createPrinter(); + + for (const stmt of varStatements) { + const declList = stmt.declarationList; + for (const decl of declList.declarations) { + if (!decl.initializer || !ts.isObjectLiteralExpression(decl.initializer)) continue; + + const obj = decl.initializer; + + const providersProp = obj.properties.find( + prop => + ts.isPropertyAssignment(prop) && + ts.isIdentifier(prop.name) && + prop.name.text === 'providers', + ) as ts.PropertyAssignment; + + if (!providersProp || !ts.isArrayLiteralExpression(providersProp.initializer)) continue; + + const arrayLiteral = providersProp.initializer; + const cleanedArray = removeEmptyElementsFromArrayLiteral(arrayLiteral); + + recorder.remove(arrayLiteral.getStart(), arrayLiteral.getWidth()); + recorder.insertLeft( + arrayLiteral.getStart(), + printer.printNode(ts.EmitHint.Expression, cleanedArray, source), + ); + } + } +} From d4cc6dc9f45441ecbab0f8e7fb185e1bb25b9f44 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Mon, 21 Apr 2025 15:54:55 +0300 Subject: [PATCH 23/27] docs update --- .../schematics/src/utils/ng-module.ts | 47 ++++++++++++++--- .../schematics/src/utils/standalone.ts | 52 +++++++++++++++++++ 2 files changed, 93 insertions(+), 6 deletions(-) diff --git a/npm/ng-packs/packages/schematics/src/utils/ng-module.ts b/npm/ng-packs/packages/schematics/src/utils/ng-module.ts index f048588d5f9..ca2d327eb6a 100644 --- a/npm/ng-packs/packages/schematics/src/utils/ng-module.ts +++ b/npm/ng-packs/packages/schematics/src/utils/ng-module.ts @@ -7,6 +7,21 @@ import { normalize, Path } from '@angular-devkit/core'; import * as path from 'path'; import { removeEmptyElementsFromArrayLiteral } from './ast'; +/** + * Checks whether a specific import or provider exists in the specified metadata + * array (`imports`, `providers`, etc.) of the `NgModule` decorator in the AppModule. + * + * This function locates the AppModule file of the given Angular project, + * parses its AST, and inspects the specified metadata array to determine + * if it includes an element matching the provided string (e.g., `CommonModule`, `HttpClientModule`). + * + * @param host - The virtual file system tree used by Angular schematics. + * @param projectName - The name of the Angular project. + * @param metadataFn - The name (string) to match against the elements of the metadata array. + * @param metadataName - The metadata field to search in (e.g., 'imports', 'providers'). Defaults to 'imports'. + * @returns A promise that resolves to `true` if the metadata function is found, or `false` otherwise. + * @throws SchematicsException if the AppModule file or expected metadata is not found or malformed. + */ export const hasImportInNgModule = async ( host: Tree, projectName: string, @@ -23,14 +38,9 @@ export const hasImportInNgModule = async ( const source = createSourceFile(host, appModulePath); - console.log('AppModule content:\n', buffer.toString('utf-8')); - // Get the NgModule decorator metadata const ngModuleDecorator = getDecoratorMetadata(source, 'NgModule', '@angular/core')[0]; - console.log( - 'Found NgModule decorators:', - getDecoratorMetadata(source, 'NgModule', '@angular/core'), - ); + if (!ngModuleDecorator) { throw new SchematicsException('The app module does not found'); } @@ -50,6 +60,20 @@ export const hasImportInNgModule = async ( return elements.some(f => f.getText().match(metadataFn)); }; +/** + * Attempts to locate the path of the `AppRoutingModule` file that is imported + * within the root AppModule file of an Angular application. + * + * This function reads the AppModule file (resolved from the main file path), + * parses its AST, and searches for an import declaration that imports + * `AppRoutingModule`. Once found, it resolves the import path to a normalized + * file path relative to the workspace root. + * + * @param tree - The virtual file system tree used by Angular schematics. + * @param mainFilePath - The path to the main entry file of the Angular application (typically `main.ts`). + * @returns A normalized workspace-relative path to the AppRoutingModule file if found, or `null` otherwise. + * @throws If the route file path is resolved but the file does not exist in the tree. + */ export async function findAppRoutesModulePath( tree: Tree, mainFilePath: string, @@ -100,6 +124,17 @@ export async function findAppRoutesModulePath( return null; } +/** + * Cleans up empty or invalid expressions (e.g., extra commas) from the `imports` and `providers` + * arrays in the NgModule decorator of an Angular module file. + * + * This function parses the source file's AST, locates the `NgModule` decorator, and processes + * the `imports` and `providers` metadata fields. If these fields contain array literals with + * empty slots (such as trailing or double commas), they are removed and the array is rewritten. + * + * @param source - The TypeScript source file containing the Angular module. + * @param recorder - The recorder used to apply changes to the source file. + */ export function cleanEmptyExprFromModule(source: ts.SourceFile, recorder: UpdateRecorder): void { const ngModuleNode = getDecoratorMetadata(source, 'NgModule', '@angular/core')[0]; if (!ngModuleNode) return; diff --git a/npm/ng-packs/packages/schematics/src/utils/standalone.ts b/npm/ng-packs/packages/schematics/src/utils/standalone.ts index 982c56875e4..60364eb9764 100644 --- a/npm/ng-packs/packages/schematics/src/utils/standalone.ts +++ b/npm/ng-packs/packages/schematics/src/utils/standalone.ts @@ -7,12 +7,36 @@ import * as path from 'path'; import { findNodes } from './angular'; import { removeEmptyElementsFromArrayLiteral } from './ast'; +/** + * Retrieves the file path of the application's configuration used in a standalone + * Angular application setup. + * + * This function locates the `bootstrapApplication` call in the main entry file and + * resolves the path to the configuration object passed to it (typically `appConfig`). + * + * @param host - The virtual file system tree used by Angular schematics. + * @param mainFilePath - The path to the main entry file of the Angular application (e.g., `main.ts`). + * @returns The resolved file path of the application's configuration, or an empty string if not found. + */ export const getAppConfigPath = (host: Tree, mainFilePath: string): string => { const bootstrapCall = findBootstrapApplicationCall(host, mainFilePath); const appConfig = findAppConfig(bootstrapCall, host, mainFilePath); return appConfig?.filePath || ''; }; +/** + * Attempts to locate the file path of the `routes` array used in a standalone + * Angular application configuration. + * + * This function resolves the application's config file (typically where `routes` is defined or imported), + * parses the file, and inspects its import declarations to find the import associated with `routes`. + * It then resolves and normalizes the file path of the `routes` definition and returns it. + * + * @param tree - The virtual file system tree used by Angular schematics. + * @param mainFilePath - The path to the main entry file of the Angular application (e.g., `main.ts`). + * @returns The normalized workspace-relative path to the file where `routes` is defined, or `null` if not found. + * @throws If the `routes` import path is found but the file does not exist in the tree. + */ export function findAppRoutesPath(tree: Tree, mainFilePath: string): Path | null { const appConfigPath = getAppConfigPath(tree, mainFilePath); if (!appConfigPath || !tree.exists(appConfigPath)) return null; @@ -60,6 +84,20 @@ export function findAppRoutesPath(tree: Tree, mainFilePath: string): Path | null return null; } +/** + * Checks whether a specific provider is registered in the `providers` array of the + * standalone application configuration (typically within `app.config.ts`) in an Angular project. + * + * This function reads and parses the application configuration file, looks for the + * `providers` property in the configuration object, and checks whether it includes + * the specified provider name. + * + * @param host - The virtual file system tree used by Angular schematics. + * @param projectName - The name of the Angular project. + * @param providerName - The name of the provider to search for (as a string match). + * @returns A promise that resolves to `true` if the provider is found, otherwise `false`. + * @throws SchematicsException if the app config file cannot be read. + */ export const hasProviderInStandaloneAppConfig = async ( host: Tree, projectName: string, @@ -95,6 +133,20 @@ export const hasProviderInStandaloneAppConfig = async ( return providersArray.elements.some(el => el.getText().includes(providerName)); }; +/** + * Cleans up empty or invalid expressions (e.g., extra or trailing commas) from the + * `providers` array within a standalone Angular application configuration object. + * + * This function parses the source file's AST to locate variable declarations that + * define an object literal. It then searches for a `providers` property and removes + * any empty elements from its array literal, replacing it with a cleaned version. + * + * Typically used in Angular schematics to ensure the `providers` array in `app.config.ts` + * is free of empty slots after modifications. + * + * @param source - The TypeScript source file containing the app configuration. + * @param recorder - The recorder used to apply changes to the source file. + */ export function cleanEmptyExprFromProviders(source: ts.SourceFile, recorder: UpdateRecorder): void { const varStatements = findNodes(source, ts.isVariableStatement); const printer = ts.createPrinter(); From 4ea28553e15f5bc097193cfb019b8f3749f01062 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Mon, 21 Apr 2025 19:10:10 +0300 Subject: [PATCH 24/27] template type enum added --- .../src/commands/create-lib/index.ts | 21 +++++++++++-------- .../create-lib/models/generate-lib-schema.ts | 7 ++++++- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts index 01eb241dbee..b30a7cc011a 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts @@ -37,7 +37,7 @@ import { ProjectDefinition, WorkspaceDefinition } from '../../utils/angular/work import { addLibToWorkspaceFile } from '../../utils/angular-schematic/generate-lib'; import * as cases from '../../utils/text'; import { Exception } from '../../enums/exception'; -import { GenerateLibSchema } from './models/generate-lib-schema'; +import { GenerateLibSchema, GenerateLibTemplateType } from './models/generate-lib-schema'; import { getMainFilePath } from '../../utils/angular/standalone/util'; export default function (schema: GenerateLibSchema) { @@ -74,13 +74,13 @@ function createLibrary(options: GenerateLibSchema): Rule { const target = await resolveProject(tree, options.packageName, null); if (!target || options.override) { if (options.isSecondaryEntrypoint) { - if (options.templateType === 'standalone') { + if (options.templateType === GenerateLibTemplateType.Standalone) { return createLibSecondaryEntryWithStandaloneTemplate(tree, options); } else { return createLibSecondaryEntry(tree, options); } } - if (options.templateType === 'module') { + if (options.templateType === GenerateLibTemplateType.Module) { return createLibFromModuleTemplate(tree, options); } else { return createLibFromModuleStandaloneTemplate(tree, options); @@ -238,16 +238,16 @@ export function importConfigModuleToDefaultProjectAppModule( : await hasImportInNgModule( tree, projectName, - options.templateType === 'standalone' + options.templateType === GenerateLibTemplateType.Standalone ? `provide${pascal(packageName)}Config` : `${pascal(packageName)}ConfigModule`, - options.templateType === 'standalone' ? 'providers' : 'imports', + options.templateType === GenerateLibTemplateType.Standalone ? 'providers' : 'imports', ); if (providerAlreadyExists) { return; } - if (options.templateType === 'standalone') { + if (options.templateType === GenerateLibTemplateType.Standalone) { rules.push( addRootProvider(projectName, code => { const configFn = code.external( @@ -299,7 +299,7 @@ export function addRoutingToAppRoutingModule( const content = buffer.toString(); const source = ts.createSourceFile(appRoutesPath, content, ts.ScriptTarget.Latest, true); const routeExpr = - options.templateType === 'standalone' + options.templateType === GenerateLibTemplateType.Standalone ? `() => import('${routePath}').then(m => m.${macroName}_ROUTES)` : `() => import('${routePath}').then(m => m.${moduleName}.forLazy())`; const routeToAdd = `{ path: '${routePath}', loadChildren: ${routeExpr} }`; @@ -322,7 +322,10 @@ export function addRoutingToAppRoutingModule( return; } const appRoutingModuleContent = appRoutingModule.toString(); - const routeExpr = options.templateType === 'standalone' ? `${macroName}_ROUTES` : moduleName; + const routeExpr = + options.templateType === GenerateLibTemplateType.Standalone + ? `${macroName}_ROUTES` + : moduleName; if (appRoutingModuleContent.includes(routeExpr)) { return; } @@ -334,7 +337,7 @@ export function addRoutingToAppRoutingModule( true, ); const importStatement = - options.templateType === 'standalone' + options.templateType === GenerateLibTemplateType.Standalone ? `() => import('${routePath}').then(m => m.${macroName}_ROUTES)` : `() => import('${routePath}').then(m => m.${moduleName}.forLazy())`; const routeDefinition = `{ path: '${routePath}', loadChildren: ${importStatement} }`; diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/models/generate-lib-schema.ts b/npm/ng-packs/packages/schematics/src/commands/create-lib/models/generate-lib-schema.ts index a3a9314abe8..aca9f08e491 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/models/generate-lib-schema.ts +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/models/generate-lib-schema.ts @@ -1,3 +1,8 @@ +export enum GenerateLibTemplateType { + Standalone = 'standalone', + Module = 'module', +} + export interface GenerateLibSchema { /** * Angular package name will create @@ -11,7 +16,7 @@ export interface GenerateLibSchema { /** * İs the package has standalone template */ - templateType: 'standalone' | 'module'; + templateType: GenerateLibTemplateType; override: boolean; From d0f3090e933defe4e78d78c76175b1a7f06e1955 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Tue, 22 Apr 2025 13:28:49 +0300 Subject: [PATCH 25/27] standalone template component name updated --- .../src/lib/__libraryName@kebab__.component.ts.template | 2 +- .../src/lib/__libraryName@kebab__.routes.ts.template | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__.component.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__.component.ts.template index 78dae13b85f..3c8ada66b8f 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__.component.ts.template +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__.component.ts.template @@ -8,4 +8,4 @@ import {ThemeSharedModule} from "@abp/ng.theme.shared"; template: `

Lazy Loaded Test Component

`, imports: [CoreModule, ThemeSharedModule], }) -export class <%= pascal(libraryName) %>HomeComponent {} +export class <%= pascal(libraryName) %>Component {} diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__.routes.ts.template b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__.routes.ts.template index 4fb52f82597..975fdaa261d 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__.routes.ts.template +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__.routes.ts.template @@ -4,6 +4,6 @@ export const <%= macro(libraryName) %>_ROUTES: Routes = [ { path: '', loadComponent: () => - import('./<%= kebab(libraryName) %>.component').then(m => m.<%= pascal(libraryName) %>HomeComponent), + import('./<%= kebab(libraryName) %>.component').then(m => m.<%= pascal(libraryName) %>Component), }, ]; From 65063e5fe5026faeee5f479164118cebc7102955 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Tue, 22 Apr 2025 19:04:53 +0300 Subject: [PATCH 26/27] standalone router variable updated --- .../packages/schematics/src/commands/create-lib/index.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts index b30a7cc011a..49645c3e454 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts @@ -303,7 +303,7 @@ export function addRoutingToAppRoutingModule( ? `() => import('${routePath}').then(m => m.${macroName}_ROUTES)` : `() => import('${routePath}').then(m => m.${moduleName}.forLazy())`; const routeToAdd = `{ path: '${routePath}', loadChildren: ${routeExpr} }`; - const change = addRouteToRoutesArray(source, 'routes', routeToAdd); + const change = addRouteToRoutesArray(source, 'APP_ROUTES', routeToAdd); if (change instanceof InsertChange) { const recorder = tree.beginUpdate(appRoutesPath); @@ -396,12 +396,13 @@ export function addRouteToRoutesArray( return null; } + const hasTrailingComma = arrayLiteral.elements.hasTrailingComma ?? false; const insertPos = - arrayLiteral.elements.hasTrailingComma || arrayLiteral.elements.length === 0 + hasTrailingComma || arrayLiteral.elements.length === 0 ? arrayLiteral.getEnd() - 1 : arrayLiteral.elements[arrayLiteral.elements.length - 1].getEnd(); - const prefix = arrayLiteral.elements.length > 0 ? ',\n ' : ' '; + const prefix = arrayLiteral.elements.length > 0 && !hasTrailingComma ? ',\n ' : ' '; const toAdd = `${prefix}${routeToAdd}`; return new InsertChange(source.fileName, insertPos, toAdd); From b8996b9ec6f460f4acdf2fba9921cbe40301b723 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Fri, 25 Apr 2025 15:58:06 +0300 Subject: [PATCH 27/27] comment removed --- npm/ng-packs/packages/schematics/src/utils/workspace.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/npm/ng-packs/packages/schematics/src/utils/workspace.ts b/npm/ng-packs/packages/schematics/src/utils/workspace.ts index 9c71d318642..8105a5e1e4f 100644 --- a/npm/ng-packs/packages/schematics/src/utils/workspace.ts +++ b/npm/ng-packs/packages/schematics/src/utils/workspace.ts @@ -52,7 +52,6 @@ export async function resolveProject( // @typescript-eslint/no-explicit-any notFoundValue: T = NOT_FOUND_VALUE as unknown as any, ): Promise { - // name = name || readWorkspaceSchema(tree).defaultProject || getFirstApplication(tree).name!; name = name || getFirstApplication(tree).name!; const workspace = await getWorkspace(tree); let definition: Project['definition'] | undefined;