Skip to content

Extending Tempo with Plugins ​

Tempo is designed with a "lean core" philosophy. While it provides robust date-time manipulation and parsing out of the box, advanced functionality (like reactive Tickers or domain-specific business logic) is added through a flexible Plugin System.

In the Tempo ecosystem, a Plugin is the universal overarching term for any feature added to the core library. To make authoring plugins easy and consistent, Tempo provides two specialized factory functions:

  1. definePlugin: The standard factory for general-purpose features (e.g., adding prototype instance methods, static tools, or altering configuration).
  2. defineTerm: A specialized factory exclusively for defining temporal vocabulary constraints (a "Term" is technically just a highly-opinionated "Plugin" focused on date ranges and schedules).
  3. defineNamespace: A factory for creating lazily-evaluated property landing pads (e.g., Tempo().finance.taxYear).

Naming Convention Standard ​

To provide a consistent and intuitive developer experience, the exported symbol of your plugin should use a suffix that directly matches the factory used to construct it. This makes it instantly obvious to consumers how the extension will attach to the Tempo core:

  • Built with definePlugin ➡️ [Name]Plugin (e.g., TickerPlugin)
  • Built with defineTerm ➡️ [Name]Term (e.g., FiscalTerm)
  • Built with defineNamespace ➡️ [Name]Namespace (e.g., FinanceNamespace)

(Note: The Module suffix and defineModule factory are strictly reserved for Tempo's core internal injection APIs like ParseModule and should not be used by external plugins.)

To register a plugin, choose between explicit registration (Tempo.use) or zero-boilerplate side-effect imports (/install):

typescript
// 1. Explicit registration (Recommended for production apps & tree-shaking)
import { Tempo } from '@magmacomputing/tempo/core';
import { MyPlugin } from './my-plugin.js';
import { HolidayPlugin } from './my-holiday-plugin.js';

Tempo.use(MyPlugin);
Tempo.use(HolidayPlugin({ region: 'US-NY' }));

// 2. Zero-boilerplate side-effect registration (Recommended for scripts & REPLs)
import '@magmacomputing/tempo-plugin-geo/install';
import '@magmacomputing/tempo-plugin-ai/install';

1. Creating a Custom Plugin ​

Tempo provides a dedicated step-by-step guide for developers wishing to author their own general-purpose plugins.

👉 Read the Custom Plugin Guide (tempo.extension.md) to learn how to:

  • Use the definePlugin factory
  • Safely extend the TempoClass.prototype
  • Build a full BusinessDaysPlugin from scratch

Type Safety (TypeScript) ​

To ensure your plugin is discoverable by the IDE, use Module Augmentation to extend the Tempo namespace and the Tempo class interface.

typescript
declare module '@magmacomputing/tempo/core' {
  namespace Tempo {
    // 1. Define new types/interfaces here
    interface HolidayOptions { ... }

    // 2. Add static methods to the Tempo namespace
    function myStaticMethod(): void;
  }

  interface Tempo {
    // 3. Add instance methods to the Tempo class
    toHoliday(): Tempo;
  }
}

Understanding Tempo Versions:

  • @magmacomputing/tempo/core (Lite): A bare-bones engine with zero side-effects. This is the recommended choice for production builds and plugin authoring.
  • @magmacomputing/tempo (Full): The "Batteries Included" version which automatically imports and registers all standard modules.

Avoid Circular Dependencies: When authoring a plugin, never import the Tempo class directly from the Full version (@magmacomputing/tempo). Doing so triggers the library's automatic registration sequence in a recursive loop, which will break your application's initialization.

Instead:

  1. Use types: import type { Tempo } from '@magmacomputing/tempo/core'.
  2. Use the argument: Rely on the TempoClass argument passed into your plugin function for static method access.
  3. Use the engine: If you need a class reference (e.g., for instanceof checks), import only from the Lite engine (@magmacomputing/tempo/core).

Modern Tempo plugins are designed to be "plug-and-play." By using the definePlugin factory, a plugin registers itself with the global Tempo registry as soon as it's imported.

typescript
import { Tempo } from '@magmacomputing/tempo/core';   // 1. Load the `lite` engine
import { TickerPlugin } from '@magmacomputing/tempo-plugin-ticker'; // 2. Import the plugin

Tempo.init({ 
  plugins: [TickerPlugin]                             // 3. Register and activate plugin during init
});

// Ticker is now available on the core Tempo class!
const pulse = Tempo.ticker(1);

Dynamic Extension vs Startup Registration

Tempo.init({ plugins: [...] }) establishes baseline configuration at startup and performs initial registration of plugins. In full Tempo (@magmacomputing/tempo), standard plugins are registered automatically upon import. To dynamically register custom plugins loaded later at runtime, use Tempo.use(Plugin) directly rather than re-running Tempo.init().


Best Practices ​

1. Selective Immobility ​

The core methods of Tempo (like add, set, format) are protected. The use() system will prevent you from accidentally overwriting these essential behaviors. By standardizing plugins through the Tempo module system, the entire library remains small and fast, while offering unbounded domain-specific customization.

2. Immutability ​

When adding instance methods that "modify" the date, always follow the Tempo pattern of returning a new instance. Do not mutate this. Rely on the core methods (like this.add()) inside your plugin, as they automatically guarantee a fresh, cloned instance.

3. Namespace Respect ​

If your plugin provides many related methods, consider grouping them under a single namespace property on the instance (e.g., tempo.holiday.isPublic() rather than tempo.isPublicHoliday()). This keeps the root Tempo interface clean and minimizes the risk of naming collisions.

Instead of manually building these namespaces on the prototype, Tempo provides the defineNamespace factory to automate lazy-evaluation. 👉 Read the Namespace Guide to learn more.

4. Error Handling & The Diagnostic Engine ​

When building plugins that perform complex parsing or logic, follow Tempo's "Fail-fast by Default" principle.

  • Strict Mode (Default): If your plugin encounters a terminal error (e.g., invalid input that cannot be recovered), you should throw a descriptive error.
  • Catch Mode: Respect the user's catch configuration. If this.config.catch is true, instead of throwing, you should log a warning using this.warn() and return a sensible fallback (or the original input).
  • Configuration Dependencies: You are responsible for managing missing configuration keys that your plugin depends on. The core engine will not validate your plugin's specific requirements. If a required config key is missing (e.g., sphere for a Season plugin), either provide a reasonable default fallback value or warn the user explicitly using this.warn(). Do not make assumptions that lead to silent failures.
typescript
// Example within a plugin instance method
if (errorCondition) {
  const msg = `Custom Error: ${details}`;
  if (this.config.catch === true) {
    this.warn(msg);
    return this; // or a fallback value
  }
  throw new Error(msg);
}

This pattern ensures that Tempo remains robust in production environments while providing strict validation during development.

5. Unified Logging Hooks (logWarn, logError, logDebug) ​

Plugins should avoid invoking raw console.warn, console.error, or console.log directly. Instead, import and use the standard logging hooks exported by @magmacomputing/tempo/plugin/sdk:

typescript
import { logWarn, logError, logDebug } from '@magmacomputing/tempo/plugin/sdk';

// Log operational warnings or errors while passing instance config
logWarn("MyPlugin: Missing optional coordinate parameters.", this?.config);
logError(err, this?.config);

Passing the instance configuration allows Tempo's logging subsystem to automatically respect { silent: true }, custom logger delegates, and debug verbosity levels. Additionally, logError(err, this?.config) automatically throws the error when config.catch is false (the default) and swallows it gracefully when catch: true, eliminating the need for manual if (!config.catch) throw checks.

Alternative: Standalone Functions (tempo-fns) ​

The JavaScript ecosystem is divided between two architectural preferences: Chained Fluent APIs (like Tempo Plugins) and Pure Standalone Functions (for aggressive tree-shaking).

To support teams that mandate strict 0kb bundle-impacts and functional programming paradigms, Magma Computing provides the @magmacomputing/tempo-fns library.

typescript
// The Pure, Tree-shakeable approach:
import { isFirstDayOfMonth } from '@magmacomputing/tempo-fns';
import { Tempo } from '@magmacomputing/tempo/core';

if (isFirstDayOfMonth(new Tempo('2024-01-01'))) { ... }

When building complex logic, consider whether it belongs as a core Plugin extension, or as a standalone utility in tempo-fns (or a hybrid wrapper of both!).

Distributing Your Plugin ​

To make your plugin available to the community, package it as a standard NPM module.

Plugin Factories (with Options) ​

If your plugin requires its own configuration, use definePlugin directly and export the result as a callable PluginFactory. Consumers can then invoke it at registration time with their options, following the Vite / Rollup colocated plugin model.

typescript
// tempo-plugin-holiday/index.ts
import { definePlugin } from '@magmacomputing/tempo/plugin/sdk';
import type { TempoPlugin } from '@magmacomputing/tempo/plugin/sdk';

export interface HolidayPluginOptions {
  region?: string;
  observeWeekendShifts?: boolean;
}

// Export the options type so consumers get IDE intellisense inside HolidayPlugin({ ... })
export const HolidayPlugin = definePlugin({
  name: 'holiday',
  install(TempoClass, options?: any) {
    const opts = options as HolidayPluginOptions | undefined;
    const region = opts?.region ?? 'US-NY';
    // ... register methods or terms using the resolved options ...
  },
});

Consumers can then register it using any of the three patterns:

typescript
plugins: [
  HolidayPlugin,                            // Pattern 0: bare (uses pluginOptions fallback)
  HolidayPlugin({ region: 'AU-NSW' }),      // Pattern 1: factory closure
  [HolidayPlugin, { region: 'AU-NSW' }],   // Pattern 2: tuple
]

The Module Aggregator Pattern ​

If your plugin provides multiple related components, wrap them in an aggregator module to provide a uniform activation experience for the user.

typescript
// index.ts
import { definePlugin } from '@magmacomputing/tempo/plugin/sdk';
import { PluginA } from './plugin.a.js';
import { PluginB } from './plugin.b.js';

export const MyFeaturePlugin = definePlugin((TempoClass, options) => {
  TempoClass.use([PluginA, PluginB]);
});

Commercial & Enterprise Extensions ​

If you require custom commercial plugins, domain-specific extensions, or enterprise-grade features with dedicated support, see our Commercial & Professional Services guide.


Consuming a Plugin ​

For developers using your plugin, registration can be handled imperatively via use() or declaratively via configuration. As of v4.3.0, Tempo supports three registration patterns:

PatternSyntaxDescription
0 — BareTickerPluginDefault options; reads from pluginOptions fallback
1 — Factory ClosureTickerPlugin({ interval: 1000 })Colocated options injected at call site
2 — Tuple[TickerPlugin, { interval: 1000 }]Works with any plugin, including third-party

1. Imperative Registration (Tempo.use) ​

For standalone usage, pass the plugin directly to Tempo.use() using any pattern:

typescript
import { Tempo } from '@magmacomputing/tempo';
import { TickerPlugin } from '@magmacomputing/tempo-plugin-ticker';
import { HolidayPlugin } from '@magmacomputing/tempo-plugin-holidays';
import { CelestialPlugin } from '@magmacomputing/tempo-plugin-celestial';
import { GeoPlugin } from '@magmacomputing/tempo-plugin-geo';

// Pattern 0: bare
Tempo.use(TickerPlugin);

// Pattern 1: factory closure — options colocated at the call site
Tempo.use(TickerPlugin({ interval: 1000 }));

// Pattern 2: tuple — pairs any plugin with its options
Tempo.use([HolidayPlugin, { region: 'US-NY' }]);

// Mix and match in a single call:
Tempo.use(
  CelestialPlugin,
  TickerPlugin({ interval: 500 }),
  [GeoPlugin, { timeout: 5000 }],
);

2. Declarative Configuration (defineConfig / Tempo.init) ​

All three patterns are supported directly in the plugins array of tempo.config.ts or Tempo.init():

typescript
import { TickerPlugin } from '@magmacomputing/tempo-plugin-ticker';
import { GeoPlugin } from '@magmacomputing/tempo-plugin-geo';
import { CelestialPlugin } from '@magmacomputing/tempo-plugin-celestial';

Tempo.init({
  plugins: [
    CelestialPlugin,                       // Pattern 0: bare
    TickerPlugin({ interval: 1000 }),      // Pattern 1: factory closure
    [GeoPlugin, { timeout: 5000 }],        // Pattern 2: tuple
  ],
});

Options Precedence (lowest → highest):

  1. pluginOptions[name] from config (base/shared fallback)
  2. Trailing shared options passed to Tempo.use(PluginA, PluginB, sharedOpts)
  3. Call-site colocated options (Pattern 1 or Pattern 2)

3. pluginOptions — Legacy / Shared Fallback ​

pluginOptions remains fully supported as a passive configuration registry. It is the right tool for:

  • Bare-registered plugins that read their config from TempoClass.config.pluginOptions
  • Lazy / dynamically imported plugins that aren't in the main bundle
  • Enterprise base configs shared via extends across microservices
typescript
Tempo.init({
  plugins: [HolidayPlugin, TickerPlugin],  // bare (Pattern 0)
  pluginOptions: {
    holiday: { region: 'US-NY', observeWeekendShifts: true },
    ticker: { interval: 500 },
    ai: { mode: 'fallback', timeout: 10000 },  // safe even if 'ai' plugin not imported
  },
});

Deprecation Notice: Configuration Dictionaries in `plugins`

Supplying plain configuration dictionaries directly inside the plugins array (e.g. plugins: [{ holiday: { ... } }]) remains supported for backwards compatibility, but is marked @deprecated in favor of pluginOptions or the new colocated option patterns.


Bulk Registration ​

Tempo.use() supports rest parameters and arrays, allowing you to register multiple plugins in a single call. If the last argument is a plain object (and not a plugin/term), it is treated as a shared configuration for all plugins in that batch.

ts
// Mix and match arrays and individual arguments
Tempo.use(
  [PluginA, PluginB], 
  PluginC, 
  { debug: 5 } // applied to A, B, and C
);

🤝 Need Help Writing a Plugin? ​

If you have a complex business requirement or need a high-performance plugin built to professional standards, we can help. Our team can design, implement, and verify custom Tempo plugins tailored to your specific domain.

Contact Magma Computing Solutions to discuss your requirements.

Released under the MIT License.