diff --git a/Directory.Packages.props b/Directory.Packages.props index 39cdffbe87..07eaa65740 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -3,187 +3,187 @@ true - + - - + + - + - - + + - - - - - - + + + + + + - - - - - - - + + + + + + + - - - + + + - + - - + + - + - + - + - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + - - - + + + - - - - - - - - + + + + + + + + - - - - + + + + - - - - - - - - + + + + + + + + - + - - - + + + - - - - + + + + - + - + - - - - - + + + + + - - - - + + + + - + diff --git a/docs/en/framework/architecture/modularity/extending/module-entity-extensions.md b/docs/en/framework/architecture/modularity/extending/module-entity-extensions.md index 8a5b50652e..ac041895d7 100644 --- a/docs/en/framework/architecture/modularity/extending/module-entity-extensions.md +++ b/docs/en/framework/architecture/modularity/extending/module-entity-extensions.md @@ -151,6 +151,17 @@ property => > Tip: Use `DefaultValueFactory` option only if the default value may change over the time (like `DateTime.Now` in this example). If it is a constant value, then use the `DefaultValue` option. +### DataTypeAttribute + +`DataTypeAttribute` is used to specify the type of the property. It is used to determine how to render the property on the user interface: + +```csharp +property => +{ + property.Attributes.Add(new DataTypeAttribute(DataType.Date)); +} +``` + ### Validation Entity extension system allows you to define validation for extension properties in a few ways. diff --git a/docs/en/framework/data/entity-framework-core/index.md b/docs/en/framework/data/entity-framework-core/index.md index 293e58daad..a50edb2d4a 100644 --- a/docs/en/framework/data/entity-framework-core/index.md +++ b/docs/en/framework/data/entity-framework-core/index.md @@ -755,7 +755,17 @@ public static class QADbContextModelCreatingExtensions > The `Object Extension` feature need the `Change Tracking`, which means you can't use the read-only repositories for the entities that have `extension properties(MapEfCoreProperty)`, Please see the [Repositories documentation](../../architecture/domain-driven-design/repositories.md) to learn the change tracking behavior. -See the "*ConfigureByConvention Method*" section above for more information. +See the **ConfigureByConvention Method** section above for more information. + +### Accessing Extra Properties(Shadow Properties) + +Extra properties stored in separate fields in the database are known as **Shadow Properties**. These properties are not defined in the entity class, but are part of the EF Core model and can be referenced in LINQ queries using the EF.Property static method + +```csharp +var query = (await GetQueryableAsync()).Where(x => EF.Property(x, "Title") == "MyTitle"); +``` + +See the [EF Core Shadow and Indexer Properties document](https://learn.microsoft.com/en-us/ef/core/modeling/shadow-properties) for more information. ## Advanced Topics diff --git a/docs/en/framework/fundamentals/localization.md b/docs/en/framework/fundamentals/localization.md index 61118fd742..6e8a3f678a 100644 --- a/docs/en/framework/fundamentals/localization.md +++ b/docs/en/framework/fundamentals/localization.md @@ -248,6 +248,17 @@ namespace MyProject The `L` property is also available for some other base classes like `AbpController` and `AbpPageModel`. +## Supported Languages + +You can configure the `AbpLocalizationOptions`'s `Languages` property to add the languages supported by the application. The template already sets common languages, but you can add new languages as shown below: + +```csharp +Configure(options => +{ + options.Languages.Add(new LanguageInfo("uz", "uz", "Uzbek")); +}); +``` + ## The Client Side See the following documents to learn how to reuse the same localization texts in the JavaScript side; diff --git a/docs/en/framework/ui/maui/index.md b/docs/en/framework/ui/maui/index.md index 573278a060..28f74aa6a3 100644 --- a/docs/en/framework/ui/maui/index.md +++ b/docs/en/framework/ui/maui/index.md @@ -37,10 +37,15 @@ Open a command line terminal and run the `adb reverse` command to expose a port `adb reverse tcp:44305 tcp:44305` -> You should replace "44305" with the real port. -> You should run the command after starting the emulator. +> Replace `44305` with the port number your backend application is running on. +> +> Run this command **after** the Android emulator has started. -> If you don't have a separate installation of Android Debug Bridge, you can open it from **Visual Studio** by following toolbar menu `Tools` > `Android` > `Android Adb Command Prompt`. Android emulator has to be running for this operation. +> [!IMPORTANT] +> If your project uses a **tiered** or **microservice** architecture, ensure that both the **auth server** and all **remote service ports** are properly proxied using the `adb reverse` command. You can find all the required remote service ports and AuthServer configurations in your `YourProjectName.Maui/appsettings.json` file. + +> [!NOTE] +> If you don't have a separate installation of **Android Debug Bridge** _(adb)_, you can open it from **Visual Studio** by following toolbar menu `Tools` > `Android` > `Android Adb Command Prompt`. Android emulator has to be running for this operation. ### iOS diff --git a/docs/en/framework/ui/mvc-razor-pages/customization-user-interface.md b/docs/en/framework/ui/mvc-razor-pages/customization-user-interface.md index bc3b7d8fc2..c279f685c0 100644 --- a/docs/en/framework/ui/mvc-razor-pages/customization-user-interface.md +++ b/docs/en/framework/ui/mvc-razor-pages/customization-user-interface.md @@ -89,7 +89,10 @@ This example overrides the **login page** defined by the [Account Module](../../ Create a page model class deriving from the ` LoginModel ` (defined in the ` Volo.Abp.Account.Web.Pages.Account ` namespace): +> If you are using the `AbpAccountWebOpenIddictModule` or `AbpAccountPublicWebOpenIddictModule`, the base class is `OpenIddictSupportedLoginModel` instead of `LoginModel`. And you should change the `ExposeServices` attribute to `[ExposeServices(typeof (MyLoginModel), typeof(OpenIddictSupportedLoginModel), typeof(LoginModel))]` + ````csharp +[ExposeServices(typeof (MyLoginModel), typeof(LoginModel))] public class MyLoginModel : LoginModel { public MyLoginModel( @@ -114,8 +117,6 @@ public class MyLoginModel : LoginModel You can override any method or add new properties/methods if needed. -> Notice that we didn't use `[Dependency(ReplaceServices = true)]` or `[ExposeServices(typeof(LoginModel))]` since we don't want to replace the existing class in the dependency injection, we define a new one. - Copy `Login.cshtml` file into your solution as just described above. Change the **@model** directive to point to the `MyLoginModel`: ````xml diff --git a/docs/en/modules/cms-kit-pro/url-forwarding.md b/docs/en/modules/cms-kit-pro/url-forwarding.md index a059ef1305..10ca0a7f98 100644 --- a/docs/en/modules/cms-kit-pro/url-forwarding.md +++ b/docs/en/modules/cms-kit-pro/url-forwarding.md @@ -37,6 +37,20 @@ You can create new forwardings or update/delete existing ones, in the admin side ![url-forwarding-page](../../images/url-forwarding-page.png) + +## Options + +### ShortenedUrlCacheOptions + +`ShortenedUrlCacheOptions` is used to configure the cache settings for the URL forwarding system. Example: + +```csharp +Configure(options => +{ + options.CacheAllOnStartup = true; // Cache all shortened URLs on startup +}); +``` + # Internals ## Domain Layer diff --git a/docs/en/modules/language-management.md b/docs/en/modules/language-management.md index 77490f28ef..3bce0d46b0 100644 --- a/docs/en/modules/language-management.md +++ b/docs/en/modules/language-management.md @@ -62,7 +62,7 @@ This module adds some initial data (see [the data seed system](../framework/infr * Creates language records configured using `AbpLocalizationOptions`. -If you want to change the seeded language list, see the next section. +If you want to change the seeded language list, see the [Localization](../framework/fundamentals/localization.md#Supported-Languages) document. ## Internals diff --git a/docs/en/release-info/migration-guides/abp-8-2.md b/docs/en/release-info/migration-guides/abp-8-2.md index 15de922d4f..5f811da2d4 100644 --- a/docs/en/release-info/migration-guides/abp-8-2.md +++ b/docs/en/release-info/migration-guides/abp-8-2.md @@ -28,6 +28,7 @@ With this version on, ABP Framework allows you to use single blog mode, without * `Volo.Blogging.Pages.Members` -> `Volo.Blogging.Pages.Blogs.Members` (members folder) > If you haven't overridden the pages above, then you don't need to make any additional changes. See [#19418](https://github.com/abpframework/abp/pull/19418) for more information. + ## Removed `FlagIcon` property from the `ILanguageInfo` The `FlagIcon` property has been removed from the `ILanguageInfo` interface since we removed the flag icon library in the earlier versions from all of our themes and none of them using it now. @@ -57,11 +58,20 @@ In this version, the Angular UI has been updated to use the Angular version 17.3 The **Session Management** feature allows you to prevent concurrent login and manage user sessions. -In this version, a new entity called `IdentitySession` has been added to the framework and you should create a new migration and apply it to your database. +In this version, a new entity called `IdentitySession` has been added to the framework and you need to add new `DbSet` to your `DbContext` class if it implements `IIdentityDbContext` interface. + +```csharp +public class YourDbContext : AbpDbContext, IIdentityDbContext +{ + public DbSet Sessions { get; set; } +} +``` + +You should also create a new migration and apply it to your database. ## Upgraded NuGet Dependencies -You can see the following list of NuGet libraries that have been upgraded with this release, if you are using one of these packages explicitly, you may consider upgrading them in your solution: +You can see the following list of NuGet libraries that have been upgraded with this release, **if you are using one of these packages explicitly**, you may consider upgrading them in your solution, especially **Microsoft.IdentityModel.*** packages: | Package | Old Version | New Version | | ---------------------------------------------------------- | ----------- | ----------- | diff --git a/docs/en/tutorials/microservice/part-06.md b/docs/en/tutorials/microservice/part-06.md index 18db56cc0b..1cc19bf6d8 100644 --- a/docs/en/tutorials/microservice/part-06.md +++ b/docs/en/tutorials/microservice/part-06.md @@ -107,9 +107,9 @@ public class ProductIntegrationService : ApplicationService, IProductIntegration Now that we have created the `IProductIntegrationService` interface and the `ProductIntegrationService` class, we can consume this service from the Ordering service. -### Adding a Reference to the `CloudCrm.CatalogService.Contracts` Package +### Adding a Reference to the `CloudCrm.OrderingService` Package -First, we need to add a reference to the `CloudCrm.CatalogService.Contracts` package in the Ordering service. Open the ABP Studio, and stop the application(s) if it is running. Then, open the *Solution Explorer* and right-click on the `CloudCrm.OrderingService` package. Select *Add* -> *Package Reference* command: +First, we need to add a reference to the `CloudCrm.OrderingService` package in the Ordering service. Open the ABP Studio, and stop the application(s) if it is running. Then, open the *Solution Explorer* and right-click on the `CloudCrm.OrderingService` package. Select *Add* -> *Package Reference* command: ![add-package-reference-ordering-service](images/add-package-reference-ordering-service.png) diff --git a/docs/en/tutorials/todo/layered/index.md b/docs/en/tutorials/todo/layered/index.md index e6e310a39a..86a480ad42 100644 --- a/docs/en/tutorials/todo/layered/index.md +++ b/docs/en/tutorials/todo/layered/index.md @@ -528,7 +528,7 @@ The interesting part here is how we communicate with the server. See the *Dynami ### Index.css -As the final touch, Create a file named `Index.css` in the `Pages` folder of the *TodoApp.Web* project and replace it with the following content: +As the final touch, Create a file named `Index.css` in the `Pages` folder of the *TodoApp.Web* project and add the following content: ```css #TodoList{ @@ -664,7 +664,7 @@ Open the `Index.razor` file in the `Pages` folder of the {{if UI=="Blazor" || UI ### Index.razor.css -As the final touch, open the `Index.razor.css` file in the `Pages` folder of the {{if UI=="Blazor" || UI=="BlazorWebApp"}}*TodoApp.Blazor.Client*{{else if UI=="BlazorServer"}} *TodoApp.Blazor* {{else if UI=="MAUIBlazor"}} *TodoApp.MauiBlazor* {{end}} project and replace it with the following content: +As the final touch, open the `Index.razor.css` file in the `Pages` folder of the {{if UI=="Blazor" || UI=="BlazorWebApp"}}*TodoApp.Blazor.Client*{{else if UI=="BlazorServer"}} *TodoApp.Blazor* {{else if UI=="MAUIBlazor"}} *TodoApp.MauiBlazor* {{end}} project and add the following content: ```css #TodoList{ @@ -827,7 +827,7 @@ Open the `/angular/src/app/home/home.component.html` file and replace its conten ### home.component.scss -As the final touch, open the `/angular/src/app/home/home.component.scss` file and replace its content with the following code block: +As the final touch, open the `/angular/src/app/home/home.component.scss` file and add the following code block: ```css #TodoList{ diff --git a/docs/en/tutorials/todo/single-layer/index.md b/docs/en/tutorials/todo/single-layer/index.md index dc865a0a75..6ce5bec5b0 100644 --- a/docs/en/tutorials/todo/single-layer/index.md +++ b/docs/en/tutorials/todo/single-layer/index.md @@ -309,10 +309,8 @@ public interface ITodoAppService : IApplicationService Create a `TodoAppService` class under the `Services` folder of {{if UI=="Blazor"}}your `TodoApp.Host` project{{else}}your project{{end}}, as shown below: ```csharp -using TodoApp.Services; using TodoApp.Services.Dtos; using TodoApp.Entities; -using Volo.Abp.Application.Services; using Volo.Abp.Domain.Repositories; namespace TodoApp.Services; @@ -517,7 +515,7 @@ The interesting part here is how we communicate with the server. See the *Dynami ### Index.cshtml.css -As for the final touch, open the `Index.cshtml.css` file in the `Pages` folder and replace with the following content: +As for the final touch, open the `Index.cshtml.css` file in the `Pages` folder and add the following code block at the end of the file: ````css #TodoList{ @@ -655,7 +653,7 @@ Open the `Index.razor` file in the `Pages` folder and replace the content with t ### Index.razor.css -As the final touch, open the `Index.razor.css` file in the `Pages` folder and replace it with the following content: +As the final touch, open the `Index.razor.css` file in the `Pages` folder and add the following code block at the end of the file: ````css #TodoList{ @@ -801,7 +799,7 @@ Open the `/angular/src/app/home/home.component.html` file and replace its conten ### home.component.scss -As the final touch, open the `/angular/src/app/home/home.component.scss` file and replace its content with the following code block: +As the final touch, open the `/angular/src/app/home/home.component.scss` file and add the following code block at the end of the file: ````css #TodoList{ diff --git a/docs/en/ui-themes/lepton-x/images/account-layout-background-style.png b/docs/en/ui-themes/lepton-x/images/account-layout-background-style.png new file mode 100644 index 0000000000..5b2f32661d Binary files /dev/null and b/docs/en/ui-themes/lepton-x/images/account-layout-background-style.png differ diff --git a/docs/en/ui-themes/lepton-x/mvc.md b/docs/en/ui-themes/lepton-x/mvc.md index 683306adfe..e41d12a331 100644 --- a/docs/en/ui-themes/lepton-x/mvc.md +++ b/docs/en/ui-themes/lepton-x/mvc.md @@ -47,7 +47,8 @@ Before starting to customize the theme, you can consider downloading the source --- ### Appearance -You can set a default theme, add or remove appearance styles by using **LeptonXThemeOptions**. + +You can set a default theme, add or remove appearance styles and layout background styles by using **LeptonXThemeOptions**. - `DefaultStyle`: Defines the default fallback theme. The default value is **Dim** @@ -133,15 +134,26 @@ Layout options of the MVC Razor Pages UI can be manageable by using **LeptonXThe - `MobileMenuSelector`: Defines items to be displayed at the mobile menu. The default value is the first 2 items from the main menu items. + ```csharp + Configure(options => + { + options.MobileMenuSelector = items => items.Where(x => x.MenuItem.Name == "MyProjectName.Home" || x.MenuItem.Name == "MyProjectName.Dashboard"); + }); + ``` + ![leptonx-mobile-menu-preview](images/mobile-menu-preview.png) +- `AccountLayoutBackgroundStyle`: Defines the background style of the account layout. + ```csharp Configure(options => { - options.MobileMenuSelector = items => items.Where(x => x.MenuItem.Name == "MyProjectName.Home" || x.MenuItem.Name == "MyProjectName.Dashboard"); + options.AccountLayoutBackgroundStyle = "background-image: url('/images/login-background-image.svg') !important;"; }); ``` + ![leptonx-account-layout-background-style](images/account-layout-background-style.png) + ### Layouts **LeptonX** offers two **ready-made layouts** for your web application. One of them is **placed** with the **menu items** on the **top** and the other with the **menu items** on the **sides**. diff --git a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Theming/DefaultThemeManager.cs b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Theming/DefaultThemeManager.cs index 5abd114f83..76c7bbf556 100644 --- a/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Theming/DefaultThemeManager.cs +++ b/framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Theming/DefaultThemeManager.cs @@ -29,6 +29,6 @@ public class DefaultThemeManager : IThemeManager, IScopedDependency, IServicePro } _currentTheme = (ITheme)ServiceProvider.GetRequiredService(ThemeSelector.GetCurrentThemeInfo().ThemeType); - return CurrentTheme; + return _currentTheme; } } diff --git a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs index 8dfa51768d..73edea9fdf 100644 --- a/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs +++ b/framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs @@ -313,17 +313,42 @@ public class AbpApplicationConfigurationAppService : ApplicationService, IAbpApp { var timeZone = await _settingProvider.GetOrNullAsync(TimingSettingNames.TimeZone); + string? timeZoneId = null; + string? timeZoneName = null; + if (!timeZone.IsNullOrWhiteSpace()) + { + try + { + if (_timezoneProvider.GetIanaTimezones().Any(x => x.Value == timeZone)) + { + timeZoneId = _timezoneProvider.IanaToWindows(timeZone); + timeZoneName = timeZone; + } + else if (_timezoneProvider.GetWindowsTimezones().Any(x => x.Value == timeZone)) + { + timeZoneId = timeZone; + timeZoneName = _timezoneProvider.WindowsToIana(timeZone); + } + } + catch (Exception ex) + { + timeZoneId = null; + timeZoneName = null; + Logger.LogWarning(ex, $"Exception occurred while getting timezone({timeZone}) information"); + } + } + return new TimingDto { TimeZone = new TimeZone { Windows = new WindowsTimeZone { - TimeZoneId = timeZone.IsNullOrWhiteSpace() ? null : _timezoneProvider.IanaToWindows(timeZone) + TimeZoneId = timeZoneId }, Iana = new IanaTimeZone { - TimeZoneName = timeZone + TimeZoneName = timeZoneName } } }; diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/AbpCachingModule.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/AbpCachingModule.cs index de5f60ee4e..632bfa416e 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/AbpCachingModule.cs +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/AbpCachingModule.cs @@ -1,7 +1,5 @@ using Microsoft.Extensions.DependencyInjection; using System; -using Microsoft.Extensions.Caching.Hybrid; -using Microsoft.Extensions.DependencyInjection.Extensions; using Volo.Abp.Caching.Hybrid; using Volo.Abp.Json; using Volo.Abp.Modularity; diff --git a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCache.cs b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCache.cs index 2894af9f3d..66506a0b56 100644 --- a/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCache.cs +++ b/framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCache.cs @@ -75,7 +75,7 @@ public class AbpHybridCache : IHybridCache : IHybridCache>.Instance; KeyNormalizer = keyNormalizer; @@ -215,10 +215,15 @@ public class AbpHybridCache : IHybridCache(bytes, 0, bytes.Length));; + // Because HybridCache wraps the cache in L2(distributed cache), we can’t unwrap it directly and can only retrieve the value through its API + return await HybridCache.GetOrCreateAsync( + key: NormalizeKey(key), + factory: async cancel => await factory(), + options: optionsFactory?.Invoke(), + tags: null, + cancellationToken: token); } value = await factory(); diff --git a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/PathHelper.cs b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/PathHelper.cs index 1618004bb5..4d3ff3ee77 100644 --- a/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/PathHelper.cs +++ b/framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/PathHelper.cs @@ -20,8 +20,9 @@ static internal class PathHelper static internal string GetMauiBlazorAssemblyFilePath(string directory, string projectFileName) { - return Directory.GetFiles(directory, "*.dll", SearchOption.AllDirectories).FirstOrDefault(f => + return Directory.GetFiles(Path.Combine(directory, "bin"), "*.dll", SearchOption.AllDirectories).FirstOrDefault(f => !f.Contains("android") && + !f.Contains("windows10") && f.EndsWith(projectFileName + ".dll", StringComparison.OrdinalIgnoreCase)); } diff --git a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/AbpEntityChangeOptions.cs b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/AbpEntityChangeOptions.cs index a42b921068..2c705a01fd 100644 --- a/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/AbpEntityChangeOptions.cs +++ b/framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/AbpEntityChangeOptions.cs @@ -10,8 +10,18 @@ public class AbpEntityChangeOptions public IEntitySelectorList IgnoredNavigationEntitySelectors { get; set; } + /// + /// Default: true. + /// Update the aggregate root when any navigation property changes. + /// Some properties like ConcurrencyStamp,LastModificationTime,LastModifierId etc. will be updated. + /// + public bool UpdateAggregateRootWhenNavigationChanges { get; set; } = true; + + public IEntitySelectorList IgnoredUpdateAggregateRootSelectors { get; set; } + public AbpEntityChangeOptions() { IgnoredNavigationEntitySelectors = new EntitySelectorList(); + IgnoredUpdateAggregateRootSelectors = new EntitySelectorList(); } } diff --git a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs index 626daf0bb4..e91d08b8c9 100644 --- a/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs +++ b/framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs @@ -274,7 +274,9 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, continue; } - if (entityEntry.State == EntityState.Unchanged) + if (EntityChangeOptions.Value.UpdateAggregateRootWhenNavigationChanges && + EntityChangeOptions.Value.IgnoredUpdateAggregateRootSelectors.All(selector => !selector.Predicate(entityEntry.Entity.GetType())) && + entityEntry.State == EntityState.Unchanged) { ApplyAbpConceptsForModifiedEntity(entityEntry, true); } @@ -446,7 +448,12 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, EntityChangeOptions.Value.IgnoredNavigationEntitySelectors.All(selector => !selector.Predicate(entry.Entity.GetType())) && AbpEfCoreNavigationHelper.IsNavigationEntryModified(entry)) { - ApplyAbpConceptsForModifiedEntity(entry, true); + if (EntityChangeOptions.Value.UpdateAggregateRootWhenNavigationChanges && + EntityChangeOptions.Value.IgnoredUpdateAggregateRootSelectors.All(selector => !selector.Predicate(entry.Entity.GetType()))) + { + ApplyAbpConceptsForModifiedEntity(entry, true); + } + if (entry.Entity is ISoftDelete && entry.Entity.As().IsDeleted) { EntityChangeEventHelper.PublishEntityDeletedEvent(entry.Entity); @@ -478,11 +485,13 @@ public abstract class AbpDbContext : DbContext, IAbpEfCoreDbContext, } } - if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges) + if (EntityChangeOptions.Value.PublishEntityUpdatedEventWhenNavigationChanges && + EntityChangeOptions.Value.UpdateAggregateRootWhenNavigationChanges) { foreach (var entry in AbpEfCoreNavigationHelper.GetChangedEntityEntries() .Where(x => x.State == EntityState.Unchanged) - .Where(x=> EntityChangeOptions.Value.IgnoredNavigationEntitySelectors.All(selector => !selector.Predicate(x.Entity.GetType())))) + .Where(x => EntityChangeOptions.Value.IgnoredNavigationEntitySelectors.All(selector => !selector.Predicate(x.Entity.GetType()))) + .Where(x => EntityChangeOptions.Value.IgnoredUpdateAggregateRootSelectors.All(selector => !selector.Predicate(x.Entity.GetType())))) { UpdateConcurrencyStamp(entry); } diff --git a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/DomainEvents_Tests.cs b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/DomainEvents_Tests.cs index d82fb195ab..6249d588f5 100644 --- a/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/DomainEvents_Tests.cs +++ b/framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/DomainEvents_Tests.cs @@ -396,3 +396,134 @@ public class AbpEfCoreDomainEvents_Tests : EntityFrameworkCoreTestBase entityUpdatedEventTriggered.ShouldBeTrue(); } } + + +public abstract class AbpEfCoreDomainEvents_Disable_UpdateAggregateRoot_Tests : EntityFrameworkCoreTestBase +{ + protected readonly IRepository AppEntityWithNavigationsRepository; + protected readonly IRepository AppEntityWithNavigationForeignRepository; + protected readonly ILocalEventBus LocalEventBus; + + protected AbpEfCoreDomainEvents_Disable_UpdateAggregateRoot_Tests() + { + AppEntityWithNavigationsRepository = GetRequiredService>(); + AppEntityWithNavigationForeignRepository = GetRequiredService>(); + LocalEventBus = GetRequiredService(); + } + + protected override void AfterAddApplication(IServiceCollection services) + { + services.Configure(options => + { + options.PublishEntityUpdatedEventWhenNavigationChanges = true; + options.UpdateAggregateRootWhenNavigationChanges = false; + }); + + base.AfterAddApplication(services); + } + + [Fact] + public async Task Should_Trigger_Domain_Events_But_Do_Not_Change_Aggregate_Root_When_Navigation_Changes_Tests() + { + var entityId = Guid.NewGuid(); + + var newEntity = await AppEntityWithNavigationsRepository.InsertAsync(new AppEntityWithNavigations(entityId, "TestEntity")); + + var latestConcurrencyStamp = newEntity.ConcurrencyStamp; + var lastModificationTime = newEntity.LastModificationTime; + + var entityUpdatedEventTriggered = false; + + LocalEventBus.Subscribe>(data => + { + entityUpdatedEventTriggered = true; + + // The Aggregate will not be updated + data.Entity.ConcurrencyStamp.ShouldBe(latestConcurrencyStamp); + data.Entity.LastModificationTime.ShouldBe(lastModificationTime); + return Task.CompletedTask; + }); + + // Test with value object + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.AppEntityWithValueObjectAddress = new AppEntityWithValueObjectAddress("Turkey"); + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + + // Test with one to one + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToOne = new AppEntityWithNavigationChildOneToOne + { + ChildName = "ChildName" + }; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + + // Test with one to many + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.OneToMany = new List() + { + new AppEntityWithNavigationChildOneToMany + { + AppEntityWithNavigationId = entity.Id, + ChildName = "ChildName1" + } + }; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + + // Test with many to many + entityUpdatedEventTriggered = false; + await WithUnitOfWorkAsync(async () => + { + var entity = await AppEntityWithNavigationsRepository.GetAsync(entityId); + entity.ManyToMany = new List() + { + new AppEntityWithNavigationChildManyToMany + { + ChildName = "ChildName1" + } + }; + await AppEntityWithNavigationsRepository.UpdateAsync(entity); + }); + entityUpdatedEventTriggered.ShouldBeTrue(); + } +} + +public class AbpEfCoreDomainEvents_UpdateAggregateRootWhenNavigationChanges_Tests : AbpEfCoreDomainEvents_Disable_UpdateAggregateRoot_Tests +{ + protected override void AfterAddApplication(IServiceCollection services) + { + services.Configure(options => + { + options.UpdateAggregateRootWhenNavigationChanges = false; + }); + + base.AfterAddApplication(services); + } +} + +public class AbpEfCoreDomainEvents_IgnoredUpdateAggregateRootSelectors_Test : AbpEfCoreDomainEvents_Disable_UpdateAggregateRoot_Tests +{ + protected override void AfterAddApplication(IServiceCollection services) + { + services.Configure(options => + { + options.IgnoredUpdateAggregateRootSelectors.Add("AppEntityWithValueObjectAddress", x => x == typeof(AppEntityWithNavigations)); + }); + + base.AfterAddApplication(services); + } +} diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/ar.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/ar.json index 52de9ea37a..30f4b647e8 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/ar.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/ar.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "منح كافة الأذونات", "SelectAllInThisTab": "تحديد الكل", "SaveWithoutAnyPermissionsWarningMessage": "هل أنت متأكد أنك تريد الحفظ بدون أي أذونات؟", - "PermissionGroup": "مجموعة الأذونات" + "PermissionGroup": "مجموعة الأذونات", + "Filter": "تصفية" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/cs.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/cs.json index c0e38b9320..814b8e423c 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/cs.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/cs.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Dát veškerá oprávnění", "SelectAllInThisTab": "Vybrat vše", "SaveWithoutAnyPermissionsWarningMessage": "Opravdu chcete ukládat bez jakýchkoli oprávnění?", - "PermissionGroup": "Skupina oprávnění" + "PermissionGroup": "Skupina oprávnění", + "Filter": "Filtr" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/de.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/de.json index a2bb5fe72a..7a40c5d4d4 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/de.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/de.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Alle Berechtigungen erteilen", "SelectAllInThisTab": "Alle auswählen", "SaveWithoutAnyPermissionsWarningMessage": "Sind Sie sicher, dass Sie ohne Berechtigungen speichern möchten?", - "PermissionGroup": "Berechtigungsgruppe" + "PermissionGroup": "Berechtigungsgruppe", + "Filter": "Filtern" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/el.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/el.json index bc2c96db04..3449b7fab6 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/el.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/el.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Χορήγηση όλων των δικαιώματων", "SelectAllInThisTab": "Επιλογή όλων", "SaveWithoutAnyPermissionsWarningMessage": "Είστε βέβαιοι ότι θέλετε να αποθηκεύσετε χωρίς δικαιώματα;", - "PermissionGroup": "Ομάδα δικαιωμάτων" + "PermissionGroup": "Ομάδα δικαιωμάτων", + "Filter": "Φίλτρο" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/en-GB.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/en-GB.json index 62762d00a8..be6cbcd1f5 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/en-GB.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/en-GB.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Grant all permissions", "SelectAllInThisTab": "Select all", "SaveWithoutAnyPermissionsWarningMessage": "Are you sure you want to save without any permissions?", - "PermissionGroup": "Permission Group" + "PermissionGroup": "Permission Group", + "Filter": "Filter" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/en.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/en.json index 45aeb905e2..b8299d4e5b 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/en.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/en.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Grant all permissions", "SelectAllInThisTab": "Select all", "SaveWithoutAnyPermissionsWarningMessage": "Are you sure you want to save without any permissions?", - "PermissionGroup": "Permission Group" + "PermissionGroup": "Permission Group", + "Filter": "Filter" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/es.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/es.json index 610d86a2ff..622883b259 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/es.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/es.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Conceder todos los permisos", "SelectAllInThisTab": "Seleccionar todo", "SaveWithoutAnyPermissionsWarningMessage": "¿Estás seguro de que quieres guardar sin ningún permiso?", - "PermissionGroup": "Grupo de permisos" + "PermissionGroup": "Grupo de permisos", + "Filter": "Filtrar" } } diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/fa.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/fa.json index 615ef79412..7ea9a6c4f3 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/fa.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/fa.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "اعطای همه مجوزها", "SelectAllInThisTab": "انتخاب همه", "SaveWithoutAnyPermissionsWarningMessage": "آیا مطمئن هستید که می خواهید بدون هیچ دسترسی ذخیره کنید؟", - "PermissionGroup": "گروه دسترسی" + "PermissionGroup": "گروه دسترسی", + "Filter": "فیلتر" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/fi.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/fi.json index 2c24727ad5..f9b828ade4 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/fi.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/fi.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Myönnä kaikki käyttöoikeudet", "SelectAllInThisTab": "Valitse kaikki", "SaveWithoutAnyPermissionsWarningMessage": "Haluatko varmasti tallentaa ilman käyttöoikeuksia?", - "PermissionGroup": "Käyttöoikeus" + "PermissionGroup": "Käyttöoikeus", + "Filter": "Suodatus" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/fr.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/fr.json index b2676631a2..79f4e68377 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/fr.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/fr.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Accorder toutes les autorisations", "SelectAllInThisTab": "Sélectionner tous les", "SaveWithoutAnyPermissionsWarningMessage": "Êtes-vous sûr de vouloir enregistrer sans aucune autorisation ?", - "PermissionGroup": "Groupe d'autorisations" + "PermissionGroup": "Groupe d'autorisations", + "Filter": "Filtrer" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/hi.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/hi.json index fa271bc752..cdc030e8ef 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/hi.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/hi.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "सभी अनुमतियां प्रदान करें", "SelectAllInThisTab": "सभी का चयन करे", "SaveWithoutAnyPermissionsWarningMessage": "क्या आप वाकई बिना किसी अनुमति के सहेजना चाहते हैं?", - "PermissionGroup": "अनुमति समूह" + "PermissionGroup": "अनुमति समूह", + "Filter": "फ़िल्टर" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/hr.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/hr.json index 45034a5bb6..531d0a35d2 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/hr.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/hr.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Dodijelite sva dopuštenja", "SelectAllInThisTab": "Odaberi sve", "SaveWithoutAnyPermissionsWarningMessage": "Jeste li sigurni da želite spremiti bez ikakvih dopuštenja?", - "PermissionGroup": "Grupa dozvola" + "PermissionGroup": "Grupa dozvola", + "Filter": "Filtriraj" } } diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/hu.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/hu.json index fdb727bf2b..3177ef8fa7 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/hu.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/hu.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Adjon meg minden engedélyt", "SelectAllInThisTab": "Mindet kiválaszt", "SaveWithoutAnyPermissionsWarningMessage": "Biztos, hogy engedélyek nélkül akar menteni?", - "PermissionGroup": "Engedélycsoport" + "PermissionGroup": "Engedélycsoport", + "Filter": "Szűrő" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/is.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/is.json index 322e6b5580..7db32eb99e 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/is.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/is.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Veita allar heimildir", "SelectAllInThisTab": "Velja allt", "SaveWithoutAnyPermissionsWarningMessage": "Ertu viss um að þú viljir vista án nokkurra heimilda?", - "PermissionGroup": "Heimildahópur" + "PermissionGroup": "Heimildahópur", + "Filter": "Sía" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/it.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/it.json index 1312568b53..e473c7b310 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/it.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/it.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Concedi tutte le autorizzazioni", "SelectAllInThisTab": "Seleziona tutto", "SaveWithoutAnyPermissionsWarningMessage": "Sei sicuro di voler salvare senza alcuna autorizzazione?", - "PermissionGroup": "Gruppo di autorizzazioni" + "PermissionGroup": "Gruppo di autorizzazioni", + "Filter": "Filtro" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/nl.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/nl.json index 0dacfe8341..4b928c4d03 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/nl.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/nl.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Verleen alle rechten", "SelectAllInThisTab": "Selecteer alles", "SaveWithoutAnyPermissionsWarningMessage": "Weet u zeker dat u zonder rechten wilt opslaan?", - "PermissionGroup": "Rechtengroep" + "PermissionGroup": "Rechtengroep", + "Filter": "Filter" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/pl-PL.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/pl-PL.json index aa0bf32851..ee483651eb 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/pl-PL.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/pl-PL.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Udziel wszystkich uprawnień", "SelectAllInThisTab": "Zaznacz wszystkie", "SaveWithoutAnyPermissionsWarningMessage": "Czy na pewno chcesz zapisać bez żadnych uprawnień?", - "PermissionGroup": "Grupa uprawnień" + "PermissionGroup": "Grupa uprawnień", + "Filter": "Filtr" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/pt-BR.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/pt-BR.json index 14f45ec46a..28231b227f 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/pt-BR.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/pt-BR.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Conceder todas as permissões", "SelectAllInThisTab": "Selecionar todos", "SaveWithoutAnyPermissionsWarningMessage": "Tem certeza que deseja salvar sem nenhuma permissão?", - "PermissionGroup": "Grupo de permissão" + "PermissionGroup": "Grupo de permissão", + "Filter": "Filtrar" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/ro-RO.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/ro-RO.json index 6466540438..af7db26acb 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/ro-RO.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/ro-RO.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Acordă toate permisiunile", "SelectAllInThisTab": "Selectează toate", "SaveWithoutAnyPermissionsWarningMessage": "Sigur doriți să salvați fără nicio permisiune?", - "PermissionGroup": "Grup de permisiuni" + "PermissionGroup": "Grup de permisiuni", + "Filter": "Filtru" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/ru.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/ru.json index d46b4e289e..6041357b0e 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/ru.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/ru.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Предоставить все разрешения", "SelectAllInThisTab": "Выбрать все", "SaveWithoutAnyPermissionsWarningMessage": "Вы уверены, что хотите сохранить без каких-либо разрешений?", - "PermissionGroup": "Группа разрешений" + "PermissionGroup": "Группа разрешений", + "Filter": "Фильтр" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/sk.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/sk.json index 722582d28f..c079b8eba1 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/sk.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/sk.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Udeliť všetky oprávnenia", "SelectAllInThisTab": "Vybrať všetky", "SaveWithoutAnyPermissionsWarningMessage": "Naozaj chcete ukladať bez akýchkoľvek povolení?", - "PermissionGroup": "Skupina oprávnení" + "PermissionGroup": "Skupina oprávnení", + "Filter": "Filtrovať" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/sl.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/sl.json index 36640ffdbd..9c906f7387 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/sl.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/sl.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Dodeli vsa dovoljenja", "SelectAllInThisTab": "Izberi vse", "SaveWithoutAnyPermissionsWarningMessage": "Ali ste prepričani, da želite shraniti brez kakršnih koli dovoljenj?", - "PermissionGroup": "Skupina dovoljenj" + "PermissionGroup": "Skupina dovoljenj", + "Filter": "Filtriraj" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/sv.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/sv.json index 3671bf1422..d46fca4dc4 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/sv.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/sv.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Ge alla behörigheter", "SelectAllInThisTab": "Välj alla", "SaveWithoutAnyPermissionsWarningMessage": "Är du säker på att du vill spara utan några behörigheter?", - "PermissionGroup": "Behörighetsgrupp" + "PermissionGroup": "Behörighetsgrupp", + "Filter": "Filtrera" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/tr.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/tr.json index 3edfc2942f..960cd8cf02 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/tr.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/tr.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Tüm izinleri ver", "SelectAllInThisTab": "Hepsini seç", "SaveWithoutAnyPermissionsWarningMessage": "Hiçbir izin olmadan kaydetmek istediğinize emin misiniz?", - "PermissionGroup": "İzin Grubu" + "PermissionGroup": "İzin Grubu", + "Filter": "Filtre" } } diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/vi.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/vi.json index d927f6a4a9..5e9a9d8565 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/vi.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/vi.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "Cấp tất cả các quyền", "SelectAllInThisTab": "Chọn tất cả", "SaveWithoutAnyPermissionsWarningMessage": "Bạn có chắc chắn muốn lưu mà không có bất kỳ quyền nào không?", - "PermissionGroup": "Nhóm quyền" + "PermissionGroup": "Nhóm quyền", + "Filter": "Lọc" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/zh-Hans.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/zh-Hans.json index 3ae69c4d58..df844ca2bf 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/zh-Hans.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/zh-Hans.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "授予所有权限", "SelectAllInThisTab": "全选", "SaveWithoutAnyPermissionsWarningMessage": "您确定要在没有任何权限的情况下保存吗?", - "PermissionGroup": "权限组" + "PermissionGroup": "权限组", + "Filter": "过滤" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/zh-Hant.json b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/zh-Hant.json index b2d46a42a5..72af56c960 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/zh-Hant.json +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/zh-Hant.json @@ -7,6 +7,7 @@ "SelectAllInAllTabs": "授予所有權限", "SelectAllInThisTab": "全選", "SaveWithoutAnyPermissionsWarningMessage": "您確定要在沒有任何權限的情況下保存嗎?", - "PermissionGroup": "權限組" + "PermissionGroup": "權限組", + "Filter": "過濾" } } \ No newline at end of file diff --git a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml index 155eebfd5a..a9caf5c348 100644 --- a/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml +++ b/modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml @@ -23,7 +23,7 @@
- +
@@ -34,7 +34,7 @@
- Permission Group + @L["PermissionGroup"].Value 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 de0c75c2ef..9e27d31c9f 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,21 +5,20 @@ import * as ts from 'typescript'; import { allStyles, importMap, styleMap } from './style-map'; import { ChangeThemeOptions } from './model'; import { - Change, - createDefaultPath, - InsertChange, + addRootImport, + addRootProvider, + getAppModulePath, isLibrary, + isStandaloneApp, updateWorkspace, WorkspaceDefinition, + getAppConfigPath, + cleanEmptyExprFromModule, + cleanEmptyExprFromProviders, } from '../../utils'; import { ThemeOptionsEnum } from './theme-options.enum'; -import { - addImportToModule, - addProviderToModule, - findNodes, - getDecoratorMetadata, - getMetadataField, -} from '../../utils/angular/ast-utils'; +import { findNodes, getDecoratorMetadata, getMetadataField } from '../../utils/angular/ast-utils'; +import { getMainFilePath } from '../../utils/angular/standalone/util'; export default function (_options: ChangeThemeOptions): Rule { return async () => { @@ -68,46 +67,60 @@ function updateProjectStyle( function updateAppModule(selectedProject: string, targetThemeName: ThemeOptionsEnum): Rule { return async (host: Tree) => { - const appModulePath = (await createDefaultPath(host, selectedProject)) + '/app.module.ts'; + const mainFilePath = await getMainFilePath(host, selectedProject); + const isStandalone = isStandaloneApp(host, mainFilePath); + const appModulePath = isStandalone + ? getAppConfigPath(host, mainFilePath) + : getAppModulePath(host, mainFilePath); return chain([ removeImportPath(appModulePath, targetThemeName), - removeImportFromNgModuleMetadata(appModulePath, targetThemeName), - removeProviderFromNgModuleMetadata(appModulePath, targetThemeName), - insertImports(appModulePath, targetThemeName), - insertProviders(appModulePath, targetThemeName), + ...(!isStandalone ? [removeImportFromNgModuleMetadata(appModulePath, targetThemeName)] : []), + isStandalone + ? removeImportsFromStandaloneProviders(appModulePath, targetThemeName) + : removeProviderFromNgModuleMetadata(appModulePath, targetThemeName), + insertImports(selectedProject, targetThemeName), + insertProviders(selectedProject, targetThemeName), + adjustProvideAbpThemeShared(appModulePath, targetThemeName), + formatFile(appModulePath), + cleanEmptyExpressions(appModulePath, isStandalone), ]); }; } -export function removeImportPath(appModulePath: string, selectedTheme: ThemeOptionsEnum): Rule { +export function removeImportPath(filePath: string, selectedTheme: ThemeOptionsEnum): Rule { return (host: Tree) => { - const recorder = host.beginUpdate(appModulePath); - const source = createSourceFile(host, appModulePath); + const buffer = host.read(filePath); + if (!buffer) return host; + + const sourceText = buffer.toString('utf-8'); + const source = ts.createSourceFile(filePath, sourceText, ts.ScriptTarget.Latest, true); + const recorder = host.beginUpdate(filePath); + const impMap = getImportPaths(selectedTheme, true); const nodes = findNodes(source, ts.isImportDeclaration); - const filteredNodes = nodes.filter(node => - impMap.some(({ path, importName }) => { - const sourceModule = node.getFullText(); - const moduleName = importName.split('.')[0]; + const filteredNodes = nodes.filter(node => { + const importPath = (node.moduleSpecifier as ts.StringLiteral).text; + const namedBindings = node.importClause?.namedBindings; - if (path && sourceModule.match(path)) { - return true; - } + return impMap.some(({ path, importName }) => { + const symbol = importName.split('.')[0]; + const matchesPath = !!path && importPath === path; - return !!(moduleName && sourceModule.match(moduleName)); - }), - ); + const matchesSymbol = + !!namedBindings && + ts.isNamedImports(namedBindings) && + namedBindings.elements.some(e => e.name.text === symbol); - if (filteredNodes?.length < 1) { - return; - } + return matchesPath || matchesSymbol; + }); + }); - filteredNodes.map(importPath => - recorder.remove(importPath.getStart(), importPath.getWidth() + 1), - ); + for (const node of filteredNodes) { + recorder.remove(node.getStart(), node.getWidth()); + } host.commitUpdate(recorder); return host; @@ -153,100 +166,188 @@ export function removeImportFromNgModuleMetadata( }; } -export function removeProviderFromNgModuleMetadata( - appModulePath: string, +export function removeImportsFromStandaloneProviders( + mainPath: string, selectedTheme: ThemeOptionsEnum, ): Rule { return (host: Tree) => { - const recorder = host.beginUpdate(appModulePath); - const source = createSourceFile(host, appModulePath); + const buffer = host.read(mainPath); + if (!buffer) return host; + + const sourceText = buffer.toString('utf-8'); + const source = ts.createSourceFile(mainPath, sourceText, ts.ScriptTarget.Latest, true); + const recorder = host.beginUpdate(mainPath); + const impMap = getImportPaths(selectedTheme, true); + const callExpressions = findNodes(source, ts.isCallExpression); - const node = getDecoratorMetadata(source, 'NgModule', '@angular/core')[0] || {}; - if (!node) { - throw new SchematicsException('The app module does not found'); - } + for (const expr of callExpressions) { + const exprText = expr.getText(); - const matchingProperties = getMetadataField(node as ts.ObjectLiteralExpression, 'providers'); - const assignment = matchingProperties[0] as ts.PropertyAssignment; - const assignmentInit = assignment.initializer as ts.ArrayLiteralExpression; + if (expr.expression.getText() === 'importProvidersFrom') { + const args = expr.arguments; - const elements = assignmentInit.elements; - if (!elements || elements.length < 1) { - throw new SchematicsException(`Elements could not found: ${elements}`); - } + let modules: readonly ts.Expression[] = []; - const filteredElements = elements.filter(f => - impMap.filter(f => !!f.provider).some(s => f.getText().match(s.provider!)), - ); + if (args.length === 1 && ts.isArrayLiteralExpression(args[0])) { + modules = (args[0] as ts.ArrayLiteralExpression).elements; + } else { + modules = args; + } - if (!filteredElements || filteredElements.length < 1) { - return; + const elementsToRemove = modules.filter(el => + impMap.some(({ importName }) => el.getText().includes(importName)), + ); + + if (elementsToRemove.length) { + for (const removeEl of elementsToRemove) { + const start = removeEl.getFullStart(); + const end = removeEl.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); + } + } + } + + const remaining = modules.filter(el => !elementsToRemove.includes(el)); + if (remaining.length === 0) { + 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); + } + } + } else { + const match = impMap.find(({ importName, provider }) => { + const moduleSymbol = importName?.split('.')[0]; + return ( + (moduleSymbol && exprText.includes(moduleSymbol)) || + (provider && exprText.includes(provider)) + ); + }); + + if (match) { + const start = expr.getFullStart(); + const end = expr.getEnd(); + const nextChar = sourceText.slice(end, end + 1); + const prevChar = sourceText.slice(start - 1, start); + + if (nextChar === ',') { + recorder.remove(start, end - start + 1); + } else if (prevChar === ',') { + recorder.remove(start - 1, end - start + 1); + } else { + recorder.remove(start, end - start); + } + } + } } - filteredElements.map(willRemoveModule => { - recorder.remove(willRemoveModule.getStart(), willRemoveModule.getWidth()); - }); host.commitUpdate(recorder); return host; }; } -export function insertImports(appModulePath: string, selectedTheme: ThemeOptionsEnum): Rule { +export function removeProviderFromNgModuleMetadata( + appModulePath: string, + selectedTheme: ThemeOptionsEnum, +): Rule { return (host: Tree) => { const recorder = host.beginUpdate(appModulePath); const source = createSourceFile(host, appModulePath); - const selected = importMap.get(selectedTheme); + const impMap = getImportPaths(selectedTheme, true); - if (!selected) { - return host; + const node = getDecoratorMetadata(source, 'NgModule', '@angular/core')[0]; + if (!node) { + throw new SchematicsException('The app module does not found'); } - const changes: Change[] = []; + const providersProperty = getMetadataField( + node as ts.ObjectLiteralExpression, + 'providers', + )[0] as ts.PropertyAssignment; - selected.map(({ importName, path }) => - changes.push(...addImportToModule(source, appModulePath, importName, path)), - ); + const providersArray = providersProperty.initializer as ts.ArrayLiteralExpression; + if (!providersArray.elements.length) return host; - if (changes.length > 0) { - for (const change of changes) { - if (change instanceof InsertChange) { - recorder.insertLeft(change.order, change.toAdd); + for (const element of providersArray.elements) { + const elementText = element.getText(); + + const match = impMap.find(({ provider }) => { + if (!provider) return false; + const providerName = provider.replace(/\(\s*\)$/, '').trim(); + return provider && elementText.includes(providerName); + }); + + if (match) { + const start = element.getFullStart(); + const end = element.getEnd(); + + const nextChar = source.text.slice(end, end + 1); + const prevChar = source.text.slice(start - 1, start); + + if (nextChar === ',') { + recorder.remove(start, end - start + 1); + } else if (prevChar === ',') { + recorder.remove(start - 1, end - start + 1); + } else { + recorder.remove(start, end - start); } } } + host.commitUpdate(recorder); return host; }; } -export function insertProviders(appModulePath: string, selectedTheme: ThemeOptionsEnum): Rule { - return (host: Tree) => { - const recorder = host.beginUpdate(appModulePath); - const source = createSourceFile(host, appModulePath); +export function insertImports(projectName: string, selectedTheme: ThemeOptionsEnum): Rule { + return addRootImport(projectName, code => { const selected = importMap.get(selectedTheme); + if (!selected?.length) return code.code``; - if (!selected) { - return host; - } - - const changes: Change[] = []; + const expressions: string[] = []; - selected.map(({ path, provider }) => { - if (provider) { - changes.push(...addProviderToModule(source, appModulePath, provider + '()', path)); + for (const { importName, path, expression } of selected) { + if (importName && path) { + code.external(importName, path); } - }); - - for (const change of changes) { - if (change instanceof InsertChange) { - recorder.insertLeft(change.order, change.toAdd); + if (expression) { + expressions.push(expression.trim()); } } + return code.code`${expressions}`; + }); +} +export function insertProviders(projectName: string, selectedTheme: ThemeOptionsEnum): Rule { + return addRootProvider(projectName, code => { + const selected = importMap.get(selectedTheme); + if (!selected || selected.length === 0) return code.code``; - host.commitUpdate(recorder); - return host; - }; + const providers = selected + .filter(s => !!s.provider) + .map(({ provider, path, importName }) => { + code.external(importName, path); + return `${provider}`; + }); + + return code.code`${providers}`; + }); } export function createSourceFile(host: Tree, appModulePath: string): ts.SourceFile { @@ -271,7 +372,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], []); } @@ -316,3 +417,120 @@ export const styleCompareFn = (item1: string | object, item2: string | object) = return o1.bundleName && o2.bundleName && o1.bundleName == o2.bundleName; }; + +export const formatFile = (filePath: string): Rule => { + return (tree: Tree) => { + const buffer = tree.read(filePath); + if (!buffer) return tree; + + const source = ts.createSourceFile(filePath, buffer.toString(), ts.ScriptTarget.Latest, true); + const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); + const formatted = printer.printFile(source); + + tree.overwrite(filePath, formatted); + return tree; + }; +}; + +export function cleanEmptyExpressions(modulePath: string, isStandalone: boolean): Rule { + return (host: Tree) => { + const buffer = host.read(modulePath); + if (!buffer) throw new SchematicsException(`Cannot read ${modulePath}`); + + const source = ts.createSourceFile( + modulePath, + buffer.toString('utf-8'), + ts.ScriptTarget.Latest, + true, + ); + const recorder = host.beginUpdate(modulePath); + + if (isStandalone) { + cleanEmptyExprFromProviders(source, recorder); + } else { + cleanEmptyExprFromModule(source, recorder); + } + host.commitUpdate(recorder); + return host; + }; +} + +export function adjustProvideAbpThemeShared( + appModulePath: string, + selectedTheme: ThemeOptionsEnum, +): Rule { + return (host: Tree) => { + const source = createSourceFile(host, appModulePath); + const recorder = host.beginUpdate(appModulePath); + const sourceText = source.getText(); + + const callExpressions = findProvideAbpThemeSharedCalls(source); + + for (const expr of callExpressions) { + const exprStart = expr.getStart(); + const exprEnd = expr.getEnd(); + const originalText = sourceText.substring(exprStart, exprEnd); + + let newText = originalText; + + const hasHttpErrorConfig = originalText.includes('withHttpErrorConfig'); + const hasValidationBluePrint = originalText.includes('withValidationBluePrint'); + + if (selectedTheme === ThemeOptionsEnum.LeptonX) { + if (!hasHttpErrorConfig) { + newText = newText.replace( + '(', + `( + withHttpErrorConfig({ + errorScreen: { + component: HttpErrorComponent, + forWhichErrors: [401, 403, 404, 500], + hideCloseIcon: true + } + }),`, + ); + } + } else { + if (hasHttpErrorConfig) { + newText = newText.replace(/withHttpErrorConfig\([^)]*\),?/, ''); + } + } + + if (!hasValidationBluePrint) { + newText = newText.replace( + '(', + `( + withValidationBluePrint({ + wrongPassword: 'Please choose 1q2w3E*' + }),`, + ); + } + + if (newText && newText !== originalText) { + recorder.remove(exprStart, exprEnd - exprStart); + recorder.insertLeft(exprStart, newText); + } + } + + host.commitUpdate(recorder); + return host; + }; +} + +function findProvideAbpThemeSharedCalls(source: ts.SourceFile): ts.CallExpression[] { + const result: ts.CallExpression[] = []; + + const visit = (node: ts.Node) => { + if (ts.isCallExpression(node)) { + const expressionText = node.expression.getText(); + if (expressionText.includes('provideAbpThemeShared')) { + result.push(node); + } + } + ts.forEachChild(node, visit); + }; + + visit(source); + + return result; +} 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 7dc9bf8053..8cae3b4641 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(); @@ -260,7 +261,7 @@ styleMap.set(ThemeOptionsEnum.LeptonXLite, [ bundleName: 'bootstrap-icons', }, ]); -// the code written by Github co-pilot. thank go-pilot. You are the best sidekick. + export const allStyles = Array.from(styleMap.values()).reduce((acc, val) => [...acc, ...val], []); export const importMap = new Map(); @@ -269,40 +270,122 @@ importMap.set(ThemeOptionsEnum.Basic, [ { path: '@abp/ng.theme.basic', importName: 'ThemeBasicModule', - provider: 'provideThemeBasicConfig', + expression: 'ThemeBasicModule', + }, + { + path: '@abp/ng.theme.basic', + importName: 'provideThemeBasicConfig', + provider: 'provideThemeBasicConfig()', + }, + { + path: '@abp/ng.theme.shared', + importName: 'ThemeSharedModule', + expression: 'ThemeSharedModule', + }, + { + path: '@abp/ng.theme.shared', + importName: 'withValidationBluePrint', + }, + { + path: '@abp/ng.theme.shared', + importName: 'provideAbpThemeShared', + provider: 'provideAbpThemeShared()', }, ]); importMap.set(ThemeOptionsEnum.Lepton, [ { path: '@volo/abp.ng.theme.lepton', - importName: 'ThemeLeptonModule', - provider: 'provideThemeLepton', + importName: 'provideThemeLepton', + provider: 'provideThemeLepton()', + }, + { + path: '@abp/ng.theme.shared', + importName: 'ThemeSharedModule', + expression: 'ThemeSharedModule', + }, + { + path: '@abp/ng.theme.shared', + importName: 'withHttpErrorConfig', + }, + { + path: '@abp/ng.theme.shared', + importName: 'withValidationBluePrint', + }, + { + path: '@abp/ng.theme.shared', + importName: 'provideAbpThemeShared', + provider: 'provideAbpThemeShared()', }, ]); 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()', + }, + { + path: '@abp/ng.theme.shared', + importName: 'ThemeSharedModule', + expression: 'ThemeSharedModule', + }, + { + path: '@abp/ng.theme.shared', + importName: 'withHttpErrorConfig', + }, + { + path: '@abp/ng.theme.shared', + importName: 'withValidationBluePrint', + }, + { + path: '@abp/ng.theme.shared', + importName: 'provideAbpThemeShared', + provider: 'provideAbpThemeShared()', }, ]); 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()', + }, + { + path: '@abp/ng.theme.shared', + importName: 'ThemeSharedModule', + expression: 'ThemeSharedModule', + }, + { + path: '@volosoft/abp.ng.theme.lepton-x', + importName: 'HttpErrorComponent', + }, + { + path: '@abp/ng.theme.shared', + importName: 'withHttpErrorConfig', + }, + { + path: '@abp/ng.theme.shared', + importName: 'withValidationBluePrint', + }, + { + path: '@abp/ng.theme.shared', + importName: 'provideAbpThemeShared', + provider: 'provideAbpThemeShared()', }, ]); 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 0000000000..de84cad019 --- /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 0000000000..6cf8e96f64 --- /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 0000000000..3bda94b078 --- /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 0000000000..3bbb75be52 --- /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 0000000000..fe08efba8c --- /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 0000000000..c890db7305 --- /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,30 @@ +import { eLayoutType, RoutesService } from '@abp/ng.core'; +import { e<%= pascal(libraryName) %>RouteNames } from '../enums/route-names'; +import { makeEnvironmentProviders, provideAppInitializer, inject, EnvironmentProviders } from '@angular/core'; + +export const <%= macro(libraryName) %>_ROUTE_PROVIDERS = [ + provideAppInitializer(() => { + configureRoutes(); + }), +]; + +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 const <%= macro(libraryName) %>_PROVIDERS: EnvironmentProviders[] = [ + ...<%= macro(libraryName) %>_ROUTE_PROVIDERS, +]; + +export function provide<%= pascal(libraryName) %>Config() { + return makeEnvironmentProviders(<%= macro(libraryName) %>_PROVIDERS); +} 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 0000000000..0003cebbef --- /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 0000000000..e181d4088d --- /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 0000000000..fd8e55d798 --- /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 0000000000..c4d36a8576 --- /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__.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 0000000000..3c8ada66b8 --- /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) %>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 new file mode 100644 index 0000000000..975fdaa261 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__.routes.ts.template @@ -0,0 +1,9 @@ +import { Routes } from '@angular/router'; + +export const <%= macro(libraryName) %>_ROUTES: Routes = [ + { + path: '', + loadComponent: () => + import('./<%= kebab(libraryName) %>.component').then(m => m.<%= pascal(libraryName) %>Component), + }, +]; 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 0000000000..b954dd6be0 --- /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) %>.routes'; 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 0000000000..5fa8ee704c --- /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 0000000000..52e55168eb --- /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 0000000000..5b574d313d --- /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 0000000000..06de549e10 --- /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 0000000000..715dd0a5d2 --- /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 0000000000..e09fb3fd03 --- /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 0000000000..a6b4121de5 --- /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 0000000000..b8bd795229 --- /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 0000000000..11aece60c4 --- /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 f353845880..49645c3e45 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,24 @@ import * as ts from 'typescript'; import { join, normalize } from '@angular-devkit/core'; import { - addImportToModule, + addRootImport, + addRootProvider, addRouteDeclarationToModule, applyWithOverwrite, - camel, + findAppRoutesModulePath, + findAppRoutesPath, getFirstApplication, getWorkspace, + hasImportInNgModule, + hasProviderInStandaloneAppConfig, InsertChange, interpolate, isLibrary, + isStandaloneApp, JSONFile, kebab, + macro, pascal, - readWorkspaceSchema, resolveProject, updateWorkspace, } from '../../utils'; @@ -32,7 +37,8 @@ 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) { return async (tree: Tree) => { @@ -67,11 +73,17 @@ function createLibrary(options: GenerateLibSchema): Rule { return async (tree: Tree) => { const target = await resolveProject(tree, options.packageName, null); if (!target || options.override) { - if (options.isModuleTemplate) { - return createLibFromModuleTemplate(tree, options); - } if (options.isSecondaryEntrypoint) { - return createLibSecondaryEntry(tree, options); + if (options.templateType === GenerateLibTemplateType.Standalone) { + return createLibSecondaryEntryWithStandaloneTemplate(tree, options); + } else { + return createLibSecondaryEntry(tree, options); + } + } + if (options.templateType === GenerateLibTemplateType.Module) { + return createLibFromModuleTemplate(tree, options); + } else { + return createLibFromModuleStandaloneTemplate(tree, options); } } else { throw new SchematicsException( @@ -109,14 +121,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 +160,8 @@ export function addLibToWorkspaceIfNotExist(name: string, packagesDir: string): : noop(), addLibToWorkspaceFile(projectRoot, packageName), updateTsConfig(packageName, pathImportLib), - importConfigModuleToDefaultProjectAppModule(workspace, packageName), - addRoutingToAppRoutingModule(workspace, packageName), + importConfigModuleToDefaultProjectAppModule(packageName, options), + addRoutingToAppRoutingModule(workspace, packageName, options), ]); }; } @@ -169,84 +199,211 @@ 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!; - const project = workspace.projects.get(projectName); - const appModulePath = `${project?.sourceRoot}/app/app.module.ts`; - const appModule = tree.read(appModulePath); - if (!appModule) { - return; - } - const appModuleContent = appModule.toString(); - if (appModuleContent.includes(`${camel(packageName)}ConfigModule`)) { - return; - } + return async (tree: Tree) => { + const projectName = getFirstApplication(tree).name!; + const mainFilePath = await getMainFilePath(tree, projectName); + const isSourceStandalone = isStandaloneApp(tree, mainFilePath); + const rules: Rule[] = []; - const forRootStatement = `${pascal(packageName)}ConfigModule.forRoot()`; - const text = tree.read(appModulePath); - if (!text) { + const providerAlreadyExists = isSourceStandalone + ? await hasProviderInStandaloneAppConfig( + tree, + projectName, + `provide${pascal(packageName)}Config`, + ) + : await hasImportInNgModule( + tree, + projectName, + options.templateType === GenerateLibTemplateType.Standalone + ? `provide${pascal(packageName)}Config` + : `${pascal(packageName)}ConfigModule`, + options.templateType === GenerateLibTemplateType.Standalone ? 'providers' : 'imports', + ); + if (providerAlreadyExists) { return; } - const sourceText = text.toString(); - if (sourceText.includes(forRootStatement)) { - return; + + if (options.templateType === GenerateLibTemplateType.Standalone) { + 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}.forRoot()`; + }), + ); } - const source = ts.createSourceFile(appModulePath, sourceText, ts.ScriptTarget.Latest, true); - - const changes = addImportToModule( - source, - appModulePath, - forRootStatement, - `${kebab(packageName)}/config`, - ); - const recorder = tree.beginUpdate(appModulePath); - for (const change of changes) { + return chain(rules); + }; +} + +export function addRoutingToAppRoutingModule( + workspace: WorkspaceDefinition, + packageName: string, + options: GenerateLibSchema, +): Rule { + return async (tree: Tree) => { + const projectName = getFirstApplication(tree).name!; + const project = workspace.projects.get(projectName); + const mainFilePath = await getMainFilePath(tree, projectName); + const isSourceStandalone = isStandaloneApp(tree, mainFilePath); + + const pascalName = pascal(packageName); + const macroName = macro(packageName); + const routePath = `${kebab(packageName)}`; + const moduleName = `${pascalName}Module`; + + if (isSourceStandalone) { + 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}`); + } + + const content = buffer.toString(); + const source = ts.createSourceFile(appRoutesPath, content, ts.ScriptTarget.Latest, true); + const routeExpr = + options.templateType === GenerateLibTemplateType.Standalone + ? `() => import('${routePath}').then(m => m.${macroName}_ROUTES)` + : `() => import('${routePath}').then(m => m.${moduleName}.forLazy())`; + const routeToAdd = `{ path: '${routePath}', loadChildren: ${routeExpr} }`; + const change = addRouteToRoutesArray(source, 'APP_ROUTES', routeToAdd); + if (change instanceof InsertChange) { + const recorder = tree.beginUpdate(appRoutesPath); recorder.insertLeft(change.pos, change.toAdd); + tree.commitUpdate(recorder); } - } - tree.commitUpdate(recorder); + } else { + const appRoutingModulePath = await findAppRoutesModulePath(tree, mainFilePath); + + if (!appRoutingModulePath) { + throw new SchematicsException(`Cannot find routing module: ${appRoutingModulePath}`); + } + + const appRoutingModule = tree.read(appRoutingModulePath); + if (!appRoutingModule) { + return; + } + const appRoutingModuleContent = appRoutingModule.toString(); + const routeExpr = + options.templateType === GenerateLibTemplateType.Standalone + ? `${macroName}_ROUTES` + : moduleName; + if (appRoutingModuleContent.includes(routeExpr)) { + return; + } + + const source = ts.createSourceFile( + appRoutingModulePath, + appRoutingModuleContent, + ts.ScriptTarget.Latest, + true, + ); + const importStatement = + options.templateType === GenerateLibTemplateType.Standalone + ? `() => 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); + const recorder = tree.beginUpdate(appRoutingModulePath); + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } + tree.commitUpdate(recorder); + } return; }; } -export function addRoutingToAppRoutingModule(workspace: WorkspaceDefinition, packageName: string) { - return (tree: Tree) => { - const projectName = readWorkspaceSchema(tree).defaultProject || getFirstApplication(tree).name!; - const project = workspace.projects.get(projectName); - 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`; - if (appRoutingModuleContent.includes(moduleName)) { - 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.name.getText() === arrayName.toUpperCase()) && + decl.initializer !== undefined && + ts.isArrayLiteralExpression(decl.initializer), + ), + ); - const source = ts.createSourceFile( - appRoutingModulePath, - appRoutingModuleContent, - ts.ScriptTarget.Latest, - true, - ); - const importPath = `${kebab(packageName)}`; - const importStatement = `() => import('${importPath}').then(m => m.${moduleName}.forLazy())`; - const routeDefinition = `{ path: '${kebab(packageName)}', loadChildren: ${importStatement} }`; - const change = addRouteDeclarationToModule(source, `${kebab(packageName)}`, routeDefinition); - - const recorder = tree.beginUpdate(appRoutingModulePath); - if (change instanceof InsertChange) { - recorder.insertLeft(change.pos, change.toAdd); - } - tree.commitUpdate(recorder); + if (!routesVar || !ts.isVariableStatement(routesVar)) { + throw new Error(`Could not find routes array named "${arrayName}".`); + } - return; + 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 hasTrailingComma = arrayLiteral.elements.hasTrailingComma ?? false; + const insertPos = + hasTrailingComma || arrayLiteral.elements.length === 0 + ? arrayLiteral.getEnd() - 1 + : arrayLiteral.elements[arrayLiteral.elements.length - 1].getEnd(); + + const prefix = arrayLiteral.elements.length > 0 && !hasTrailingComma ? ',\n ' : ' '; + const toAdd = `${prefix}${routeToAdd}`; + + return new InsertChange(source.fileName, insertPos, toAdd); } 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 f3c6e5b97f..aca9f08e49 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 @@ -8,8 +13,10 @@ export interface GenerateLibSchema { * İs the package a library or a library module */ isSecondaryEntrypoint: boolean; - - isModuleTemplate: boolean; + /** + * İs the package has standalone template + */ + templateType: GenerateLibTemplateType; 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 19df1a5427..df9f64a470 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,21 +22,25 @@ }, "x-prompt": "Is secondary entrypoint?" }, - "isModuleTemplate": { - "description": "Is module template", - "type": "boolean", - "$default": { - "$source": "argv", - "index": 2 - }, - "x-prompt": "Is module 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", "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/add-declaration-to-ng-module.ts b/npm/ng-packs/packages/schematics/src/utils/angular/add-declaration-to-ng-module.ts index f62e3922fd..6e5dc266b7 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 d435b90c85..2683e20149 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), ]; } @@ -572,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; } @@ -678,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 08df56a6dd..62e3ab659a 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 c9aa617191..06c4f38653 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 3522b59042..2f3501c33a 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/eol.ts b/npm/ng-packs/packages/schematics/src/utils/angular/eol.ts new file mode 100644 index 0000000000..5fdae3f575 --- /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/find-module.ts b/npm/ng-packs/packages/schematics/src/utils/angular/find-module.ts index 112269d5e5..085592abaa 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 169c6c389a..946c4119d3 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 abf2992d90..2c1bfd086e 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 6bb532416f..26a06e16a6 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 75962f31d7..b4eb1fd056 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 01227198c9..37a019e940 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 dffa9e841d..2842018802 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 7f4b7ba8c6..8897a3ddab 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/standalone/app_component.ts b/npm/ng-packs/packages/schematics/src/utils/angular/standalone/app_component.ts new file mode 100644 index 0000000000..f617e0baaa --- /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 0000000000..817f7b680a --- /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 0000000000..a572f3b6c9 --- /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 0000000000..b522156c49 --- /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 0000000000..79bc5ea8e4 --- /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 0000000000..0bb1419a63 --- /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; +} 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 619fe8e924..8b380d1b82 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 fb7f18c3f7..34c329b470 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 410c079e2f..b831458edf 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/ast.ts b/npm/ng-packs/packages/schematics/src/utils/ast.ts index a1ffe5a319..0b720e63cc 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)); +} diff --git a/npm/ng-packs/packages/schematics/src/utils/index.ts b/npm/ng-packs/packages/schematics/src/utils/index.ts index eeb12e6d9e..06b96892fc 100644 --- a/npm/ng-packs/packages/schematics/src/utils/index.ts +++ b/npm/ng-packs/packages/schematics/src/utils/index.ts @@ -18,3 +18,5 @@ export * from './text'; 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 0000000000..ca2d327eb6 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/utils/ng-module.ts @@ -0,0 +1,159 @@ +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'; + +/** + * 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, + 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); + + // Get the NgModule decorator metadata + const ngModuleDecorator = getDecoratorMetadata(source, 'NgModule', '@angular/core')[0]; + + 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)); +}; + +/** + * 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, +): 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; +} + +/** + * 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; + + 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 new file mode 100644 index 0000000000..60364eb976 --- /dev/null +++ b/npm/ng-packs/packages/schematics/src/utils/standalone.ts @@ -0,0 +1,180 @@ +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'; + +/** + * 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; + + 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; +} + +/** + * 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, + 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)); +}; + +/** + * 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(); + + 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), + ); + } + } +} diff --git a/npm/ng-packs/packages/schematics/src/utils/workspace.ts b/npm/ng-packs/packages/schematics/src/utils/workspace.ts index ed53e482e8..8105a5e1e4 100644 --- a/npm/ng-packs/packages/schematics/src/utils/workspace.ts +++ b/npm/ng-packs/packages/schematics/src/utils/workspace.ts @@ -52,7 +52,7 @@ 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; diff --git a/npm/ng-packs/scripts/build-schematics.ts b/npm/ng-packs/scripts/build-schematics.ts index 6690b272b3..0699eb0567 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' }, diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyCompanyName.MyProjectName.Blazor.Server.Mongo.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyCompanyName.MyProjectName.Blazor.Server.Mongo.csproj index 08c06f0ed7..ee35a6e0e0 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyCompanyName.MyProjectName.Blazor.Server.Mongo.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server.Mongo/MyCompanyName.MyProjectName.Blazor.Server.Mongo.csproj @@ -8,10 +8,10 @@ - - - - + + + + @@ -82,7 +82,7 @@ - + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj index ca6ca52f05..a76d39682f 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj @@ -8,10 +8,10 @@ - - - - + + + + @@ -83,11 +83,11 @@ - + - + runtime; build; native; contentfiles; analyzers compile; contentFiles; build; buildMultitargeting; buildTransitive; analyzers; native diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyCompanyName.MyProjectName.Blazor.WebAssembly.Client.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyCompanyName.MyProjectName.Blazor.WebAssembly.Client.csproj index 5f9478ea96..3ecc615439 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyCompanyName.MyProjectName.Blazor.WebAssembly.Client.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Client/MyCompanyName.MyProjectName.Blazor.WebAssembly.Client.csproj @@ -9,10 +9,10 @@ - - - - + + + + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.Mongo.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.Mongo.csproj index c42c2e72f1..47ac168149 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.Mongo.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server.Mongo/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.Mongo.csproj @@ -8,12 +8,12 @@ - + - - + + @@ -79,7 +79,7 @@ - + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.csproj index 8d3698cb1b..065ffb9552 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Server/MyCompanyName.MyProjectName.Blazor.WebAssembly.Server.csproj @@ -8,12 +8,12 @@ - + - - + + @@ -80,11 +80,11 @@ - + - + runtime; build; native; contentfiles; analyzers compile; contentFiles; build; buildMultitargeting; buildTransitive; analyzers; native diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Shared/MyCompanyName.MyProjectName.Blazor.WebAssembly.Shared.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Shared/MyCompanyName.MyProjectName.Blazor.WebAssembly.Shared.csproj index d086ca8003..fa43424fa6 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Shared/MyCompanyName.MyProjectName.Blazor.WebAssembly.Shared.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Blazor.WebAssembly/Shared/MyCompanyName.MyProjectName.Blazor.WebAssembly.Shared.csproj @@ -29,7 +29,7 @@ - + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/MyCompanyName.MyProjectName.Host.Mongo.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/MyCompanyName.MyProjectName.Host.Mongo.csproj index a1436f339f..1e5069d33c 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/MyCompanyName.MyProjectName.Host.Mongo.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host.Mongo/MyCompanyName.MyProjectName.Host.Mongo.csproj @@ -8,8 +8,8 @@ - - + + @@ -74,7 +74,7 @@ - + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/MyCompanyName.MyProjectName.Host.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/MyCompanyName.MyProjectName.Host.csproj index d5843b5373..0431d2befa 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/MyCompanyName.MyProjectName.Host.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Host/MyCompanyName.MyProjectName.Host.csproj @@ -8,8 +8,8 @@ - - + + @@ -75,11 +75,11 @@ - + - + runtime; build; native; contentfiles; analyzers compile; contentFiles; build; buildMultitargeting; buildTransitive; analyzers; native diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/MyCompanyName.MyProjectName.Mvc.Mongo.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/MyCompanyName.MyProjectName.Mvc.Mongo.csproj index f7de34413f..f4c32ff0c8 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/MyCompanyName.MyProjectName.Mvc.Mongo.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc.Mongo/MyCompanyName.MyProjectName.Mvc.Mongo.csproj @@ -8,8 +8,8 @@ - - + + @@ -77,7 +77,7 @@ - + diff --git a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/MyCompanyName.MyProjectName.Mvc.csproj b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/MyCompanyName.MyProjectName.Mvc.csproj index ccbc44f2b8..16e8d6ba9f 100644 --- a/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/MyCompanyName.MyProjectName.Mvc.csproj +++ b/templates/app-nolayers/aspnet-core/MyCompanyName.MyProjectName.Mvc/MyCompanyName.MyProjectName.Mvc.csproj @@ -8,8 +8,8 @@ - - + + @@ -78,11 +78,11 @@ - + - + runtime; build; native; contentfiles; analyzers compile; contentFiles; build; buildMultitargeting; buildTransitive; analyzers; native diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj index b95c06305c..2d2e9e301d 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj @@ -39,9 +39,9 @@ - - - + + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyCompanyName.MyProjectName.Blazor.Client.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyCompanyName.MyProjectName.Blazor.Client.csproj index e768be8ab5..1ba78a108b 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyCompanyName.MyProjectName.Blazor.Client.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Client/MyCompanyName.MyProjectName.Blazor.Client.csproj @@ -12,10 +12,10 @@ - - - - + + + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyCompanyName.MyProjectName.Blazor.Server.Tiered.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyCompanyName.MyProjectName.Blazor.Server.Tiered.csproj index 236ec617c2..a5952f1911 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyCompanyName.MyProjectName.Blazor.Server.Tiered.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server.Tiered/MyCompanyName.MyProjectName.Blazor.Server.Tiered.csproj @@ -14,11 +14,11 @@ - - - - - + + + + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj index 91c81d9e90..00b35d31c8 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.Server/MyCompanyName.MyProjectName.Blazor.Server.csproj @@ -14,11 +14,11 @@ - - - - - + + + + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Client.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Client.csproj index 86e8f08f28..24f55913bc 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Client.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Client.csproj @@ -13,10 +13,10 @@ - - - - + + + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client.csproj index 379c36e3cb..8d591b19ec 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.Client.csproj @@ -13,10 +13,10 @@ - - - - + + + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.csproj index a0d4ffdb4a..b5359b0e17 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered/MyCompanyName.MyProjectName.Blazor.WebApp.Tiered.csproj @@ -15,12 +15,12 @@ - - - - - - + + + + + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyCompanyName.MyProjectName.Blazor.WebApp.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyCompanyName.MyProjectName.Blazor.WebApp.csproj index 6a7ec893c9..20e3591801 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyCompanyName.MyProjectName.Blazor.WebApp.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor.WebApp/MyCompanyName.MyProjectName.Blazor.WebApp.csproj @@ -15,11 +15,11 @@ - - - - - + + + + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyCompanyName.MyProjectName.Blazor.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyCompanyName.MyProjectName.Blazor.csproj index 156e1147b7..9ea9c32df8 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyCompanyName.MyProjectName.Blazor.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Blazor/MyCompanyName.MyProjectName.Blazor.csproj @@ -8,12 +8,12 @@ - - + + - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.DbMigrator/MyCompanyName.MyProjectName.DbMigrator.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.DbMigrator/MyCompanyName.MyProjectName.DbMigrator.csproj index 7fbbb92fa8..b68c8562f0 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.DbMigrator/MyCompanyName.MyProjectName.DbMigrator.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.DbMigrator/MyCompanyName.MyProjectName.DbMigrator.csproj @@ -18,11 +18,11 @@ - - - + + + - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj index 7f6b9936fa..6a84c34249 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj @@ -26,7 +26,7 @@ - + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/MyCompanyName.MyProjectName.EntityFrameworkCore.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/MyCompanyName.MyProjectName.EntityFrameworkCore.csproj index 3c77133bb4..d03b199ebd 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/MyCompanyName.MyProjectName.EntityFrameworkCore.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.EntityFrameworkCore/MyCompanyName.MyProjectName.EntityFrameworkCore.csproj @@ -22,7 +22,7 @@ - + all runtime; build; native; contentfiles; analyzers diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj index 6928fab989..7fdb28b27f 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj @@ -11,10 +11,10 @@ - - - - + + + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/MyCompanyName.MyProjectName.HttpApi.HostWithIds.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/MyCompanyName.MyProjectName.HttpApi.HostWithIds.csproj index 05ef26fb35..f1f76659b6 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/MyCompanyName.MyProjectName.HttpApi.HostWithIds.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.HttpApi.HostWithIds/MyCompanyName.MyProjectName.HttpApi.HostWithIds.csproj @@ -11,8 +11,8 @@ - - + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj index ea2058353f..f20ee25c62 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj @@ -17,9 +17,9 @@ - - - + + + diff --git a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj index 2e932fad4c..b97fd05460 100644 --- a/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj +++ b/templates/app/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj @@ -39,8 +39,8 @@ - - + + diff --git a/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj b/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj index 8cd45f7b82..d6f2e5f70e 100644 --- a/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj +++ b/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj @@ -22,8 +22,8 @@ - - + + diff --git a/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.MongoDB.Tests/MyCompanyName.MyProjectName.MongoDB.Tests.csproj b/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.MongoDB.Tests/MyCompanyName.MyProjectName.MongoDB.Tests.csproj index 79c080216c..bff0099e07 100644 --- a/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.MongoDB.Tests/MyCompanyName.MyProjectName.MongoDB.Tests.csproj +++ b/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.MongoDB.Tests/MyCompanyName.MyProjectName.MongoDB.Tests.csproj @@ -15,10 +15,10 @@ - - + + - + diff --git a/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.TestBase/MyCompanyName.MyProjectName.TestBase.csproj b/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.TestBase/MyCompanyName.MyProjectName.TestBase.csproj index 426dcaca1c..791033b241 100644 --- a/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.TestBase/MyCompanyName.MyProjectName.TestBase.csproj +++ b/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.TestBase/MyCompanyName.MyProjectName.TestBase.csproj @@ -17,15 +17,15 @@ - - + + all runtime; build; native; contentfiles; analyzers - - - - + + + + diff --git a/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.Web.Tests/MyCompanyName.MyProjectName.Web.Tests.csproj b/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.Web.Tests/MyCompanyName.MyProjectName.Web.Tests.csproj index 3ca0425b28..8d41ed9e74 100644 --- a/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.Web.Tests/MyCompanyName.MyProjectName.Web.Tests.csproj +++ b/templates/app/aspnet-core/test/MyCompanyName.MyProjectName.Web.Tests/MyCompanyName.MyProjectName.Web.Tests.csproj @@ -9,7 +9,7 @@ - + diff --git a/templates/console/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj b/templates/console/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj index 65fee12cfc..1a29eebb8d 100644 --- a/templates/console/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj +++ b/templates/console/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj @@ -13,12 +13,12 @@ - - - - + + + + - + diff --git a/templates/maui/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj b/templates/maui/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj index f33122e97a..66e554f50f 100644 --- a/templates/maui/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj +++ b/templates/maui/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj @@ -35,7 +35,7 @@ - + diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj index 7efc0aae71..f48c02d1b4 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.AuthServer/MyCompanyName.MyProjectName.AuthServer.csproj @@ -11,10 +11,10 @@ - - - - + + + + all runtime; build; native; contentfiles; analyzers diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyCompanyName.MyProjectName.Blazor.Host.Client.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyCompanyName.MyProjectName.Blazor.Host.Client.csproj index 7213d6b05e..05e6cbac0b 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyCompanyName.MyProjectName.Blazor.Host.Client.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host.Client/MyCompanyName.MyProjectName.Blazor.Host.Client.csproj @@ -10,10 +10,10 @@ - - - - + + + + diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host/MyCompanyName.MyProjectName.Blazor.Host.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host/MyCompanyName.MyProjectName.Blazor.Host.csproj index 589c0fced6..dc15b0086f 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host/MyCompanyName.MyProjectName.Blazor.Host.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Host/MyCompanyName.MyProjectName.Blazor.Host.csproj @@ -8,12 +8,12 @@ - - + + - + diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/MyCompanyName.MyProjectName.Blazor.Server.Host.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/MyCompanyName.MyProjectName.Blazor.Server.Host.csproj index 52f7472466..c8096f7080 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/MyCompanyName.MyProjectName.Blazor.Server.Host.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Blazor.Server.Host/MyCompanyName.MyProjectName.Blazor.Server.Host.csproj @@ -13,11 +13,11 @@ - - - - - + + + + + diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj index 8db094e467..b9dffe7646 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.HttpApi.Host/MyCompanyName.MyProjectName.HttpApi.Host.csproj @@ -11,12 +11,12 @@ - - + + - - - + + + diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj index 666658da33..f2a0614cc0 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Host/MyCompanyName.MyProjectName.Web.Host.csproj @@ -11,9 +11,9 @@ - - - + + + diff --git a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyCompanyName.MyProjectName.Web.Unified.csproj b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyCompanyName.MyProjectName.Web.Unified.csproj index bc22e1a5f6..e2deed8b1f 100644 --- a/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyCompanyName.MyProjectName.Web.Unified.csproj +++ b/templates/module/aspnet-core/host/MyCompanyName.MyProjectName.Web.Unified/MyCompanyName.MyProjectName.Web.Unified.csproj @@ -11,9 +11,9 @@ - - - + + + all runtime; build; native; contentfiles; analyzers diff --git a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj index 20c07d94f8..557b0e24be 100644 --- a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj +++ b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Domain.Shared/MyCompanyName.MyProjectName.Domain.Shared.csproj @@ -15,7 +15,7 @@ - + diff --git a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj index b8791a1f3f..6d10c6e6fd 100644 --- a/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj +++ b/templates/module/aspnet-core/src/MyCompanyName.MyProjectName.Web/MyCompanyName.MyProjectName.Web.csproj @@ -22,7 +22,7 @@ - + diff --git a/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests.csproj b/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests.csproj index 7a93e9f941..6aff21d32c 100644 --- a/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests.csproj +++ b/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests/MyCompanyName.MyProjectName.EntityFrameworkCore.Tests.csproj @@ -10,7 +10,7 @@ - + diff --git a/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj b/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj index 98abac34f3..321143def3 100644 --- a/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj +++ b/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp/MyCompanyName.MyProjectName.HttpApi.Client.ConsoleTestApp.csproj @@ -23,7 +23,7 @@ - + diff --git a/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.MongoDB.Tests/MyCompanyName.MyProjectName.MongoDB.Tests.csproj b/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.MongoDB.Tests/MyCompanyName.MyProjectName.MongoDB.Tests.csproj index f63599ff8a..60bd4f1fe8 100644 --- a/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.MongoDB.Tests/MyCompanyName.MyProjectName.MongoDB.Tests.csproj +++ b/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.MongoDB.Tests/MyCompanyName.MyProjectName.MongoDB.Tests.csproj @@ -10,10 +10,10 @@ - - + + - + diff --git a/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.TestBase/MyCompanyName.MyProjectName.TestBase.csproj b/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.TestBase/MyCompanyName.MyProjectName.TestBase.csproj index 4c004ee12a..9addb70011 100644 --- a/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.TestBase/MyCompanyName.MyProjectName.TestBase.csproj +++ b/templates/module/aspnet-core/test/MyCompanyName.MyProjectName.TestBase/MyCompanyName.MyProjectName.TestBase.csproj @@ -10,15 +10,15 @@ - - + + all runtime; build; native; contentfiles; analyzers - - - - + + + + diff --git a/templates/wpf/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj b/templates/wpf/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj index 3f3a1535bb..b40aa0ffb2 100644 --- a/templates/wpf/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj +++ b/templates/wpf/src/MyCompanyName.MyProjectName/MyCompanyName.MyProjectName.csproj @@ -14,11 +14,11 @@ - - - - - + + + + +