Skip to content

ProjectionPlan

ProjectionPlan is a fluent builder API for the common case of composing multiple git sources into a single layered tree. It wraps the lower-level Workspace, Branch, Source, Mapping, and Projection classes into a concise interface.

Basic Usage

const { Repo } = require('hologit');
const ProjectionPlan = require('hologit/lib/ProjectionPlan');

const repo = new Repo({ gitDir: '/path/to/.git', ref: 'HEAD' });

const plan = new ProjectionPlan(repo);
plan.addLayer('base', { url: '/path/to/base', ref: 'refs/heads/main' });
plan.addLayer('overlay', { url: '/path/to/overlay', ref: 'refs/heads/main' }, { after: ['base'] });

const treeHash = await plan.project();

API

new ProjectionPlan(repo)

Create a new plan. Requires a Repo instance for git object storage.

plan.addSource(name, config)

Register a named source. Returns this for chaining.

config shape:

Field Required Description
url yes Git URL or local filesystem path
ref yes Git ref (refs/heads/main, refs/tags/v1.0, commit hash)
project no { holobranch: 'name' } — project through a holobranch in the source

plan.addMapping(sourceName, config)

Add a mapping for a previously registered source. Returns this for chaining.

config shape:

Field Default Description
files ['**'] Glob patterns for which files to include
root '.' Subtree of the source to map from
output '.' Target path in the output tree
layer source name Layer name for ordering
after null Sources/layers this must come after
before null Sources/layers this must come before

plan.addLayer(name, sourceConfig, mappingConfig?)

Convenience method that calls addSource then addMapping. Returns this for chaining.

This is the most common way to build a plan — each layer is a source that maps all its files (**) into the output tree:

plan.addLayer('skeleton', { url: skeletonRepo, ref: 'refs/heads/main' });
plan.addLayer('product', { url: productRepo, ref: 'refs/tags/v3.0' }, { after: ['skeleton'] });
plan.addLayer('site', { url: siteRepo, ref: 'refs/heads/main' }, { after: ['product'] });

plan.project(options?)

Compose all layers and return the git tree hash. This is an async operation.

options:

Field Default Description
lens false Whether to apply hololens transformations
fetch false Whether to fetch remote sources

Layer Ordering

Layers are composed in topological order based on after/before constraints. When two layers contain a file at the same path, the layer that comes later wins.

A typical pattern is to chain layers sequentially:

plan.addLayer('skeleton', { url: skeletonUrl, ref: skeletonRef });
plan.addLayer('product', { url: productUrl, ref: productRef }, { after: ['skeleton'] });
plan.addLayer('site', { url: siteUrl, ref: siteRef }, { after: ['product'] });

This ensures: skeleton files are laid down first, product files overlay them, site files overlay everything.

Additive directories

Files in different directories from different layers coexist naturally. If skeleton provides config/search.config.d/people.ts and product provides config/search.config.d/sections.ts, both files appear in the output. Only files at the same path are overridden.

Full Example: Emergence-style Layer Stack

const { Repo } = require('hologit');
const ProjectionPlan = require('hologit/lib/ProjectionPlan');
const { execSync } = require('child_process');

async function composeEmergenceSite({ gitDir, layers }) {
  const repo = new Repo({ gitDir, ref: 'HEAD' });
  const plan = new ProjectionPlan(repo);

  let prevName = null;
  for (const layer of layers) {
    const mappingConfig = prevName ? { after: [prevName] } : {};
    plan.addLayer(layer.name, {
      url: layer.url,
      ref: layer.ref || 'refs/heads/main'
    }, mappingConfig);
    prevName = layer.name;
  }

  return plan.project();
}

// Usage
const treeHash = await composeEmergenceSite({
  gitDir: '/path/to/object-store',
  layers: [
    { name: 'skeleton', url: 'https://github.com/org/skeleton', ref: 'refs/tags/v2.12.0' },
    { name: 'slate', url: 'https://github.com/org/slate', ref: 'refs/tags/v2.21.0' },
    { name: 'slate-cbl', url: 'https://github.com/org/slate-cbl', ref: 'refs/tags/v3.1.0' },
    { name: 'school-site', url: '/local/school-site', ref: 'refs/heads/main' },
  ]
});

// Check out the composed tree
const git = await (new Repo({ gitDir: '/path/to/object-store', ref: 'HEAD' })).getGit();
await git.readTree(treeHash);
await git.checkoutIndex({ a: true, f: true });

Relationship to Lower-Level API

ProjectionPlan is syntactic sugar over the composition API. Under the hood:

  1. Source configs become phantom Source instances via Workspace({ sources: {...} })
  2. Mapping configs become phantom Mapping instances via Branch({ mappings: {...} })
  3. project() calls Projection.projectBranch() on the constructed branch

Use ProjectionPlan for standard layer composition. Use the lower-level API when you need:

  • Custom root or output paths per mapping
  • Glob filtering (files) to include only specific file patterns from a source
  • Lens transformations
  • Multiple branches in one workspace
  • Direct access to the TreeObject before projection