Extending & Customizing
Marketplace Adapter Interface

Marketplace Adapter Interface

Marketplace adapters are the bridge between the CLI and marketplace-specific logic. They provide a standardized interface for loading manifests, recipe books, path keys, and transforming genomes.

Overview

Every marketplace must implement the MarketplaceAdapter interface:

interface MarketplaceAdapter {
  // REQUIRED
  loadManifest(): Promise<MarketplaceManifest> | MarketplaceManifest;
  transformGenome(genome: any, options?, context?): Promise<StandardizedGenome>;
  
  // OPTIONAL (but recommended)
  loadRecipeBook?(): Promise<RecipeBook> | RecipeBook;
  loadPathKeys?(): Promise<MarketplacePathKeys> | MarketplacePathKeys;
  resolvePathDefaults?(context: PathResolutionContext): Promise<PathKeyValueSet> | PathKeyValueSet;
  resolveModule?(moduleId: string): Promise<MarketplaceManifestModule | undefined>;
  getDefaultParameters?(moduleId: string): Record<string, unknown> | undefined;
  validateGenome?(genome: Genome): Promise<void> | void;
  loadTemplate?(moduleId: string, templatePath: string): Promise<string | null>;
}

Required Methods

loadManifest()

Loads the marketplace manifest that describes all available modules.

Returns: MarketplaceManifest

Structure:

{
  version: string;
  modules: {
    capabilities?: MarketplaceManifestModule[];
    features?: MarketplaceManifestModule[];
  };
}

Example:

async loadManifest() {
  const raw = await readFile(manifestPath, 'utf-8');
  return JSON.parse(raw);
}

transformGenome()

Transforms a raw genome (V2Genome, Genome, etc.) into a StandardizedGenome ready for execution.

Parameters:

  • genome: Any genome format (V2Genome, Genome, capability-driven, etc.)
  • options: Transformation options (mode, feature flags, metadata)
  • context: Transformation context (marketplace adapters, logger, project root)

Returns: StandardizedGenome (always same format for CLI)

Example:

async transformGenome(genome, options, context) {
  // Use V2GenomeHandler for V2 genomes
  const handler = new context.V2GenomeHandler(
    context.marketplaceAdapters,
    context.logger,
    context.projectRoot
  );
  
  const resolved = await handler.resolveGenome(genome, context.projectRoot);
  return handler.convertLockFileToResolvedGenome(
    resolved.lockFile,
    genome,
    context.genomeFilePath
  );
}

Optional Methods

loadRecipeBook()

Loads the recipe book that maps business packages to technical modules (V2).

Returns: RecipeBook

Structure:

{
  packages: {
    [packageName: string]: {
      providers: {
        [providerName: string]: {
          modules: Array<{ id: string; ... }>;
          targetPackage?: string;
          targetApps?: string[];
        };
      };
    };
  };
}

Example:

async loadRecipeBook() {
  const raw = await readFile(recipeBookPath, 'utf-8');
  return JSON.parse(raw);
}

loadPathKeys()

Loads the path key schema for the marketplace.

Returns: MarketplacePathKeys

Structure:

{
  keys: {
    [key: string]: {
      description?: string;
      semantic?: boolean;
      resolveToApps?: string[];
      // ... other metadata
    };
  };
}

Example:

async loadPathKeys() {
  const raw = await readFile(pathKeysPath, 'utf-8');
  return JSON.parse(raw);
}

resolvePathDefaults()

Resolves default path values based on the genome.

Parameters:

  • context: PathResolutionContext (genome, apps, packages, etc.)

Returns: PathKeyValueSet (key-value pairs of paths)

Example:

async resolvePathDefaults(context) {
  const paths = {};
  
  // Resolve app paths
  for (const app of context.apps) {
    paths[`apps.${app.type}.root`] = app.package || `apps/${app.type}/`;
  }
  
  // Resolve package paths
  for (const [name, path] of Object.entries(context.packages)) {
    paths[`packages.${name}.root`] = path;
  }
  
  return paths;
}

resolveModule()

Resolves a module entry by ID from the manifest.

Parameters:

  • moduleId: Module ID (e.g., capabilities/nextjs)

Returns: MarketplaceManifestModule | undefined

Example:

async resolveModule(moduleId) {
  await ensureManifestLoaded();
  return moduleIndex?.get(moduleId);
}

getDefaultParameters()

Returns default parameters for a module (merged before execution).

Parameters:

  • moduleId: Module ID

Returns: Record<string, unknown> | undefined

Example:

getDefaultParameters(moduleId) {
  const module = moduleIndex?.get(moduleId);
  return module?.parameters?.defaults;
}

validateGenome()

Validates a genome before transformation.

Parameters:

  • genome: Genome to validate

Throws: Error if genome is invalid

Example:

validateGenome(genome) {
  if (!genome || typeof genome !== 'object') {
    throw new Error('Genome must be an object.');
  }
  if (!genome.project || typeof genome.project !== 'object') {
    throw new Error('Genome must include a project configuration.');
  }
}

loadTemplate()

Loads template content from the marketplace.

Parameters:

  • moduleId: Module ID
  • templatePath: Template file path

Returns: string | null

Example:

async loadTemplate(moduleId, templatePath) {
  const module = await this.resolveModule(moduleId);
  if (!module) return null;
  
  const fullPath = path.join(marketplaceRoot, module.source.root, templatePath);
  return await readFile(fullPath, 'utf-8');
}

Implementation Example

Here's a complete marketplace adapter implementation:

import { readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import path from 'node:path';
 
const manifestPath = fileURLToPath(new URL('../manifest.json', import.meta.url));
const recipeBookPath = fileURLToPath(new URL('../recipe-book.json', import.meta.url));
const pathKeysPath = fileURLToPath(new URL('../path-keys.json', import.meta.url));
const marketplaceRoot = fileURLToPath(new URL('../', import.meta.url));
 
let manifestCache;
let moduleIndex;
let pathKeysCache;
 
async function ensureManifestLoaded() {
  if (manifestCache && moduleIndex) return;
  
  const raw = await readFile(manifestPath, 'utf-8');
  manifestCache = JSON.parse(raw);
  moduleIndex = new Map(
    flattenModules(manifestCache).map(mod => [mod.id, mod])
  );
}
 
export const marketplaceAdapter = {
  async loadManifest() {
    await ensureManifestLoaded();
    return manifestCache;
  },
  
  async loadRecipeBook() {
    const raw = await readFile(recipeBookPath, 'utf-8');
    return JSON.parse(raw);
  },
  
  async loadPathKeys() {
    if (pathKeysCache) return pathKeysCache;
    const raw = await readFile(pathKeysPath, 'utf-8');
    pathKeysCache = JSON.parse(raw);
    return pathKeysCache;
  },
  
  async resolvePathDefaults(context) {
    const paths = {};
    // Resolve paths based on genome
    return paths;
  },
  
  async resolveModule(moduleId) {
    await ensureManifestLoaded();
    return moduleIndex?.get(moduleId);
  },
  
  getDefaultParameters(moduleId) {
    const module = moduleIndex?.get(moduleId);
    return module?.parameters?.defaults;
  },
  
  validateGenome(genome) {
    if (!genome?.project) {
      throw new Error('Genome must include a project configuration.');
    }
  },
  
  async transformGenome(genome, options, context) {
    // Use V2GenomeHandler for transformation
    const handler = new context.V2GenomeHandler(
      context.marketplaceAdapters,
      context.logger,
      context.projectRoot
    );
    
    const resolved = await handler.resolveGenome(genome, context.projectRoot);
    return handler.convertLockFileToResolvedGenome(
      resolved.lockFile,
      genome,
      context.genomeFilePath
    );
  }
};

Marketplace Structure

A marketplace must provide:

  1. manifest.json: Module definitions
  2. recipe-book.json: Package to module mappings (V2)
  3. path-keys.json: Path key definitions
  4. adapter/index.js: Adapter implementation

Creating a Marketplace

Use the CLI command to generate marketplace structure:

architech marketplace generate <name> --type opinionated

This creates:

  • Directory structure (capabilities/, features/, adapter/)
  • package.json with correct exports
  • adapter/index.js with all required methods
  • Build scripts for manifest and recipe book generation

Related