or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

file-system-actions.mdindex.mdnode-tools.mdoutput-sinks.mdrule-system.mdschematic-engine.mdtemplate-engine.mdtree-operations.mdworkflow.md
tile.json

output-sinks.mddocs/

Output Sinks

Output destination system for committing tree changes to various targets including filesystem and dry-run reporting. Sinks provide the bridge between the virtual tree operations and actual file system modifications.

Capabilities

Sink Interface

Core interface for committing tree changes to output destinations.

/**
 * Interface for committing tree changes to output destinations
 */
interface Sink {
  /** Commit all tree changes to the target destination */
  commit(tree: Tree): Observable<void>;
}

Simple Sink Base Class

Abstract base class providing common sink functionality with lifecycle hooks.

/**
 * Abstract base class for sink implementations with lifecycle hooks
 */
abstract class SimpleSinkBase implements Sink {
  /** Hook called before committing an individual action */
  protected preCommitAction?: (action: Action) => 
    void | Action | PromiseLike<Action> | Observable<Action>;
    
  /** Hook called after committing an individual action */
  protected postCommitAction?: (action: Action) => 
    void | Observable<void>;
    
  /** Hook called before starting the commit process */
  protected preCommit?: () => void | Observable<void>;
  
  /** Hook called after completing the commit process */
  protected postCommit?: () => void | Observable<void>;
  
  // Abstract methods that must be implemented by subclasses
  protected abstract _validateFileExists(path: string): Observable<boolean>;
  protected abstract _overwriteFile(path: string, content: Buffer): Observable<void>;
  protected abstract _createFile(path: string, content: Buffer): Observable<void>;
  protected abstract _renameFile(from: string, to: string): Observable<void>;
  protected abstract _deleteFile(path: string): Observable<void>;
  protected abstract _done(): Observable<void>;
  
  /** Main commit implementation */
  commit(tree: Tree): Observable<void>;
}

Host Sink

Sink implementation that writes changes to a virtual file system host.

/**
 * Sink that writes changes to a virtual file system host
 */
class HostSink extends SimpleSinkBase {
  /**
   * Create a host sink
   * @param host - Virtual file system host to write to
   * @param force - Whether to overwrite existing files without confirmation
   */
  constructor(host: virtualFs.Host, force?: boolean);
  
  protected _validateFileExists(path: string): Observable<boolean>;
  protected _overwriteFile(path: string, content: Buffer): Observable<void>;
  protected _createFile(path: string, content: Buffer): Observable<void>;
  protected _renameFile(from: string, to: string): Observable<void>;
  protected _deleteFile(path: string): Observable<void>;
  protected _done(): Observable<void>;
}

Usage Examples:

import { HostSink } from "@angular-devkit/schematics";
import { NodeJsSyncHost } from "@angular-devkit/core/node";

// Create a sink that writes to the actual file system
function commitToFileSystem(tree: Tree): Observable<void> {
  const host = new NodeJsSyncHost();
  const sink = new HostSink(host, false); // force = false
  
  return sink.commit(tree);
}

// Create a sink with force mode enabled
function forceCommitToFileSystem(tree: Tree): Observable<void> {
  const host = new NodeJsSyncHost();
  const sink = new HostSink(host, true); // force = true
  
  return sink.commit(tree);
}

Dry Run Sink

Sink implementation for dry-run operations that reports changes without executing them.

/**
 * Sink for dry-run operations that reports changes without executing them
 */
class DryRunSink extends SimpleSinkBase {
  /** Observable stream of dry run events */
  readonly reporter: Observable<DryRunEvent>;
  
  /**
   * Create a dry run sink
   * @param host - Virtual file system host or root path string
   * @param force - Whether to simulate forced operations
   */
  constructor(host: virtualFs.Host | string, force?: boolean);
  
  protected _validateFileExists(path: string): Observable<boolean>;
  protected _overwriteFile(path: string, content: Buffer): Observable<void>;
  protected _createFile(path: string, content: Buffer): Observable<void>;
  protected _renameFile(from: string, to: string): Observable<void>;
  protected _deleteFile(path: string): Observable<void>;
  protected _done(): Observable<void>;
}

Dry Run Events

Event types emitted by the dry run sink to report what would happen.

/**
 * Base interface for dry run events
 */
interface DryRunEvent {
  kind: 'error' | 'update' | 'create' | 'delete' | 'rename';
  path: Path;
  description: string;
}

/**
 * Error event during dry run
 */
interface DryRunErrorEvent extends DryRunEvent {
  kind: 'error';
  description: string;
  error: Error;
}

/**
 * File update event during dry run
 */
interface DryRunUpdateEvent extends DryRunEvent {
  kind: 'update';
  content: Buffer;
}

/**
 * File creation event during dry run
 */
interface DryRunCreateEvent extends DryRunEvent {
  kind: 'create';
  content: Buffer;
}

/**
 * File deletion event during dry run
 */
interface DryRunDeleteEvent extends DryRunEvent {
  kind: 'delete';
}

/**
 * File rename event during dry run
 */
interface DryRunRenameEvent extends DryRunEvent {
  kind: 'rename';
  to: Path;
}

type DryRunEvent = 
  | DryRunErrorEvent 
  | DryRunUpdateEvent 
  | DryRunCreateEvent 
  | DryRunDeleteEvent 
  | DryRunRenameEvent;

Usage Examples:

import { DryRunSink, DryRunEvent } from "@angular-devkit/schematics";

// Perform dry run and log all events
function performDryRun(tree: Tree, rootPath: string): Promise<void> {
  const sink = new DryRunSink(rootPath, false);
  
  // Subscribe to dry run events
  sink.reporter.subscribe((event: DryRunEvent) => {
    switch (event.kind) {
      case 'create':
        console.log(`Would create: ${event.path}`);
        break;
      case 'update':
        console.log(`Would update: ${event.path}`);
        break;
      case 'delete':
        console.log(`Would delete: ${event.path}`);
        break;
      case 'rename':
        console.log(`Would rename: ${event.path} -> ${event.to}`);
        break;
      case 'error':
        console.error(`Error: ${event.description}`, event.error);
        break;
    }
  });
  
  return sink.commit(tree).toPromise();
}

// Collect dry run results
function collectDryRunResults(tree: Tree, rootPath: string): Promise<DryRunEvent[]> {
  const sink = new DryRunSink(rootPath);
  const events: DryRunEvent[] = [];
  
  sink.reporter.subscribe(event => events.push(event));
  
  return sink.commit(tree).toPromise().then(() => events);
}

Custom Sink Implementation

Example of creating a custom sink implementation.

/**
 * Example custom sink that logs all operations
 */
class LoggingSink extends SimpleSinkBase {
  constructor(private logger: logging.LoggerApi) {
    super();
  }
  
  protected _validateFileExists(path: string): Observable<boolean> {
    this.logger.info(`Checking if file exists: ${path}`);
    // Custom validation logic
    return of(false);
  }
  
  protected _overwriteFile(path: string, content: Buffer): Observable<void> {
    this.logger.info(`Would overwrite file: ${path} (${content.length} bytes)`);
    return of(undefined);
  }
  
  protected _createFile(path: string, content: Buffer): Observable<void> {
    this.logger.info(`Would create file: ${path} (${content.length} bytes)`);
    return of(undefined);
  }
  
  protected _renameFile(from: string, to: string): Observable<void> {
    this.logger.info(`Would rename file: ${from} -> ${to}`);
    return of(undefined);
  }
  
  protected _deleteFile(path: string): Observable<void> {
    this.logger.info(`Would delete file: ${path}`);
    return of(undefined);
  }
  
  protected _done(): Observable<void> {
    this.logger.info('Sink operations completed');
    return of(undefined);
  }
}

Advanced Sink Usage

Sink with Custom Lifecycle Hooks:

import { HostSink, Action } from "@angular-devkit/schematics";

class CustomHostSink extends HostSink {
  constructor(host: virtualFs.Host, force: boolean = false) {
    super(host, force);
    
    // Set up lifecycle hooks
    this.preCommit = () => {
      console.log('Starting commit process...');
    };
    
    this.postCommit = () => {
      console.log('Commit process completed!');
    };
    
    this.preCommitAction = (action: Action) => {
      console.log(`About to execute action: ${action.kind} ${action.path}`);
      return action; // Can modify or replace the action
    };
    
    this.postCommitAction = (action: Action) => {
      console.log(`Completed action: ${action.kind} ${action.path}`);
    };
  }
}

Conditional Sink Selection:

import { Sink, HostSink, DryRunSink } from "@angular-devkit/schematics";

function createSink(options: { dryRun: boolean; force: boolean; host: virtualFs.Host }): Sink {
  if (options.dryRun) {
    return new DryRunSink(options.host, options.force);
  } else {
    return new HostSink(options.host, options.force);
  }
}

// Usage in schematic
function commitWithOptions(tree: Tree, options: any): Observable<void> {
  const sink = createSink({
    dryRun: options.dryRun,
    force: options.force,
    host: new NodeJsSyncHost()
  });
  
  return sink.commit(tree);
}

Error Handling with Sinks:

import { HostSink } from "@angular-devkit/schematics";
import { catchError } from "rxjs/operators";

function safeCommit(tree: Tree, host: virtualFs.Host): Observable<void> {
  const sink = new HostSink(host, false);
  
  return sink.commit(tree).pipe(
    catchError(error => {
      console.error('Commit failed:', error);
      // Could fall back to dry run or other recovery
      const dryRunSink = new DryRunSink(host);
      return dryRunSink.commit(tree);
    })
  );
}

Integration with Workflow

Sinks are typically used in conjunction with workflow systems:

import { 
  Sink, 
  HostSink, 
  DryRunSink,
  SchematicContext,
  Tree 
} from "@angular-devkit/schematics";

function commitTreeWithContext(tree: Tree, context: SchematicContext): Observable<void> {
  // Determine sink type based on context
  let sink: Sink;
  
  if (context.debug) {
    // Use dry run in debug mode
    sink = new DryRunSink('/tmp/debug-output');
    
    // Log dry run events
    if (sink instanceof DryRunSink) {
      sink.reporter.subscribe(event => {
        context.logger.info(`DryRun: ${event.kind} ${event.path}`);
      });
    }
  } else {
    // Use actual file system
    const host = new NodeJsSyncHost();
    sink = new HostSink(host, false);
  }
  
  return sink.commit(tree);
}

Type Definitions

type Path = string & { __PRIVATE_DEVKIT_PATH: void };

interface Action {
  readonly id: number;
  readonly parent: number;
  readonly path: Path;
  readonly kind: 'c' | 'o' | 'r' | 'd';
}

interface CreateFileAction extends Action {
  readonly kind: 'c';
  readonly content: Buffer;
}

interface OverwriteFileAction extends Action {
  readonly kind: 'o';
  readonly content: Buffer;
}

interface RenameFileAction extends Action {
  readonly kind: 'r';
  readonly to: Path;
}

interface DeleteFileAction extends Action {
  readonly kind: 'd';
}