Skip to main content

Adapter System Architecture

Overview

Adapters abstract data source access. The default adapter clones a Git repository, but the adapter pattern allows alternative sources without changing the rest of the system.

Adapter Interface

/**
* Data source adapter interface.
* Implement this to add a new data source (e.g., API, CMS, database).
*/
interface DataAdapter {
/** Unique adapter identifier */
readonly id: string;

/** Human-readable name */
readonly name: string;

/**
* Initialize the data source.
* For GitAdapter: clones the repository.
* For FilesystemAdapter: validates the path exists.
*/
init(config: AdapterConfig): Promise<void>;

/**
* Read a file's raw contents as a string.
* @param relativePath - Path relative to content root (e.g., '.works/works.yml')
*/
readFile(relativePath: string): Promise<string>;

/**
* List all files in a directory.
* @param relativeDir - Directory relative to content root (e.g., 'data')
* @returns Array of file names (not full paths)
*/
listFiles(relativeDir: string): Promise<string[]>;

/**
* List all subdirectories in a directory.
* @param relativeDir - Directory relative to content root (e.g., 'data')
* @returns Array of directory names
*/
listDirectories(relativeDir: string): Promise<string[]>;

/**
* Check if a file or directory exists.
* @param relativePath - Path relative to content root
*/
exists(relativePath: string): Promise<boolean>;

/**
* Get the absolute path to the content root.
* Used by components that need direct filesystem access.
*/
getContentPath(): string;

/**
* Pull latest changes from the remote data source.
* For GitAdapter: git fetch + fast-forward merge.
* For FilesystemAdapter: checks file mtimes for changes.
* @returns `true` if content changed, `false` if already up-to-date
*/
refresh(): Promise<boolean>;

/**
* Get the current HEAD reference for cheap change detection.
* For GitAdapter: returns the current commit SHA.
* For FilesystemAdapter: returns a hash of file mtimes.
* @returns Reference string, or null if unavailable
*/
getHeadRef(): Promise<string | null>;
}

interface AdapterConfig {
/** Git repository URL */
repository?: string;
/** Authentication token */
token?: string;
/** Git branch */
branch?: string;
/** Local filesystem path */
localPath?: string;
/** Clone depth for git (default: 1 for shallow clone) */
cloneDepth?: number;
/** Additional adapter-specific options */
[key: string]: unknown;
}

Built-in Adapters

GitAdapter

The default adapter. Clones a Git repository at build time.

import { GitAdapter } from '@ever-works/adapters';

const adapter = new GitAdapter();
await adapter.init({
repository: process.env.DATA_REPOSITORY,
token: process.env.GH_TOKEN,
branch: process.env.GITHUB_BRANCH || 'main',
});

Implementation details:

  • Uses isomorphic-git for shallow clone (pure JS, no git binary required)
  • Clones into .content/ in the project root
  • Skips clone if .content/.git already exists
  • Supports refresh() for incremental content updates (fetch + fast-forward)

FilesystemAdapter

For local development. Points to a directory on disk.

import { FilesystemAdapter } from '@ever-works/adapters';

const adapter = new FilesystemAdapter();
await adapter.init({
localPath: '/path/to/content',
});

Implementation details:

  • No cloning, no git operations
  • Validates the path exists on init
  • Ideal for development: point to a local copy of the content repo

Adapter Selection

The adapter is selected based on environment variables in the build script:

function createAdapter(): DataAdapter {
if (process.env.CONTENT_PATH) {
// Local development: use filesystem adapter
return new FilesystemAdapter();
}
// Default: use git adapter
return new GitAdapter();
}

Creating a Custom Adapter

See docs/guides/creating-an-adapter.md for the step-by-step guide.

Example: API Adapter (hypothetical)

import type { DataAdapter, AdapterConfig } from '@ever-works/adapters';

export class ApiAdapter implements DataAdapter {
readonly id = 'api';
readonly name = 'API Adapter';
private baseUrl = '';

async init(config: AdapterConfig): Promise<void> {
this.baseUrl = config.repository!; // Reuse repository field for API URL
}

async readFile(relativePath: string): Promise<string> {
const res = await fetch(`${this.baseUrl}/files/${relativePath}`);
return res.text();
}

async listFiles(relativeDir: string): Promise<string[]> {
const res = await fetch(`${this.baseUrl}/list/${relativeDir}`);
return res.json();
}

async listDirectories(relativeDir: string): Promise<string[]> {
const res = await fetch(`${this.baseUrl}/dirs/${relativeDir}`);
return res.json();
}

async exists(relativePath: string): Promise<boolean> {
const res = await fetch(`${this.baseUrl}/exists/${relativePath}`);
return res.ok;
}

getContentPath(): string {
return this.baseUrl;
}

async refresh(): Promise<boolean> {
// Check API for content changes, return true if changed
const res = await fetch(`${this.baseUrl}/changes`);
return res.ok;
}

async getHeadRef(): Promise<string | null> {
const res = await fetch(`${this.baseUrl}/version`);
return res.ok ? res.text() : null;
}
}