Skip to content

Documentation · ARDOR

Features ​

Features package domain configurations, react-admin resources, custom routes, and localized messages into self-contained modules mounted by BaseArdorApplication.

tsx
import React from 'react';
import {
  BaseArdorApplication,
  configuration,
  Features,
  provide,
  type IApplicationInfo,
  type IFeatureBase,
  type TFeature,
} from '@venizia/ardor';

export class OrderService {}

@configuration()
export class OrdersConfiguration {
  @provide({ key: 'services.orders' })
  orderService(): OrderService {
    return new OrderService();
  }
}

const OrderList: React.FC = () => <div>Order list</div>;
const OrderAnalyticsPage: React.FC = () => <div>Order analytics</div>;

interface IShellMenuExtension {
  menu: { label: string; path: string };
}

export const ordersFeature = Features.define<IShellMenuExtension>({
  name: 'orders',
  configurations: [OrdersConfiguration],
  resources: [{ name: 'orders', list: OrderList }],
  routes: [{ path: '/orders/analytics', element: <OrderAnalyticsPage /> }],
  messages: {
    en: { orders: { title: 'Orders', analytics: 'Analytics' } },
    vi: () => Promise.resolve({ orders: { title: 'Đơn hàng', analytics: 'Thống kê' } }),
  },
  permissions: ['orders.read', 'orders.export'],
  menu: { label: 'orders.title', path: '/orders' },
});

export class CommerceApplication extends BaseArdorApplication {
  getAppInfo(): IApplicationInfo {
    return { name: 'commerce-admin', version: '1.0.0', description: 'Commerce admin' };
  }

  override features(): Array<IFeatureBase> {
    return [ordersFeature];
  }

  bindContext() {}
}

What it does ​

ExportKindPurpose
FeaturesclassStatic helpers: define() types a feature, read() resolves bound features, mount() merges resources and routes, hasPendingMessages() and loadMessages() read and load the lazy i18n sources
TFeature<TExtension>typeFull feature specification including resources, routes, messages, and optional shell extensions
IFeatureBaseinterfaceContainer-level feature contract (name, configurations, permissions) declared in packages/kernel/src/common/types.ts
TFeatureMessagestypeMap of locale codes to message dictionary objects or asynchronous loader functions
useFeatureshookReads the list of mounted features from the application container within React components
CoreBindings.FEATURESbinding keyBinding key ('@app/application/features') where preConfigure() binds the mounted feature array

Options ​

FieldTypeDefaultMeaning
namestringNone (required)Unique identifier for the feature within an application. Used in resource ownership checks and diagnostic errors.
configurationsArray<TClass<unknown>>undefinedStereotyped @configuration() classes belonging to the feature. Listing them by value prevents bundlers from dropping unreferenced modules under sideEffects: false and enables discovery checks during startup. Handled as feature.configurations ?? [] in packages/kernel/src/base/applications/abstract.ts.
resourcesArray<ResourceProps>undefinedReact-admin <Resource> definitions mounted into the application view tree. Handled as feature.resources ?? [] in packages/admin/src/features/feature.ts.
routesArray<RouteProps>undefinedCustom <Route> elements mounted under react-admin's <CustomRoutes>. Handled as feature.routes ?? [] in packages/admin/src/features/feature.ts.
messagesTFeatureMessagesundefinedDictionary mapping locale codes to message objects or lazy loader functions returning Promise<object>. Handled as feature.messages?.[locale] in packages/admin/src/features/messages.ts.
permissionsArray<string>undefinedPermission keys checked by the feature in the application's vocabulary. Preserved on the feature for consumers such as role editors; ARDOR enforces nothing. See Permission.
[extension]TExtensionNoneCustom shell metadata (such as menu entries, navigation paths, or icons) preserved by Features.define<TExtension>(). ARDOR carries the extension and never reads it.

Mount order and conflict checks ​

ArdorApplication calls Features.mount({ features, resources, routes }) to assemble the UI tree:

  1. The application's own resources and routes passed to <ArdorApplication> are mounted first.
  2. Each feature's resources and routes are mounted next, in the order listed in features().

ARDOR enforces isolation during startup and mount:

  • Duplicate feature name (start): If two features share a name, registerFeatures() in packages/kernel/src/base/applications/abstract.ts throws: [features] Two features are named '${feature.name}' | A feature name is unique within an application
  • Undiscovered configuration (start): If a class in configurations was not discovered by the metadata registry, registerFeatures() in packages/kernel/src/base/applications/abstract.ts throws: [features] '${feature.name}' lists ${target.name}, which was never discovered | Decorate it with @configuration() and import its module before start()
  • Duplicate resource name (mount): If a resource name is mounted more than once, Features.mount() in packages/admin/src/features/feature.ts throws: [features] Resource '${resource.name}' is mounted by both '${owner}' and '${entry.owner}' | A resource name is unique within an application

Mount a filtered list ​

ArdorApplication mounts every bound feature unless you pass features. A shell that serves an industry, a plan or a role filters Features.read with its own rule and passes the result:

tsx
import React from 'react';
import { type Store } from '@reduxjs/toolkit';
import {
  ArdorApplication,
  type BaseArdorApplication,
  Features,
  type TFeature,
} from '@venizia/ardor';

interface IIndustryExtension {
  industries: Array<string>;
}

export const IndustryShell: React.FC<{
  application: BaseArdorApplication;
  reduxStore: Store;
  industry: string;
}> = ({ application, reduxStore, industry }) => {
  const features = React.useMemo(() => {
    return Features.read<TFeature<IIndustryExtension>>({ container: application }).filter(
      (feature) => feature.industries.includes(industry),
    );
  }, [application, industry]);

  // A new industry is a new application: the key remounts it.
  return (
    <ArdorApplication
      key={industry}
      container={application}
      reduxStore={reduxStore}
      suspense={<div>Loading...</div>}
      features={features}
    />
  );
};
  • The rule is yours. ARDOR reads no context and has no predicate. industries is the shell's own extension, declared with Features.define<IIndustryExtension>; ARDOR carries it and never reads it.
  • The list is read as given. Pass the same array until the rule's input changes. ARDOR does not re-read it for a new workspace: remount the application with a key when the industry, plan or merchant changes. That remount is the app's job.
  • Left out is not unbound. A feature missing from features mounts no resource and no route, but the container still holds its configurations, and the i18n provider still holds its messages. A message key two features define still throws, even if one of them is not mounted.

Message loading and suspense ​

Feature messages are merged per locale by FeatureMessages in packages/admin/src/features/messages.ts. Message keys remain flat without an artificial prefix.

  • Duplicate message keys: If two features define the same leaf key for a locale, ARDOR throws: [features] Message '${key}' (${locale}) is defined by both '${owner}' and '${feature}' | Give each feature its own keys, or relabel it in the application's own i18nSources
  • Application overrides: Application dictionaries in i18nSources override feature messages. See Internationalization.
  • Lazy loading and suspense: A feature can provide messages synchronously as an object or asynchronously through a loader function returning Promise<object>. For the initial locale, ArdorApplication displays the configured suspense fallback until the loader resolves.

Reading features with useFeatures ​

The useFeatures() hook retrieves the array of features mounted in the container. If CoreBindings.FEATURES is not bound, it returns an empty array.

tsx
import React from 'react';
import { type TFeature, useFeatures } from '@venizia/ardor';

interface IMenuExtension {
  menu?: { label: string; path: string };
}

export const FeatureNavigationMenu: React.FC = () => {
  const features = useFeatures<TFeature<IMenuExtension>>();

  return (
    <nav>
      <ul>
        {features.map((feature) =>
          feature.menu ? (
            <li key={feature.name}>
              <a href={feature.menu.path}>{feature.menu.label}</a>
            </li>
          ) : null,
        )}
      </ul>
    </nav>
  );
};
  • Application - Application lifecycle, container setup, and preConfigure() binding order
  • Binding keys - Container keys in CoreBindings, including CoreBindings.FEATURES
  • Permission - Permission vocabulary declared on features
  • Internationalization - Layering application dictionaries over feature messages