Ver código fonte

Merge branch 'minor' into major

Michael Bromley 3 anos atrás
pai
commit
302456e7f4

+ 13 - 0
CHANGELOG.md

@@ -1,3 +1,16 @@
+## <small>1.9.2 (2023-01-18)</small>
+
+
+#### Fixes
+
+* **core** Fix collection delete for big collection (#1966) ([6541975](https://github.com/vendure-ecommerce/vendure/commit/6541975)), closes [#1966](https://github.com/vendure-ecommerce/vendure/issues/1966) [#1961](https://github.com/vendure-ecommerce/vendure/issues/1961)
+* **core** Fix deletion of guest Customers ([cbc500f](https://github.com/vendure-ecommerce/vendure/commit/cbc500f)), closes [#1960](https://github.com/vendure-ecommerce/vendure/issues/1960)
+* **core** Fix interop issues between native and external auth (#1968) ([82f6b61](https://github.com/vendure-ecommerce/vendure/commit/82f6b61)), closes [#1968](https://github.com/vendure-ecommerce/vendure/issues/1968)
+* **core** Validate OrderLine custom fields in ShopAPI mutations ([d272255](https://github.com/vendure-ecommerce/vendure/commit/d272255)), closes [#1953](https://github.com/vendure-ecommerce/vendure/issues/1953)
+* **create** Improve example dockerfile & docs ([86770d1](https://github.com/vendure-ecommerce/vendure/commit/86770d1))
+* **email-plugin** Fix generation of test emails ([80da8e0](https://github.com/vendure-ecommerce/vendure/commit/80da8e0)), closes [#1931](https://github.com/vendure-ecommerce/vendure/issues/1931)
+* **payments-plugin** Calculate tax per order line instead of per unit for Mollie (#1958) ([16b17b6](https://github.com/vendure-ecommerce/vendure/commit/16b17b6)), closes [#1958](https://github.com/vendure-ecommerce/vendure/issues/1958) [#1939](https://github.com/vendure-ecommerce/vendure/issues/1939)
+
 ## <small>1.9.1 (2022-12-08)</small>
 
 

+ 2 - 2
docs/content/developer-guide/customizing-the-order-process/index.md

@@ -80,9 +80,9 @@ This way multiple custom states gets defined.
 
 ### Example: Intercepting a state transition
 
-Now we have defined out new `ValidatingCustomer` state, but there is as yet nothing to enforce that the tax ID is valid. To add this constraint, we'll use the [`onTransitionStart` state transition hook]({{< relref "state-machine-config" >}}#ontransitionstart).
+Now we have defined our new `ValidatingCustomer` state, but there is as yet nothing to enforce that the tax ID is valid. To add this constraint, we'll use the [`onTransitionStart` state transition hook]({{< relref "state-machine-config" >}}#ontransitionstart).
 
-This allows us to perform our custom logic and potentially prevent the transition from occurring. We will also assume that we have available a provider named `TaxIdService` which contains the logic to validate a tax ID.
+This allows us to perform our custom logic and potentially prevent the transition from occurring. We will also assume that we have a provider named `TaxIdService` available which contains the logic to validate a tax ID.
 
 ```TypeScript
 // customer-validation-process.ts

+ 1 - 1
docs/content/plugins/writing-a-vendure-plugin.md

@@ -141,7 +141,7 @@ export class RandomCatPlugin {}
 
 ### Step 6: Declare any providers used in the resolver
 
-In order that out resolver is able to use Nest's dependency injection to inject and instance of `CatFetcher`, we must add it to the `providers` array in our plugin:
+In order that our resolver is able to use Nest's dependency injection to inject an instance of `CatFetcher`, we must add it to the `providers` array in our plugin:
 
 ```TypeScript
 @VendurePlugin({

+ 16 - 2
docs/content/user-guide/localization/index.md

@@ -5,13 +5,27 @@ weight: 10
 
 # Localization
 
-Vendure supports localization by allowing you to define translations for the following objects:
+Vendure supports **customer-facing** (Shop API) localization by allowing you to define translations for the following objects:
 
 * Collections
 * Countries
 * Facets
+* FacetValue
 * Products
-* Shipping methods
+* ProductOptions
+* ProductOptionGroups
+* ProductVariants
+* ShippingMethods  
+
+Vendure supports **admin-facing** (Admin API and Admin UI) localization by allowing you to define translations for labels and descriptions of the following objects:
+  
+* CustomFields
+* CollectionFilters
+* PaymentMethodHandlers
+* PromotionActions
+* PromotionConditions
+* ShippingCalculators
+* ShippingEligibilityCheckers
 
 ## How to enable languages
 

+ 2 - 2
docs/content/user-guide/settings/shipping-methods.md

@@ -36,13 +36,13 @@ By default, Vendure comes with a simple flat-rate shipping calculator. Your deve
 
 ## Fulfillment handler
 
-By "fulfillment" we mean how do we physically get the goods into the hands of the customer? Common fulfillment methods include:
+By "fulfillment" we mean how do we physically get the goods into the hands of the customer. Common fulfillment methods include:
 
 * Courier services such as FedEx, DPD, DHL, etc.
 * Collection by customer
 * Delivery via email for digital goods or licenses
 
-By default, Vendure comes with a "manual fulfillment handler", which allows you to manually enter the details of whatever actual method is used. For example, if you send the order by via courier, you can enter the courier name and parcel number manually when creating an order.
+By default, Vendure comes with a "manual fulfillment handler", which allows you to manually enter the details of whatever actual method is used. For example, if you send the order by courier, you can enter the courier name and parcel number manually when creating an order.
 
 Your developers can however create much more sophisticated fulfillment handlers, which can enable things like automated calls to courier APIs, automated label generation, and so on.
 

+ 4 - 4
packages/common/src/shared-types.ts

@@ -214,7 +214,7 @@ export type CustomFieldsObject = { [key: string]: any };
 export interface AdminUiConfig {
     /**
      * @description
-     * The hostname of the Vendure server which the admin ui will be making API calls
+     * The hostname of the Vendure server which the admin UI will be making API calls
      * to. If set to "auto", the Admin UI app will determine the hostname from the
      * current location (i.e. `window.location.hostname`).
      *
@@ -223,7 +223,7 @@ export interface AdminUiConfig {
     apiHost: string | 'auto';
     /**
      * @description
-     * The port of the Vendure server which the admin ui will be making API calls
+     * The port of the Vendure server which the admin UI will be making API calls
      * to. If set to "auto", the Admin UI app will determine the port from the
      * current location (i.e. `window.location.port`).
      *
@@ -331,7 +331,7 @@ export interface AdminUiConfig {
 export interface AdminUiAppConfig {
     /**
      * @description
-     * The path to the compiled admin ui app files. If not specified, an internal
+     * The path to the compiled admin UI app files. If not specified, an internal
      * default build is used. This path should contain the `vendure-ui-config.json` file,
      * index.html, the compiled js bundles etc.
      */
@@ -359,7 +359,7 @@ export interface AdminUiAppConfig {
 export interface AdminUiAppDevModeConfig {
     /**
      * @description
-     * The path to the uncompiled ui app source files. This path should contain the `vendure-ui-config.json` file.
+     * The path to the uncompiled UI app source files. This path should contain the `vendure-ui-config.json` file.
      */
     sourcePath: string;
     /**

+ 5 - 2
packages/core/src/config/auth/native-authentication-strategy.ts

@@ -62,7 +62,7 @@ export class NativeAuthenticationStrategy implements AuthenticationStrategy<Nati
     private getUserFromIdentifier(ctx: RequestContext, identifier: string): Promise<User | undefined> {
         return this.connection.getRepository(ctx, User).findOne({
             where: { identifier, deletedAt: null },
-            relations: ['roles', 'roles.channels'],
+            relations: ['roles', 'roles.channels', 'authenticationMethods'],
         });
     }
 
@@ -76,7 +76,10 @@ export class NativeAuthenticationStrategy implements AuthenticationStrategy<Nati
         if (!user) {
             return false;
         }
-        const nativeAuthMethod = user.getNativeAuthenticationMethod();
+        const nativeAuthMethod = user.getNativeAuthenticationMethod(false);
+        if (!nativeAuthMethod) {
+            return false;
+        }
         const pw =
             (
                 await this.connection

+ 1 - 1
packages/core/src/config/payment/payment-method-handler.ts

@@ -298,7 +298,7 @@ export interface PaymentMethodConfigOptions<T extends ConfigArgs> extends Config
 /**
  * @description
  * A PaymentMethodHandler contains the code which is used to generate a Payment when a call to the
- * `addPaymentToOrder` mutation is made. If contains any necessary steps of interfacing with a
+ * `addPaymentToOrder` mutation is made. It contains any necessary steps of interfacing with a
  * third-party payment gateway before the Payment is created and can also define actions to fire
  * when the state of the payment is changed.
  *

+ 6 - 6
packages/core/src/entity/user/user.entity.ts

@@ -29,10 +29,7 @@ export class User extends VendureEntity implements HasCustomFields, SoftDeletabl
     @Column()
     identifier: string;
 
-    @OneToMany(
-        type => AuthenticationMethod,
-        method => method.user,
-    )
+    @OneToMany(type => AuthenticationMethod, method => method.user)
     authenticationMethods: AuthenticationMethod[];
 
     @Column({ default: false })
@@ -48,14 +45,17 @@ export class User extends VendureEntity implements HasCustomFields, SoftDeletabl
     @Column(type => CustomUserFields)
     customFields: CustomUserFields;
 
-    getNativeAuthenticationMethod(): NativeAuthenticationMethod {
+    getNativeAuthenticationMethod(): NativeAuthenticationMethod;
+    getNativeAuthenticationMethod(strict?: boolean): NativeAuthenticationMethod | undefined;
+
+    getNativeAuthenticationMethod(strict?: boolean): NativeAuthenticationMethod | undefined {
         if (!this.authenticationMethods) {
             throw new InternalServerError('error.user-authentication-methods-not-loaded');
         }
         const match = this.authenticationMethods.find(
             (m): m is NativeAuthenticationMethod => m instanceof NativeAuthenticationMethod,
         );
-        if (!match) {
+        if (!match && (strict === undefined || strict)) {
             throw new InternalServerError('error.native-authentication-methods-not-found');
         }
         return match;

+ 6 - 11
packages/core/src/service/helpers/external-authentication/external-authentication.service.ts

@@ -214,21 +214,16 @@ export class ExternalAuthenticationService {
         strategy: string,
         externalIdentifier: string,
     ): Promise<User | undefined> {
-        const usersWithMatchingIdentifier = await this.connection
+        const user = await this.connection
             .getRepository(ctx, User)
             .createQueryBuilder('user')
-            .leftJoinAndSelect('user.authenticationMethods', 'authMethod')
+            .leftJoinAndSelect('user.authenticationMethods', 'aums')
+            .leftJoin('user.authenticationMethods', 'authMethod')
             .andWhere('authMethod.externalIdentifier = :externalIdentifier', { externalIdentifier })
+            .andWhere('authMethod.strategy = :strategy', { strategy })
             .andWhere('user.deletedAt IS NULL')
-            .getMany();
-
-        const matchingUser = usersWithMatchingIdentifier.find(user =>
-            user.authenticationMethods.find(
-                m => m instanceof ExternalAuthenticationMethod && m.strategy === strategy,
-            ),
-        );
-
-        return matchingUser;
+            .getOne();
+        return user;
     }
 
     private async findExistingCustomerUserByEmailAddress(ctx: RequestContext, emailAddress: string) {

+ 5 - 1
packages/core/src/service/services/auth.service.ts

@@ -17,6 +17,7 @@ import {
 } from '../../config/auth/native-authentication-strategy';
 import { ConfigService } from '../../config/config.service';
 import { TransactionalConnection } from '../../connection/transactional-connection';
+import { ExternalAuthenticationMethod } from '../../entity/authentication-method/external-authentication-method.entity';
 import { AuthenticatedSession } from '../../entity/session/authenticated-session.entity';
 import { User } from '../../entity/user/user.entity';
 import { EventBus } from '../../event-bus/event-bus';
@@ -87,7 +88,10 @@ export class AuthService {
             user.roles = userWithRoles?.roles || [];
         }
 
-        if (this.configService.authOptions.requireVerification && !user.verified) {
+        const extAuths = (user.authenticationMethods ?? []).filter(
+            am => am instanceof ExternalAuthenticationMethod,
+        );
+        if (!extAuths.length && this.configService.authOptions.requireVerification && !user.verified) {
             return new NotVerifiedError();
         }
         if (ctx.session && ctx.session.activeOrderId) {

+ 11 - 5
packages/core/src/service/services/user.service.ts

@@ -179,8 +179,9 @@ export class UserService {
         const user = await this.connection
             .getRepository(ctx, User)
             .createQueryBuilder('user')
-            .leftJoinAndSelect('user.authenticationMethods', 'authenticationMethod')
-            .addSelect('authenticationMethod.passwordHash')
+            .leftJoinAndSelect('user.authenticationMethods', 'aums')
+            .leftJoin('user.authenticationMethods', 'authenticationMethod')
+            .addSelect('aums.passwordHash')
             .where('authenticationMethod.verificationToken = :verificationToken', { verificationToken })
             .getOne();
         if (user) {
@@ -222,7 +223,10 @@ export class UserService {
         if (!user) {
             return;
         }
-        const nativeAuthMethod = user.getNativeAuthenticationMethod();
+        const nativeAuthMethod = user.getNativeAuthenticationMethod(false);
+        if (!nativeAuthMethod) {
+            return undefined;
+        }
         nativeAuthMethod.passwordResetToken = this.verificationTokenGenerator.generateVerificationToken();
         await this.connection.getRepository(ctx, NativeAuthenticationMethod).save(nativeAuthMethod);
         return user;
@@ -245,7 +249,8 @@ export class UserService {
         const user = await this.connection
             .getRepository(ctx, User)
             .createQueryBuilder('user')
-            .leftJoinAndSelect('user.authenticationMethods', 'authenticationMethod')
+            .leftJoinAndSelect('user.authenticationMethods', 'aums')
+            .leftJoin('user.authenticationMethods', 'authenticationMethod')
             .where('authenticationMethod.passwordResetToken = :passwordResetToken', { passwordResetToken })
             .getOne();
         if (!user) {
@@ -330,7 +335,8 @@ export class UserService {
         const user = await this.connection
             .getRepository(ctx, User)
             .createQueryBuilder('user')
-            .leftJoinAndSelect('user.authenticationMethods', 'authenticationMethod')
+            .leftJoinAndSelect('user.authenticationMethods', 'aums')
+            .leftJoin('user.authenticationMethods', 'authenticationMethod')
             .where('authenticationMethod.identifierChangeToken = :identifierChangeToken', {
                 identifierChangeToken: token,
             })

+ 53 - 13
packages/ui-devkit/src/compiler/scaffold.ts

@@ -8,13 +8,12 @@ import {
     GLOBAL_STYLES_OUTPUT_DIR,
     MODULES_OUTPUT_DIR,
     SHARED_EXTENSIONS_FILE,
-    STATIC_ASSETS_OUTPUT_DIR,
 } from './constants';
 import { getAllTranslationFiles, mergeExtensionTranslations } from './translations';
 import {
-    AdminUiExtension,
     AdminUiExtensionLazyModule,
     AdminUiExtensionSharedModule,
+    AdminUiExtensionWithId,
     Extension,
     GlobalStylesExtension,
     SassVariableOverridesExtension,
@@ -35,10 +34,13 @@ import {
 
 export async function setupScaffold(outputPath: string, extensions: Extension[]) {
     deleteExistingExtensionModules(outputPath);
-    copyAdminUiSource(outputPath);
 
     const adminUiExtensions = extensions.filter(isAdminUiExtension);
     const normalizedExtensions = normalizeExtensions(adminUiExtensions);
+
+    const modulePathMapping = generateModulePathMapping(normalizedExtensions);
+    copyAdminUiSource(outputPath, modulePathMapping);
+
     await copyExtensionModules(outputPath, normalizedExtensions);
 
     const staticAssetExtensions = extensions.filter(isStaticAssetExtension);
@@ -70,11 +72,30 @@ function deleteExistingExtensionModules(outputPath: string) {
     fs.removeSync(path.join(outputPath, MODULES_OUTPUT_DIR));
 }
 
+/**
+ * Generates a module path mapping object for all extensions with a "pathAlias"
+ * property declared (if any).
+ */
+function generateModulePathMapping(extensions: AdminUiExtensionWithId[]) {
+    const extensionsWithAlias = extensions.filter(e => e.pathAlias);
+    if (extensionsWithAlias.length === 0) {
+        return undefined;
+    }
+
+    return extensionsWithAlias.reduce((acc, e) => {
+        // for imports from the index file if there is one
+        acc[e.pathAlias as string] = [`src/extensions/${e.id}`];
+        // direct access to files / deep imports
+        acc[`${e.pathAlias as string}/*`] = [`src/extensions/${e.id}/*`];
+        return acc;
+    }, {} as Record<string, string[]>);
+}
+
 /**
  * Copies all files from the extensionPaths of the configured extensions into the
  * admin-ui source tree.
  */
-async function copyExtensionModules(outputPath: string, extensions: Array<Required<AdminUiExtension>>) {
+async function copyExtensionModules(outputPath: string, extensions: AdminUiExtensionWithId[]) {
     const extensionRoutesSource = generateLazyExtensionRoutes(extensions);
     fs.writeFileSync(path.join(outputPath, EXTENSION_ROUTES_FILE), extensionRoutesSource, 'utf8');
     const sharedExtensionModulesSource = generateSharedExtensionModule(extensions);
@@ -142,9 +163,9 @@ export async function copyGlobalStyleFile(outputPath: string, stylePath: string)
     await fs.copyFile(stylePath, styleOutputPath);
 }
 
-function generateLazyExtensionRoutes(extensions: Array<Required<AdminUiExtension>>): string {
+function generateLazyExtensionRoutes(extensions: AdminUiExtensionWithId[]): string {
     const routes: string[] = [];
-    for (const extension of extensions as Array<Required<AdminUiExtension>>) {
+    for (const extension of extensions as AdminUiExtensionWithId[]) {
         for (const module of extension.ngModules) {
             if (module.type === 'lazy') {
                 routes.push(`  {
@@ -159,7 +180,7 @@ function generateLazyExtensionRoutes(extensions: Array<Required<AdminUiExtension
     return `export const extensionRoutes = [${routes.join(',\n')}];\n`;
 }
 
-function generateSharedExtensionModule(extensions: Array<Required<AdminUiExtension>>) {
+function generateSharedExtensionModule(extensions: AdminUiExtensionWithId[]) {
     return `import { NgModule } from '@angular/core';
 import { CommonModule } from '@angular/common';
 ${extensions
@@ -193,15 +214,17 @@ function getModuleFilePath(
 }
 
 /**
- * Copy the Admin UI sources & static assets to the outputPath if it does not already
- * exists there.
+ * Copies the Admin UI sources & static assets to the outputPath if it does not already
+ * exist there.
  */
-function copyAdminUiSource(outputPath: string) {
-    const angularJsonFile = path.join(outputPath, 'angular.json');
-    const indexFile = path.join(outputPath, '/src/index.html');
-    if (fs.existsSync(angularJsonFile) && fs.existsSync(indexFile)) {
+function copyAdminUiSource(outputPath: string, modulePathMapping: Record<string, string[]> | undefined) {
+    const tsconfigFilePath = path.join(outputPath, 'tsconfig.json');
+    const indexFilePath = path.join(outputPath, '/src/index.html');
+    if (fs.existsSync(tsconfigFilePath) && fs.existsSync(indexFilePath)) {
+        configureModulePathMapping(tsconfigFilePath, modulePathMapping);
         return;
     }
+
     const scaffoldDir = path.join(__dirname, '../scaffold');
     const adminUiSrc = path.join(require.resolve('@vendure/admin-ui'), '../../static');
 
@@ -216,6 +239,7 @@ function copyAdminUiSource(outputPath: string) {
     fs.removeSync(outputPath);
     fs.ensureDirSync(outputPath);
     fs.copySync(scaffoldDir, outputPath);
+    configureModulePathMapping(tsconfigFilePath, modulePathMapping);
 
     // copy source files from admin-ui package
     const outputSrc = path.join(outputPath, 'src');
@@ -223,6 +247,22 @@ function copyAdminUiSource(outputPath: string) {
     fs.copySync(adminUiSrc, outputSrc);
 }
 
+/**
+ * Adds module path mapping to the bundled tsconfig.json file if defined as a UI extension.
+ */
+function configureModulePathMapping(
+    tsconfigFilePath: string,
+    modulePathMapping: Record<string, string[]> | undefined,
+) {
+    if (!modulePathMapping) {
+        return;
+    }
+
+    const tsconfig = require(tsconfigFilePath);
+    tsconfig.compilerOptions.paths = modulePathMapping;
+    fs.writeFileSync(tsconfigFilePath, JSON.stringify(tsconfig, null, 2));
+}
+
 /**
  * Attempts to find out it the ngcc compiler has been run on the Angular packages, and if not,
  * attemps to run it. This is done this way because attempting to run ngcc from a sub-directory

+ 92 - 0
packages/ui-devkit/src/compiler/types.ts

@@ -113,11 +113,99 @@ export interface AdminUiExtension
      * scss style sheets etc.
      */
     extensionPath: string;
+
     /**
      * @description
      * One or more Angular modules which extend the default Admin UI.
      */
     ngModules: Array<AdminUiExtensionSharedModule | AdminUiExtensionLazyModule>;
+
+    /**
+     * @description
+     * An optional alias for the module so it can be referenced by other UI extension modules.
+     *
+     * By default, Angular modules declared in an AdminUiExtension do not have access to code outside the directory
+     * defined by the `extensionPath`. A scenario in which that can be useful though is in a monorepo codebase where
+     * a common NgModule is shared across different plugins, each defined in its own package. An example can be found
+     * below - note that the main `tsconfig.json` also maps the target module but using a path relative to the project's
+     * root folder. The UI module is not part of the main TypeScript build task as explained in
+     * [Extending the Admin UI](https://www.vendure.io/docs/plugins/extending-the-admin-ui/) but having `paths`
+     * properly configured helps with usual IDE code editing features such as code completion and quick navigation, as
+     * well as linting.
+     *
+     * @example
+     * ```ts
+     * // packages/common-ui-module/src/ui/ui-shared.module.ts
+     * import { NgModule } from '\@angular/core';
+     * import { SharedModule } from '\@vendure/admin-ui/core';
+     * import { CommonUiComponent } from './components/common-ui/common-ui.component';
+     *
+     * export { CommonUiComponent };
+     *
+     * \@NgModule({
+     *  imports: [SharedModule],
+     *  exports: [CommonUiComponent],
+     *  declarations: [CommonUiComponent],
+     * })
+     * export class CommonSharedUiModule {}
+     * ```
+     *
+     * ```ts
+     * // packages/common-ui-module/src/index.ts
+     * import path from 'path';
+     *
+     * import { AdminUiExtension } from '\@vendure/ui-devkit/compiler';
+     *
+     * export const uiExtensions: AdminUiExtension = {
+     *   pathAlias: '\@common-ui-module',     // this is the important part
+     *   extensionPath: path.join(__dirname, 'ui'),
+     *   ngModules: [
+     *     {
+     *       type: 'shared' as const,
+     *       ngModuleFileName: 'ui-shared.module.ts',
+     *       ngModuleName: 'CommonSharedUiModule',
+     *     },
+     *   ],
+     * };
+     * ```
+     *
+     * ```json
+     * // tsconfig.json
+     * {
+     *   "compilerOptions": {
+     *     "baseUrl": ".",
+     *     "paths": {
+     *       "\@common-ui-module/*": ["packages/common-ui-module/src/ui/*"]
+     *     }
+     *   }
+     * }
+     * ```
+     *
+     * ```ts
+     * // packages/sample-plugin/src/ui/ui-extension.module.ts
+     * import { NgModule } from '\@angular/core';
+     * import { SharedModule } from '\@vendure/admin-ui/core';
+     * // the import below works both in the context of the custom Admin UI app as well as the main project
+     * // '\@common-ui-module' is the value of "pathAlias" and 'ui-shared.module' is the file we want to reference inside "extensionPath"
+     * import { CommonSharedUiModule, CommonUiComponent } from '\@common-ui-module/ui-shared.module';
+     *
+     * \@NgModule({
+     *   imports: [
+     *     SharedModule,
+     *     CommonSharedUiModule,
+     *     RouterModule.forChild([
+     *       {
+     *         path: '',
+     *         pathMatch: 'full',
+     *         component: CommonUiComponent,
+     *       },
+     *     ]),
+     *   ],
+     * })
+     * export class SampleUiExtensionModule {}
+     * ```
+     */
+    pathAlias?: string;
 }
 
 /**
@@ -280,3 +368,7 @@ export interface BrandingOptions {
     largeLogoPath?: string;
     faviconPath?: string;
 }
+
+export interface AdminUiExtensionWithId extends AdminUiExtension {
+    id: string;
+}

+ 2 - 1
packages/ui-devkit/src/compiler/utils.ts

@@ -8,6 +8,7 @@ import * as path from 'path';
 import { STATIC_ASSETS_OUTPUT_DIR } from './constants';
 import {
     AdminUiExtension,
+    AdminUiExtensionWithId,
     Extension,
     GlobalStylesExtension,
     SassVariableOverridesExtension,
@@ -79,7 +80,7 @@ export async function copyStaticAsset(outputPath: string, staticAssetDef: Static
  * If not defined by the user, a deterministic ID is generated
  * from a hash of the extension config.
  */
-export function normalizeExtensions(extensions?: AdminUiExtension[]): Array<Required<AdminUiExtension>> {
+export function normalizeExtensions(extensions?: AdminUiExtension[]): AdminUiExtensionWithId[] {
     return (extensions || []).map(e => {
         let id = e.id;
         if (!id) {