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.
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>;
}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>;
}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);
}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>;
}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);
}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);
}
}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);
})
);
}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 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';
}