Module Linking

Module Linking is Esmx's cross-application code sharing mechanism, based on ECMAScript module standards, achieving micro-frontend architecture with zero runtime overhead.

Why Module Linking?

In micro-frontend architectures, multiple independent applications often need to use the same third-party libraries (such as HTTP clients, utility libraries, UI component libraries) and shared components. Traditional approaches have the following issues:

  • Resource Duplication: Each application bundles the same dependencies independently, causing users to download duplicate code
  • Version Inconsistency: Different applications using different versions of the same library may cause compatibility issues
  • Memory Waste: Multiple instances of the same library exist in the browser, occupying additional memory
  • Cache Invalidation: Different bundled versions of the same library cannot share browser cache

Module Linking solves these problems through the ECMAScript module system and Import Maps specification, enabling multiple applications to safely and efficiently share code modules.

How It Works

Module Linking is based on technology standards natively supported by modern browsers:

Shared Module Provider

One application acts as a module provider, responsible for building and exposing shared third-party libraries and components. Other applications act as module consumers, using these shared modules through standard ESM import syntax.

Import Maps Resolution

Browsers use Import Maps to map module import statements to actual file paths:

// Import statements in application code
import axios from 'axios';  // Mapped via scopes configuration
import { formatDate } from 'shared-lib/src/utils/date-utils';  // Mapped via imports configuration

// Import Maps resolves them to
import axios from 'shared-lib/axios.389c4cab.final.mjs';
import { formatDate } from 'shared-lib/src/utils/date-utils.2d79c0c2.final.mjs';

Module Instance Sharing

All applications share the same module instance, ensuring:

  • Global state consistency (such as library global configuration)
  • Unified event system (such as global event bus)
  • Memory usage optimization (avoiding duplicate instantiation)

Quick Start

Basic Example

Assume we have a shared library application (shared-lib) and a business application (business-app):

// shared-lib/entry.node.ts - Provide shared modules
export default {
  modules: {
    exports: [
      'npm:axios',                    // Share HTTP client
      'root:src/utils/format.ts'      // Share utility functions
    ]
  }
} satisfies EsmxOptions;

// business-app/entry.node.ts - Use shared modules
export default {
  modules: {
    links: { 'shared-lib': '../shared-lib/dist' },
    imports: { 'axios': 'shared-lib/axios' }
  }
} satisfies EsmxOptions;

Using in business application:

// business-app/src/api/orders.ts
import axios from 'axios';  // Use shared axios instance
import { formatDate } from 'shared-lib/src/utils/format';  // Use shared utility functions

export async function fetchOrders() {
  const response = await axios.get('/api/orders');
  return response.data.map(order => ({
    ...order,
    date: formatDate(order.createdAt)
  }));
}

Configuration Guide

Module Linking configuration is located in the modules field of the entry.node.ts file, containing three core configuration items:

Basic Configuration

Module Export

exports configuration defines content that modules expose externally, supporting two prefixes:

Prefix Note: npm: and root: prefixes are syntactic sugar for configuration simplification, only valid in string items of exports array form. They automatically apply best practice configurations to simplify common use cases.

// shared-lib/entry.node.ts
import type { EsmxOptions } from '@esmx/core';

export default {
  // Other configurations...
  modules: {
    exports: [
      // npm packages: maintain original import paths
      'npm:axios',                    // Import: import axios from 'axios'
      'npm:lodash',                   // Import: import { debounce } from 'lodash'
      
      // Source modules: automatically rewrite to module paths
      'root:src/utils/date-utils.ts',     // Import: import { formatDate } from 'shared-lib/src/utils/date-utils'
      'root:src/components/Chart.js'      // Import: import Chart from 'shared-lib/src/components/Chart'
    ]
  }
} satisfies EsmxOptions;

Prefix Processing:

  • npm:axios → Equivalent to { 'axios': { input: 'axios', rewrite: false } }
  • root:src/utils/date-utils.ts → Equivalent to { 'src/utils/date-utils': { input: './src/utils/date-utils', rewrite: true } }

Module Linking

links configuration specifies paths where the current module links to other modules:

// business-app/entry.node.ts
import type { EsmxOptions } from '@esmx/core';

export default {
  // Other configurations...
  modules: {
    links: {
      'shared-lib': '../shared-lib/dist',     // Relative path
      'api-utils': '/var/www/api-utils/dist'  // Absolute path
    }
  }
} satisfies EsmxOptions;

Module Import

imports configuration maps local module names to remote module identifiers, mainly used for standard imports of third-party libraries:

// business-app/entry.node.ts
import type { EsmxOptions } from '@esmx/core';

export default {
  modules: {
    links: {
      'shared-lib': '../shared-lib/dist'
    },
    imports: {
      // Third-party library standard import mapping
      'axios': 'shared-lib/axios',
      'lodash': 'shared-lib/lodash'
    }
  }
} satisfies EsmxOptions;

// Usage
import axios from 'axios';  // Correct - use standard library name
import { debounce } from 'lodash';  // Correct - use standard library name

// Custom module import
import { formatDate } from 'shared-lib/src/utils/date-utils';  // Correct - directly use link path

Advanced Configuration

Advanced exports Configuration

exports supports multiple configuration forms. When complex configurations (such as entryPoints) are needed, prefix syntactic sugar cannot satisfy requirements, and complete object form is needed:

Array Form:

// shared-lib/entry.node.ts
export default {
  modules: {
    exports: [
      // String form - using prefix syntactic sugar
      'npm:axios',                    // Export npm package
      'root:src/utils/format.ts',     // Export source file
      
      // Object form - complex configurations cannot use prefixes
      {
        'api': './src/api.ts',        // Simple mapping
        'store': {                    // Complete configuration
          input: './src/store.ts',
          rewrite: true
        }
      }
    ]
  }
} satisfies EsmxOptions;

Object Form:

// shared-lib/entry.node.ts
export default {
  modules: {
    exports: {
      // Simple mapping
      'axios': 'axios',            // Directly specify npm package name
      
      // Complete configuration object
      'src/utils/format': {
        input: './src/utils/format',  // Input file path
        rewrite: true,                // Whether to rewrite import paths (default true)
        entryPoints: {                // Client/server differentiated builds
          client: './src/utils/format.client',  // Client-specific version
          server: './src/utils/format.server'   // Server-specific version
        }
      }
    }
  }
} satisfies EsmxOptions;

entryPoints Environment Differentiated Builds

exports: {
  'src/storage/db': {
    entryPoints: {
      client: './src/storage/indexedDB',  // Client uses IndexedDB
      server: './src/storage/mongoAdapter' // Server uses MongoDB adapter
    }
  }
}

Setting false can disable builds for specific environments:

exports: {
  'src/client-only': {
    entryPoints: {
      client: './src/client-feature',  // Only available on client
      server: false                    // Not available on server
    }
  }
}

Best Practices

Use Case Evaluation

Suitable Cases:

  • Multiple applications using the same third-party libraries (axios, lodash, moment, etc.)
  • Need to share business component libraries or utility functions
  • Want to reduce application size and improve loading performance
  • Need to ensure library version consistency across multiple applications

Not Suitable Cases:

  • Single application projects (no sharing needs)
  • Frequently changing experimental code
  • Scenarios requiring extremely low coupling between applications

Import Guidelines

Third-party Library Imports:

// ✅ Recommended - Use standard library names, conforming to ecosystem standards
import axios from 'axios';
import { debounce } from 'lodash';

// ❌ Not Recommended - Violates module ecosystem standards, not conducive to library replacement and maintenance
import axios from 'shared-lib/axios';

Custom Module Imports:

// ✅ Recommended - Directly use module linking paths, clearly indicating dependency source
import { formatDate } from 'shared-lib/src/utils/date-utils';

// ❌ Invalid Configuration - imports does not support path prefix functionality
imports: { 'components': 'shared-lib/src/components' }
import Chart from 'components/Chart';  // This import cannot be resolved

Configuration Principles

  1. Third-party Libraries: Must configure imports mapping, use standard names for importing
  2. Custom Modules: Directly use complete module linking paths
  3. imports Purpose: Only for third-party library standard name mapping, not directory aliases