Low-level atomic operations for file system modifications that can be applied to trees and tracked for optimization. Actions represent individual file system operations that can be queued, optimized, and executed through sinks.
Core interfaces representing different types of file system operations.
/**
* Base interface for all file system actions
*/
interface Action {
readonly id: number;
readonly parent: number;
readonly path: Path;
readonly kind: 'c' | 'o' | 'r' | 'd';
}
/**
* Action to create a new file
*/
interface CreateFileAction extends Action {
readonly kind: 'c';
readonly content: Buffer;
}
/**
* Action to overwrite an existing file
*/
interface OverwriteFileAction extends Action {
readonly kind: 'o';
readonly content: Buffer;
}
/**
* Action to rename a file or directory
*/
interface RenameFileAction extends Action {
readonly kind: 'r';
readonly to: Path;
}
/**
* Action to delete a file or directory
*/
interface DeleteFileAction extends Action {
readonly kind: 'd';
}
type Action = CreateFileAction | OverwriteFileAction | RenameFileAction | DeleteFileAction;Container class for managing and optimizing sequences of file system actions.
/**
* Container for managing file system actions with optimization capabilities
*/
class ActionList implements Iterable<Action> {
/** Create a new file action */
create(path: Path, content: Buffer): void;
/** Overwrite an existing file action */
overwrite(path: Path, content: Buffer): void;
/** Rename a file or directory action */
rename(path: Path, to: Path): void;
/** Delete a file or directory action */
delete(path: Path): void;
/** Optimize the action sequence by removing redundant operations */
optimize(): void;
/** Add an action to the list */
push(action: Action): void;
/** Get action at specific index */
get(index: number): Action;
/** Check if action exists in the list */
has(action: Action): boolean;
/** Find action matching predicate */
find(predicate: (value: Action) => boolean): Action | null;
/** Execute function for each action */
forEach(fn: (value: Action, index: number, array: Action[]) => void, thisArg?: any): void;
/** Number of actions in the list */
readonly length: number;
// Iterable implementation
[Symbol.iterator](): Iterator<Action>;
}Usage Examples:
import { ActionList, Action } from "@angular-devkit/schematics";
// Create and manage actions
function manageActions(): ActionList {
const actions = new ActionList();
// Add various actions
actions.create('/new-file.txt', Buffer.from('Hello World'));
actions.overwrite('/existing-file.txt', Buffer.from('Updated content'));
actions.rename('/old-name.txt', '/new-name.txt');
actions.delete('/unwanted-file.txt');
// Optimize to remove redundant operations
actions.optimize();
return actions;
}
// Iterate through actions
function processActions(actions: ActionList): void {
for (const action of actions) {
switch (action.kind) {
case 'c':
console.log(`Create: ${action.path} (${action.content.length} bytes)`);
break;
case 'o':
console.log(`Overwrite: ${action.path} (${action.content.length} bytes)`);
break;
case 'r':
console.log(`Rename: ${action.path} -> ${action.to}`);
break;
case 'd':
console.log(`Delete: ${action.path}`);
break;
}
}
}The action optimization process removes redundant operations and consolidates related actions.
Optimization Rules:
import { ActionList } from "@angular-devkit/schematics";
// Example of optimization effects
function demonstrateOptimization(): void {
const actions = new ActionList();
// Add redundant operations
actions.create('/temp.txt', Buffer.from('Initial'));
actions.overwrite('/temp.txt', Buffer.from('Updated'));
actions.overwrite('/temp.txt', Buffer.from('Final'));
console.log('Before optimization:', actions.length); // 3
// Optimize - consolidates to single create with final content
actions.optimize();
console.log('After optimization:', actions.length); // 1
const finalAction = actions.get(0);
console.log('Final action:', finalAction.kind); // 'c'
console.log('Final content:', finalAction.content.toString()); // 'Final'
}Helper functions for creating specific action types.
/**
* Create a file creation action
*/
function createAction(path: Path, content: Buffer): CreateFileAction;
/**
* Create a file overwrite action
*/
function overwriteAction(path: Path, content: Buffer): OverwriteFileAction;
/**
* Create a file rename action
*/
function renameAction(path: Path, to: Path): RenameFileAction;
/**
* Create a file delete action
*/
function deleteAction(path: Path): DeleteFileAction;Actions can be applied to trees using the apply method.
/**
* Apply an action to a tree with optional merge strategy
*/
Tree.prototype.apply = function(action: Action, strategy?: MergeStrategy): void {
// Implementation applies the action to the tree
};Usage Examples:
import { Tree, MergeStrategy } from "@angular-devkit/schematics";
function applyActionsToTree(tree: Tree): Tree {
// Create actions
const createAction = {
id: 1,
parent: 0,
path: '/new-file.txt',
kind: 'c' as const,
content: Buffer.from('New file content')
};
const deleteAction = {
id: 2,
parent: 0,
path: '/old-file.txt',
kind: 'd' as const
};
// Apply actions to tree
tree.apply(createAction, MergeStrategy.Default);
tree.apply(deleteAction, MergeStrategy.Default);
return tree;
}Trees maintain a history of all actions applied to them.
/**
* Access the action history of a tree
*/
interface Tree {
readonly actions: Action[];
}Usage Examples:
import { Tree } from "@angular-devkit/schematics";
function analyzeTreeActions(tree: Tree): void {
console.log(`Total actions: ${tree.actions.length}`);
const actionCounts = tree.actions.reduce((counts, action) => {
counts[action.kind] = (counts[action.kind] || 0) + 1;
return counts;
}, {} as Record<string, number>);
console.log('Action breakdown:', {
creates: actionCounts['c'] || 0,
overwrites: actionCounts['o'] || 0,
renames: actionCounts['r'] || 0,
deletes: actionCounts['d'] || 0
});
}
function findActionsByPath(tree: Tree, searchPath: string): Action[] {
return tree.actions.filter(action =>
action.path === searchPath ||
(action.kind === 'r' && action.to === searchPath)
);
}Advanced patterns for working with actions.
import { Action, ActionList } from "@angular-devkit/schematics";
// Custom action processor
class ActionProcessor {
constructor(private actions: ActionList) {}
// Get all file paths affected by actions
getAffectedPaths(): Set<string> {
const paths = new Set<string>();
for (const action of this.actions) {
paths.add(action.path);
if (action.kind === 'r') {
paths.add(action.to);
}
}
return paths;
}
// Get actions that create or modify content
getContentActions(): Action[] {
return Array.from(this.actions).filter(
(action): action is CreateFileAction | OverwriteFileAction =>
action.kind === 'c' || action.kind === 'o'
);
}
// Calculate total content size
getTotalContentSize(): number {
return this.getContentActions()
.reduce((total, action) => total + action.content.length, 0);
}
// Group actions by directory
groupByDirectory(): Map<string, Action[]> {
const groups = new Map<string, Action[]>();
for (const action of this.actions) {
const dir = action.path.substring(0, action.path.lastIndexOf('/')) || '/';
if (!groups.has(dir)) {
groups.set(dir, []);
}
groups.get(dir)!.push(action);
}
return groups;
}
}Utilities for validating action sequences.
import { Action, ActionList } from "@angular-devkit/schematics";
class ActionValidator {
static validate(actions: ActionList): ValidationResult {
const errors: string[] = [];
const warnings: string[] = [];
// Check for conflicting operations
const pathOperations = new Map<string, Action[]>();
for (const action of actions) {
const path = action.path;
if (!pathOperations.has(path)) {
pathOperations.set(path, []);
}
pathOperations.get(path)!.push(action);
}
for (const [path, ops] of pathOperations) {
// Check for create after delete without intermediate operations
const hasCreate = ops.some(op => op.kind === 'c');
const hasDelete = ops.some(op => op.kind === 'd');
if (hasCreate && hasDelete) {
const createIndex = ops.findIndex(op => op.kind === 'c');
const deleteIndex = ops.findIndex(op => op.kind === 'd');
if (deleteIndex > createIndex) {
warnings.push(`Path ${path}: Created then deleted`);
}
}
// Check for multiple creates
const creates = ops.filter(op => op.kind === 'c');
if (creates.length > 1) {
errors.push(`Path ${path}: Multiple create operations`);
}
}
return {
isValid: errors.length === 0,
errors,
warnings
};
}
}
interface ValidationResult {
isValid: boolean;
errors: string[];
warnings: string[];
}Actions are closely integrated with the Tree interface:
import { Tree, Action } from "@angular-devkit/schematics";
// Extension methods for working with tree actions
class TreeActionHelper {
static getLatestContent(tree: Tree, path: string): Buffer | null {
// Find the most recent content-affecting action for the path
const actions = tree.actions
.filter((action): action is CreateFileAction | OverwriteFileAction =>
(action.kind === 'c' || action.kind === 'o') && action.path === path
)
.sort((a, b) => b.id - a.id); // Most recent first
return actions.length > 0 ? actions[0].content : tree.read(path);
}
static hasBeenDeleted(tree: Tree, path: string): boolean {
// Check if there's a delete action for this path
return tree.actions.some(action =>
action.kind === 'd' && action.path === path
);
}
static getActionHistory(tree: Tree, path: string): Action[] {
return tree.actions
.filter(action =>
action.path === path ||
(action.kind === 'r' && action.to === path)
)
.sort((a, b) => a.id - b.id); // Chronological order
}
}type Path = string & { __PRIVATE_DEVKIT_PATH: void };
type ActionKind = 'c' | 'o' | 'r' | 'd';
interface ActionBase {
readonly id: number;
readonly parent: number;
readonly path: Path;
}
enum MergeStrategy {
Default = 0,
Error = 1,
AllowOverwriteConflict = 2,
AllowCreationConflict = 4,
AllowDeleteConflict = 8,
ContentOnly = 2,
Overwrite = 14
}