Browse Source

Merge branch 'rel-9.2' into feature/#22507

pull/22619/head
oykuermann 9 months ago
parent
commit
944836a735
  1. 260
      Directory.Packages.props
  2. 11
      docs/en/framework/architecture/modularity/extending/module-entity-extensions.md
  3. 12
      docs/en/framework/data/entity-framework-core/index.md
  4. 11
      docs/en/framework/fundamentals/localization.md
  5. 11
      docs/en/framework/ui/maui/index.md
  6. 5
      docs/en/framework/ui/mvc-razor-pages/customization-user-interface.md
  7. 14
      docs/en/modules/cms-kit-pro/url-forwarding.md
  8. 2
      docs/en/modules/language-management.md
  9. 14
      docs/en/release-info/migration-guides/abp-8-2.md
  10. 4
      docs/en/tutorials/microservice/part-06.md
  11. 6
      docs/en/tutorials/todo/layered/index.md
  12. 8
      docs/en/tutorials/todo/single-layer/index.md
  13. BIN
      docs/en/ui-themes/lepton-x/images/account-layout-background-style.png
  14. 16
      docs/en/ui-themes/lepton-x/mvc.md
  15. 2
      framework/src/Volo.Abp.AspNetCore.Components.Web.Theming/Theming/DefaultThemeManager.cs
  16. 29
      framework/src/Volo.Abp.AspNetCore.Mvc/Volo/Abp/AspNetCore/Mvc/ApplicationConfigurations/AbpApplicationConfigurationAppService.cs
  17. 2
      framework/src/Volo.Abp.Caching/Volo/Abp/Caching/AbpCachingModule.cs
  18. 15
      framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCache.cs
  19. 3
      framework/src/Volo.Abp.Cli.Core/Volo/Abp/Cli/Bundling/PathHelper.cs
  20. 10
      framework/src/Volo.Abp.Ddd.Domain/Volo/Abp/Domain/Entities/Events/AbpEntityChangeOptions.cs
  21. 17
      framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs
  22. 131
      framework/test/Volo.Abp.EntityFrameworkCore.Tests/Volo/Abp/EntityFrameworkCore/DomainEvents/DomainEvents_Tests.cs
  23. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/ar.json
  24. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/cs.json
  25. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/de.json
  26. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/el.json
  27. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/en-GB.json
  28. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/en.json
  29. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/es.json
  30. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/fa.json
  31. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/fi.json
  32. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/fr.json
  33. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/hi.json
  34. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/hr.json
  35. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/hu.json
  36. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/is.json
  37. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/it.json
  38. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/nl.json
  39. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/pl-PL.json
  40. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/pt-BR.json
  41. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/ro-RO.json
  42. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/ru.json
  43. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/sk.json
  44. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/sl.json
  45. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/sv.json
  46. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/tr.json
  47. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/vi.json
  48. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/zh-Hans.json
  49. 3
      modules/permission-management/src/Volo.Abp.PermissionManagement.Domain.Shared/Volo/Abp/PermissionManagement/Localization/Domain/zh-Hant.json
  50. 4
      modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml
  51. 398
      npm/ng-packs/packages/schematics/src/commands/change-theme/index.ts
  52. 101
      npm/ng-packs/packages/schematics/src/commands/change-theme/style-map.ts
  53. 44
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/.eslintrc.json.template
  54. 7
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/ng-package.json.template
  55. 1
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/enums/index.ts.template
  56. 3
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/enums/route-names.ts.template
  57. 1
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/providers/index.ts.template
  58. 30
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/providers/route.provider.ts.template
  59. 2
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/config/src/public-api.ts.template
  60. 44
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/karma.conf.js.template
  61. 7
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/ng-package.json.template
  62. 11
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/package.json.template
  63. 11
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__.component.ts.template
  64. 9
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/__libraryName@kebab__.routes.ts.template
  65. 1
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/lib/index.ts.template
  66. 4
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/public-api.ts.template
  67. 26
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/src/test.ts.template
  68. 20
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.json.template
  69. 10
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.lib.prod.json.template
  70. 17
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-package-standalone/__libraryName@kebab__/tsconfig.spec.json.template
  71. 6
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-secondary-entrypoint-standalone/__libraryName@kebab__/ng-package.json.template
  72. 7
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-secondary-entrypoint-standalone/__libraryName@kebab__/src/lib/__target@kebab__-__libraryName@kebab__.ts.template
  73. 1
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-secondary-entrypoint-standalone/__libraryName@kebab__/src/lib/index.ts.template
  74. 1
      npm/ng-packs/packages/schematics/src/commands/create-lib/files-secondary-entrypoint-standalone/__libraryName@kebab__/src/public-api.ts.template
  75. 307
      npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts
  76. 11
      npm/ng-packs/packages/schematics/src/commands/create-lib/models/generate-lib-schema.ts
  77. 22
      npm/ng-packs/packages/schematics/src/commands/create-lib/schema.json
  78. 11
      npm/ng-packs/packages/schematics/src/utils/angular/add-declaration-to-ng-module.ts
  79. 141
      npm/ng-packs/packages/schematics/src/utils/angular/ast-utils.ts
  80. 20
      npm/ng-packs/packages/schematics/src/utils/angular/change.ts
  81. 2
      npm/ng-packs/packages/schematics/src/utils/angular/dependencies.ts
  82. 10
      npm/ng-packs/packages/schematics/src/utils/angular/dependency.ts
  83. 25
      npm/ng-packs/packages/schematics/src/utils/angular/eol.ts
  84. 12
      npm/ng-packs/packages/schematics/src/utils/angular/find-module.ts
  85. 22
      npm/ng-packs/packages/schematics/src/utils/angular/generate-from-files.ts
  86. 1
      npm/ng-packs/packages/schematics/src/utils/angular/index.ts
  87. 18
      npm/ng-packs/packages/schematics/src/utils/angular/json-file.ts
  88. 24
      npm/ng-packs/packages/schematics/src/utils/angular/ng-ast-utils.ts
  89. 2
      npm/ng-packs/packages/schematics/src/utils/angular/parse-name.ts
  90. 12
      npm/ng-packs/packages/schematics/src/utils/angular/paths.ts
  91. 12
      npm/ng-packs/packages/schematics/src/utils/angular/project-targets.ts
  92. 148
      npm/ng-packs/packages/schematics/src/utils/angular/standalone/app_component.ts
  93. 127
      npm/ng-packs/packages/schematics/src/utils/angular/standalone/app_config.ts
  94. 115
      npm/ng-packs/packages/schematics/src/utils/angular/standalone/code_block.ts
  95. 10
      npm/ng-packs/packages/schematics/src/utils/angular/standalone/index.ts
  96. 258
      npm/ng-packs/packages/schematics/src/utils/angular/standalone/rules.ts
  97. 171
      npm/ng-packs/packages/schematics/src/utils/angular/standalone/util.ts
  98. 5
      npm/ng-packs/packages/schematics/src/utils/angular/validation.ts
  99. 20
      npm/ng-packs/packages/schematics/src/utils/angular/workspace-models.ts
  100. 12
      npm/ng-packs/packages/schematics/src/utils/angular/workspace.ts

260
Directory.Packages.props

@ -3,187 +3,187 @@
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="AlibabaCloud.SDK.Dysmsapi20170525" Version="3.0.0" />
<PackageVersion Include="AlibabaCloud.SDK.Dysmsapi20170525" Version="4.0.0" />
<PackageVersion Include="aliyun-net-sdk-sts" Version="3.1.2" />
<PackageVersion Include="Aliyun.OSS.SDK.NetCore" Version="2.14.1" />
<PackageVersion Include="AsyncKeyedLock" Version="7.1.3" />
<PackageVersion Include="Autofac" Version="8.1.0" />
<PackageVersion Include="AsyncKeyedLock" Version="7.1.6" />
<PackageVersion Include="Autofac" Version="8.2.1" />
<PackageVersion Include="Autofac.Extensions.DependencyInjection" Version="10.0.0" />
<PackageVersion Include="Autofac.Extras.DynamicProxy" Version="7.1.0" />
<PackageVersion Include="AutoMapper" Version="13.0.1" />
<PackageVersion Include="AutoMapper" Version="14.0.0" />
<PackageVersion Include="Asp.Versioning.Mvc" Version="8.1.0" />
<PackageVersion Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.1.0" />
<PackageVersion Include="AWSSDK.S3" Version="3.7.410.9" />
<PackageVersion Include="AWSSDK.SecurityToken" Version="3.7.401.16" />
<PackageVersion Include="AWSSDK.S3" Version="4.0.0" />
<PackageVersion Include="AWSSDK.SecurityToken" Version="4.0.0" />
<PackageVersion Include="BunnyCDN.Net.Storage" Version="1.0.4" />
<PackageVersion Include="Azure.Messaging.ServiceBus" Version="7.18.1" />
<PackageVersion Include="Azure.Storage.Blobs" Version="12.22.1" />
<PackageVersion Include="Blazorise" Version="1.7.5" />
<PackageVersion Include="Blazorise.Components" Version="1.7.5" />
<PackageVersion Include="Blazorise.DataGrid" Version="1.7.5" />
<PackageVersion Include="Blazorise.Snackbar" Version="1.7.5" />
<PackageVersion Include="Azure.Messaging.ServiceBus" Version="7.19.0" />
<PackageVersion Include="Azure.Storage.Blobs" Version="12.24.0" />
<PackageVersion Include="Blazorise" Version="1.7.6" />
<PackageVersion Include="Blazorise.Components" Version="1.7.6" />
<PackageVersion Include="Blazorise.DataGrid" Version="1.7.6" />
<PackageVersion Include="Blazorise.Snackbar" Version="1.7.6" />
<PackageVersion Include="Castle.Core" Version="5.1.1" />
<PackageVersion Include="Castle.Core.AsyncInterceptor" Version="2.1.0" />
<PackageVersion Include="CommonMark.NET" Version="0.15.1" />
<PackageVersion Include="Confluent.Kafka" Version="2.5.3" />
<PackageVersion Include="Dapper" Version="2.1.44" />
<PackageVersion Include="Dapr.AspNetCore" Version="1.14.0" />
<PackageVersion Include="Dapr.Client" Version="1.14.0" />
<PackageVersion Include="DeviceDetector.NET" Version="6.3.3" />
<PackageVersion Include="Devart.Data.Oracle.EFCore" Version="10.4.190.9" />
<PackageVersion Include="DistributedLock.Core" Version="1.0.7" />
<PackageVersion Include="Confluent.Kafka" Version="2.10.0" />
<PackageVersion Include="Dapper" Version="2.1.66" />
<PackageVersion Include="Dapr.AspNetCore" Version="1.15.4" />
<PackageVersion Include="Dapr.Client" Version="1.15.4" />
<PackageVersion Include="DeviceDetector.NET" Version="6.4.2" />
<PackageVersion Include="Devart.Data.Oracle.EFCore" Version="10.4.235.9" />
<PackageVersion Include="DistributedLock.Core" Version="1.0.8" />
<PackageVersion Include="DistributedLock.Redis" Version="1.0.3" />
<PackageVersion Include="DeepL.net" Version="1.10.0" />
<PackageVersion Include="EphemeralMongo.Core" Version="1.1.3" />
<PackageVersion Include="EphemeralMongo6.runtime.linux-x64" Version="1.1.3" />
<PackageVersion Include="DeepL.net" Version="1.15.0" />
<PackageVersion Include="EphemeralMongo.Core" Version="2.0.0" />
<PackageVersion Include="EphemeralMongo6.runtime.linux-x64" Version="2.0.0" />
<PackageVersion Include="EphemeralMongo6.runtime.osx-x64" Version="1.1.3" />
<PackageVersion Include="EphemeralMongo6.runtime.win-x64" Version="1.1.3" />
<PackageVersion Include="EphemeralMongo6.runtime.win-x64" Version="2.0.0" />
<PackageVersion Include="MongoSandbox.Core" Version="1.0.1" />
<PackageVersion Include="MongoSandbox6.runtime.linux-x64" Version="1.0.1" />
<PackageVersion Include="MongoSandbox6.runtime.osx-x64" Version="1.0.1" />
<PackageVersion Include="MongoSandbox6.runtime.win-x64" Version="1.0.1" />
<PackageVersion Include="FluentValidation" Version="11.10.0" />
<PackageVersion Include="Google.Cloud.Storage.V1" Version="4.10.0" />
<PackageVersion Include="FluentValidation" Version="11.11.0" />
<PackageVersion Include="Google.Cloud.Storage.V1" Version="4.13.0" />
<PackageVersion Include="Hangfire.AspNetCore" Version="1.8.18" />
<PackageVersion Include="Hangfire.SqlServer" Version="1.8.18" />
<PackageVersion Include="HtmlSanitizer" Version="8.1.870" />
<PackageVersion Include="HtmlSanitizer" Version="9.0.884" />
<PackageVersion Include="IdentityModel" Version="7.0.0" />
<PackageVersion Include="IdentityServer4" Version="4.1.2" />
<PackageVersion Include="IdentityServer4.AspNetIdentity" Version="4.1.2" />
<PackageVersion Include="JetBrains.Annotations" Version="2024.2.0" />
<PackageVersion Include="JetBrains.Annotations" Version="2024.3.0" />
<PackageVersion Include="LdapForNet" Version="2.7.15" />
<PackageVersion Include="LibGit2Sharp" Version="0.30.0" />
<PackageVersion Include="LibGit2Sharp" Version="0.31.0" />
<PackageVersion Include="Magick.NET-Q16-AnyCPU" Version="13.4.0" />
<PackageVersion Include="MailKit" Version="4.8.0" />
<PackageVersion Include="Markdig.Signed" Version="0.37.0" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="9.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Components.Web" Version="9.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="9.0.30" />
<PackageVersion Include="Microsoft.Maui.Controls" Version="9.0.30" />
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="9.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.2" />
<PackageVersion Include="MailKit" Version="4.12.0" />
<PackageVersion Include="Markdig.Signed" Version="0.41.1" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.JwtBearer" Version="9.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" Version="9.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Authorization" Version="9.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Components" Version="9.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Components.Authorization" Version="9.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Components.Web" Version="9.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="9.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.Authentication" Version="9.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Components.WebView.Maui" Version="9.0.60" />
<PackageVersion Include="Microsoft.Maui.Controls" Version="9.0.60" />
<PackageVersion Include="Microsoft.AspNetCore.DataProtection.StackExchangeRedis" Version="9.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="9.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="9.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.Razor.Language" Version="6.0.36" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="9.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.WebUtilities" Version="9.0.2" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.2" />
<PackageVersion Include="Microsoft.AspNetCore.TestHost" Version="9.0.4" />
<PackageVersion Include="Microsoft.AspNetCore.WebUtilities" Version="9.0.4" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.4" />
<PackageVersion Include="Microsoft.CodeAnalysis.CSharp" Version="4.5.0" />
<PackageVersion Include="Microsoft.CSharp" Version="4.7.0" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.2" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.0.1" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="9.0.0-preview.7.24406.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.CommandLine" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Composite" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Localization" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.2" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.2" />
<PackageVersion Include="Microsoft.Data.Sqlite" Version="9.0.4" />
<PackageVersion Include="Microsoft.Data.SqlClient" Version="6.0.2" />
<PackageVersion Include="Microsoft.EntityFrameworkCore" Version="9.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.InMemory" Version="9.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Proxies" Version="9.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Relational" Version="9.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Sqlite" Version="9.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.SqlServer" Version="9.0.4" />
<PackageVersion Include="Microsoft.EntityFrameworkCore.Tools" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.Hybrid" Version="9.4.0" />
<PackageVersion Include="Microsoft.Extensions.Caching.Memory" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Caching.StackExchangeRedis" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Configuration.Binder" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Configuration.CommandLine" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Configuration.UserSecrets" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Composite" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Embedded" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.FileProviders.Physical" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.FileSystemGlobbing" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Hosting" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Hosting.Abstractions" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Http" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Identity.Core" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Localization" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Logging.Abstractions" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Logging" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Logging.Console" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Options" Version="9.0.4" />
<PackageVersion Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="9.0.4" />
<PackageVersion Include="Microsoft.NET.Test.Sdk" Version="17.13.0" />
<PackageVersion Include="Microsoft.VisualStudio.Web.CodeGeneration.Design" Version="9.0.0" />
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.6.0" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.6.0" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.6.0" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.6.0" />
<PackageVersion Include="Minio" Version="6.0.3" />
<PackageVersion Include="System.IdentityModel.Tokens.Jwt" Version="8.9.0" />
<PackageVersion Include="Microsoft.IdentityModel.Protocols.OpenIdConnect" Version="8.9.0" />
<PackageVersion Include="Microsoft.IdentityModel.Tokens" Version="8.9.0" />
<PackageVersion Include="Microsoft.IdentityModel.JsonWebTokens" Version="8.9.0" />
<PackageVersion Include="Minio" Version="6.0.4" />
<PackageVersion Include="MongoDB.Driver" Version="3.3.0" />
<PackageVersion Include="NEST" Version="7.17.5" />
<PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="Nito.AsyncEx.Context" Version="5.1.2" />
<PackageVersion Include="Npgsql.EntityFrameworkCore.PostgreSQL" Version="9.0.4" />
<PackageVersion Include="NSubstitute" Version="5.1.0" />
<PackageVersion Include="NuGet.Versioning" Version="6.11.1" />
<PackageVersion Include="NUglify" Version="1.21.9" />
<PackageVersion Include="NSubstitute" Version="5.3.0" />
<PackageVersion Include="NuGet.Versioning" Version="6.13.2" />
<PackageVersion Include="NUglify" Version="1.21.15" />
<PackageVersion Include="Nullable" Version="1.3.1" />
<PackageVersion Include="Octokit" Version="13.0.1" />
<PackageVersion Include="OpenIddict.Abstractions" Version="6.1.1" />
<PackageVersion Include="OpenIddict.Core" Version="6.1.1" />
<PackageVersion Include="OpenIddict.Server.AspNetCore" Version="6.1.1" />
<PackageVersion Include="OpenIddict.Validation.AspNetCore" Version="6.1.1" />
<PackageVersion Include="OpenIddict.Validation.ServerIntegration" Version="6.1.1" />
<PackageVersion Include="Oracle.EntityFrameworkCore" Version="9.23.60" />
<PackageVersion Include="Polly" Version="8.4.2" />
<PackageVersion Include="Octokit" Version="14.0.0" />
<PackageVersion Include="OpenIddict.Abstractions" Version="6.2.1" />
<PackageVersion Include="OpenIddict.Core" Version="6.2.1" />
<PackageVersion Include="OpenIddict.Server.AspNetCore" Version="6.2.1" />
<PackageVersion Include="OpenIddict.Validation.AspNetCore" Version="6.2.1" />
<PackageVersion Include="OpenIddict.Validation.ServerIntegration" Version="6.2.1" />
<PackageVersion Include="Oracle.EntityFrameworkCore" Version="9.23.80" />
<PackageVersion Include="Polly" Version="8.5.2" />
<PackageVersion Include="Polly.Extensions.Http" Version="3.0.0" />
<PackageVersion Include="Pomelo.EntityFrameworkCore.MySql" Version="9.0.0-preview.3.efcore.9.0.0" />
<PackageVersion Include="Quartz" Version="3.13.0" />
<PackageVersion Include="Quartz.Extensions.DependencyInjection" Version="3.13.0" />
<PackageVersion Include="Quartz.Plugins.TimeZoneConverter" Version="3.13.0" />
<PackageVersion Include="Quartz.Serialization.Json" Version="3.13.0" />
<PackageVersion Include="Quartz" Version="3.14.0" />
<PackageVersion Include="Quartz.Extensions.DependencyInjection" Version="3.14.0" />
<PackageVersion Include="Quartz.Plugins.TimeZoneConverter" Version="3.14.0" />
<PackageVersion Include="Quartz.Serialization.Json" Version="3.14.0" />
<PackageVersion Include="RabbitMQ.Client" Version="6.8.1" />
<PackageVersion Include="Rebus" Version="8.6.0" />
<PackageVersion Include="Rebus.ServiceProvider" Version="10.2.0" />
<PackageVersion Include="Scriban" Version="5.10.0" />
<PackageVersion Include="Serilog" Version="4.0.2" />
<PackageVersion Include="Serilog.AspNetCore" Version="8.0.2" />
<PackageVersion Include="Serilog.Extensions.Hosting" Version="8.0.0" />
<PackageVersion Include="Serilog.Extensions.Logging" Version="8.0.0" />
<PackageVersion Include="Serilog.Sinks.Async" Version="2.0.0" />
<PackageVersion Include="Rebus" Version="8.8.0" />
<PackageVersion Include="Rebus.ServiceProvider" Version="10.3.0" />
<PackageVersion Include="Scriban" Version="6.2.1" />
<PackageVersion Include="Serilog" Version="4.2.0" />
<PackageVersion Include="Serilog.AspNetCore" Version="9.0.0" />
<PackageVersion Include="Serilog.Extensions.Hosting" Version="9.0.0" />
<PackageVersion Include="Serilog.Extensions.Logging" Version="9.0.1" />
<PackageVersion Include="Serilog.Sinks.Async" Version="2.1.0" />
<PackageVersion Include="Serilog.Sinks.Console" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="6.0.0" />
<PackageVersion Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageVersion Include="SharpZipLib" Version="1.4.2" />
<PackageVersion Include="Shouldly" Version="4.2.1" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.5" />
<PackageVersion Include="SixLabors.ImageSharp.Drawing" Version="2.1.4" />
<PackageVersion Include="Shouldly" Version="4.3.0" />
<PackageVersion Include="SixLabors.ImageSharp" Version="3.1.8" />
<PackageVersion Include="SixLabors.ImageSharp.Drawing" Version="2.1.6" />
<PackageVersion Include="SkiaSharp" Version="2.88.8" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="2.88.8" />
<PackageVersion Include="SkiaSharp.NativeAssets.macOS" Version="2.88.8" />
<PackageVersion Include="Slugify.Core" Version="4.0.1" />
<PackageVersion Include="Spectre.Console" Version="0.49.1" />
<PackageVersion Include="StackExchange.Redis" Version="2.8.16" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="6.8.1" />
<PackageVersion Include="System.Collections.Immutable" Version="9.0.2" />
<PackageVersion Include="Spectre.Console" Version="0.50.0" />
<PackageVersion Include="StackExchange.Redis" Version="2.8.31" />
<PackageVersion Include="Swashbuckle.AspNetCore" Version="8.1.1" />
<PackageVersion Include="System.Collections.Immutable" Version="9.0.4" />
<PackageVersion Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageVersion Include="System.Linq.Async" Version="6.0.1" />
<PackageVersion Include="System.Linq.Dynamic.Core" Version="1.6.1" />
<PackageVersion Include="System.Linq.Dynamic.Core" Version="1.6.2" />
<PackageVersion Include="System.Linq.Queryable" Version="4.3.0" />
<PackageVersion Include="System.Runtime.Loader" Version="4.3.0" />
<PackageVersion Include="System.Security.Permissions" Version="9.0.2" />
<PackageVersion Include="System.Security.Permissions" Version="9.0.4" />
<PackageVersion Include="System.Security.Principal.Windows" Version="5.0.0" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.2" />
<PackageVersion Include="System.Text.Encodings.Web" Version="9.0.2" />
<PackageVersion Include="System.Text.Json" Version="9.0.2" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.0" />
<PackageVersion Include="TencentCloudSDK.Sms" Version="3.0.1142" />
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.4" />
<PackageVersion Include="System.Text.Encodings.Web" Version="9.0.4" />
<PackageVersion Include="System.Text.Json" Version="9.0.4" />
<PackageVersion Include="System.Threading.Tasks.Extensions" Version="4.6.3" />
<PackageVersion Include="TencentCloudSDK.Sms" Version="3.0.1231" />
<PackageVersion Include="TimeZoneConverter" Version="7.0.0" />
<PackageVersion Include="Unidecode.NET" Version="2.1.0" />
<PackageVersion Include="xunit" Version="2.9.2" />
<PackageVersion Include="xunit.extensibility.execution" Version="2.9.2" />
<PackageVersion Include="xunit.runner.visualstudio" Version="2.8.2" />
<PackageVersion Include="coverlet.collector" Version="6.0.2" />
<PackageVersion Include="xunit" Version="2.9.3" />
<PackageVersion Include="xunit.extensibility.execution" Version="2.9.3" />
<PackageVersion Include="xunit.runner.visualstudio" Version="3.0.2" />
<PackageVersion Include="coverlet.collector" Version="6.0.4" />
<PackageVersion Include="ConfigureAwait.Fody" Version="3.3.2" />
<PackageVersion Include="Fody" Version="6.8.2" />
<PackageVersion Include="Fody" Version="6.9.2" />
</ItemGroup>
</Project>

11
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.

12
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<string>(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

11
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<AbpLocalizationOptions>(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;

11
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

5
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

14
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<ShortenedUrlCacheOptions>(options =>
{
options.CacheAllOnStartup = true; // Cache all shortened URLs on startup
});
```
# Internals
## Domain Layer

2
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

14
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<IdentitySession>` to your `DbContext` class if it implements `IIdentityDbContext` interface.
```csharp
public class YourDbContext : AbpDbContext<YourDbContext>, IIdentityDbContext
{
public DbSet<IdentitySession> 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 |
| ---------------------------------------------------------- | ----------- | ----------- |

4
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)

6
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{

8
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{

BIN
docs/en/ui-themes/lepton-x/images/account-layout-background-style.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

16
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<LeptonXThemeMvcOptions>(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<LeptonXThemeMvcOptions>(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**.

2
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;
}
}

29
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
}
}
};

2
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;

15
framework/src/Volo.Abp.Caching/Volo/Abp/Caching/Hybrid/AbpHybridCache.cs

@ -75,7 +75,7 @@ public class AbpHybridCache<TCacheItem, TCacheKey> : IHybridCache<TCacheItem, TC
protected HybridCache HybridCache { get; }
protected IDistributedCache DistributedCacheCache { get; }
protected IDistributedCache DistributedCache { get; }
protected ICancellationTokenProvider CancellationTokenProvider { get; }
@ -105,7 +105,7 @@ public class AbpHybridCache<TCacheItem, TCacheKey> : IHybridCache<TCacheItem, TC
ServiceProvider = serviceProvider;
DistributedCacheOption = distributedCacheOption.Value;
HybridCache = hybridCache;
DistributedCacheCache = distributedCache;
DistributedCache = distributedCache;
CancellationTokenProvider = cancellationTokenProvider;
Logger = NullLogger<AbpHybridCache<TCacheItem, TCacheKey>>.Instance;
KeyNormalizer = keyNormalizer;
@ -215,10 +215,15 @@ public class AbpHybridCache<TCacheItem, TCacheKey> : IHybridCache<TCacheItem, TC
}
}
var bytes = await DistributedCacheCache.GetAsync(NormalizeKey(key), token);
if (bytes != null)
if (await DistributedCache.GetAsync(NormalizeKey(key), token) != null)
{
return ResolveSerializer().Deserialize(new ReadOnlySequence<byte>(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();

3
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));
}

10
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; }
/// <summary>
/// Default: true.
/// Update the aggregate root when any navigation property changes.
/// Some properties like ConcurrencyStamp,LastModificationTime,LastModifierId etc. will be updated.
/// </summary>
public bool UpdateAggregateRootWhenNavigationChanges { get; set; } = true;
public IEntitySelectorList IgnoredUpdateAggregateRootSelectors { get; set; }
public AbpEntityChangeOptions()
{
IgnoredNavigationEntitySelectors = new EntitySelectorList();
IgnoredUpdateAggregateRootSelectors = new EntitySelectorList();
}
}

17
framework/src/Volo.Abp.EntityFrameworkCore/Volo/Abp/EntityFrameworkCore/AbpDbContext.cs

@ -274,7 +274,9 @@ public abstract class AbpDbContext<TDbContext> : 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<TDbContext> : 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<ISoftDelete>().IsDeleted)
{
EntityChangeEventHelper.PublishEntityDeletedEvent(entry.Entity);
@ -478,11 +485,13 @@ public abstract class AbpDbContext<TDbContext> : 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);
}

131
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<AppEntityWithNavigations, Guid> AppEntityWithNavigationsRepository;
protected readonly IRepository<AppEntityWithNavigationsForeign, Guid> AppEntityWithNavigationForeignRepository;
protected readonly ILocalEventBus LocalEventBus;
protected AbpEfCoreDomainEvents_Disable_UpdateAggregateRoot_Tests()
{
AppEntityWithNavigationsRepository = GetRequiredService<IRepository<AppEntityWithNavigations, Guid>>();
AppEntityWithNavigationForeignRepository = GetRequiredService<IRepository<AppEntityWithNavigationsForeign, Guid>>();
LocalEventBus = GetRequiredService<ILocalEventBus>();
}
protected override void AfterAddApplication(IServiceCollection services)
{
services.Configure<AbpEntityChangeOptions>(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<EntityUpdatedEventData<AppEntityWithNavigations>>(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<AppEntityWithNavigationChildOneToMany>()
{
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<AppEntityWithNavigationChildManyToMany>()
{
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<AbpEntityChangeOptions>(options =>
{
options.UpdateAggregateRootWhenNavigationChanges = false;
});
base.AfterAddApplication(services);
}
}
public class AbpEfCoreDomainEvents_IgnoredUpdateAggregateRootSelectors_Test : AbpEfCoreDomainEvents_Disable_UpdateAggregateRoot_Tests
{
protected override void AfterAddApplication(IServiceCollection services)
{
services.Configure<AbpEntityChangeOptions>(options =>
{
options.IgnoredUpdateAggregateRootSelectors.Add("AppEntityWithValueObjectAddress", x => x == typeof(AppEntityWithNavigations));
});
base.AfterAddApplication(services);
}
}

3
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": "تصفية"
}
}

3
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"
}
}

3
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"
}
}

3
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": "Φίλτρο"
}
}

3
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"
}
}

3
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"
}
}

3
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"
}
}

3
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": "فیلتر"
}
}

3
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"
}
}

3
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"
}
}

3
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": "फ़िल्टर"
}
}

3
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"
}
}

3
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ő"
}
}

3
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"
}
}

3
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"
}
}

3
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"
}
}

3
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"
}
}

3
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"
}
}

3
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"
}
}

3
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": "Фильтр"
}
}

3
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ť"
}
}

3
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"
}
}

3
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"
}
}

3
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"
}
}

3
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"
}
}

3
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": "过滤"
}
}

3
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": "過濾"
}
}

4
modules/permission-management/src/Volo.Abp.PermissionManagement.Web/Pages/AbpPermissionManagement/PermissionManagementModal.cshtml

@ -23,7 +23,7 @@
<div class="col">
<div class="input-group mb-2">
<span class="input-group-text" id="basic-addon1"><i class="bi bi-search"></i></span>
<input type="text" class="form-control" id="permission-search" placeholder="Filter" />
<input type="text" class="form-control" id="permission-search" placeholder="@L["Filter"].Value" />
</div>
</div>
<div class="col-auto">
@ -34,7 +34,7 @@
</div>
</div>
<fieldset class="border rounded-4 p-3">
<legend class="px-1 h5 mb-0">Permission Group</legend>
<legend class="px-1 h5 mb-0">@L["PermissionGroup"].Value</legend>
<abp-tabs name="PermissionsTabs" class="lpx-scroll-pills-container" tab-style="PillVertical"
vertical-header-size="_4">

398
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;
}

101
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<ThemeOptionsEnum, StyleDefinition[]>();
@ -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<ThemeOptionsEnum, ImportDefinition[]>();
@ -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()',
},
]);

44
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": {}
}
]
}

7
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"
}
}

1
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';

3
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) %>',
}

1
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';

30
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);
}

2
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';

44
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
});
};

7
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"
}
}

11
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"
}
}

11
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: `<h1>Lazy Loaded Test Component</h1>`,
imports: [CoreModule, ThemeSharedModule],
})
export class <%= pascal(libraryName) %>Component {}

9
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),
},
];

1
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';

4
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';

26
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[];
<T>(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);

20
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"
]
}

10
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"
}
}

17
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"
]
}

6
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"
}
}

7
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
];
}

1
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) %>';

1
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';

307
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);
}

11
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;

22
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?"
}

11
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) {

141
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<ts.Node>,
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;
}

20
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<void> {
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<void> {
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);

2
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';

10
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<SchematicContext, Set<string>>();
@ -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}.

25
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;
}

12
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;

22
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))),
]);

1
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';

18
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,
},

24
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;
}
}

2
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';

12
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), '/') || '.';
}

12
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;
}

148
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;
}

127
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;
}

115
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<string, Map<string, string>>;
/** 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<string, Map<string, string>>();
// 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<string, string>;
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 };
}
}

10
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';

258
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)]);
}
}

171
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<string> {
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<string, string>;
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;
}

5
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;

20
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<Builders.Protractor, E2EOptions>;
interface WorkspaceCLISchema {
warnings?: Record<string, boolean>;
schematicCollections?: string[];
defaultCollection?: string;
}
export interface WorkspaceSchema {
version: 1;
defaultProject?: string;
cli?: WorkspaceCLISchema;
projects: {
[key: string]: WorkspaceProject<ProjectType.Application | ProjectType.Library>;
@ -165,6 +172,7 @@ export interface WorkspaceProject<TProjectType extends ProjectType = ProjectType
* Tool options.
*/
architect?: WorkspaceTargets<TProjectType>;
/**
* Tool options.
*/

12
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<string> {
@ -58,14 +58,12 @@ class TreeWorkspaceHost implements workspaces.WorkspaceHost {
export function updateWorkspace(
updater: (workspace: WorkspaceDefinition) => void | Rule | PromiseLike<void | Rule>,
): 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;
};

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save