Rails-inspired generator system that provides scaffolding for your apps
Process spawning and command execution with both synchronous and asynchronous support using execa for cross-platform compatibility.
Execute commands asynchronously with full control over input/output streams.
/**
* Normalize a command across OS and spawn it (asynchronously)
* @param command - Program to execute
* @param options - Execa options for command execution
* @returns ExecaChildProcess for command interaction
*/
spawnCommand(command: string, options?: ExecaOptions): ExecaChildProcess;
/**
* Normalize a command across OS and spawn it (asynchronously)
* @param command - Program to execute
* @param args - List of arguments to pass to the program
* @param options - Execa options for command execution
* @returns ExecaChildProcess for command interaction
*/
spawn(command: string, args?: readonly string[], options?: ExecaOptions): ExecaChildProcess;Usage Examples:
export default class MyGenerator extends Generator {
async install() {
// Execute command with default options (inherits stdio)
await this.spawnCommand('npm install');
// Execute with custom options
await this.spawnCommand('npm run build', {
stdio: 'pipe', // Capture output
cwd: this.destinationPath()
});
// Execute with arguments array
await this.spawn('git', ['init'], {
cwd: this.destinationPath()
});
// Execute with environment variables
await this.spawn('npm', ['run', 'test'], {
env: {
...process.env,
NODE_ENV: 'test',
CI: '1'
}
});
}
async writing() {
// Conditional command execution
if (this.answers.initGit) {
await this.spawn('git', ['init']);
await this.spawn('git', ['add', '.']);
await this.spawn('git', ['commit', '-m', 'Initial commit']);
}
if (this.answers.installDeps && !this.options.skipInstall) {
const packageManager = this.answers.packageManager || 'npm';
await this.spawnCommand(`${packageManager} install`);
}
}
}Execute commands synchronously when you need to block execution.
/**
* Normalize a command across OS and spawn it (synchronously)
* @param command - Program to execute
* @param options - Execa sync options for command execution
* @returns ExecaSyncReturnValue with command results
*/
spawnCommandSync(command: string, options?: SyncOptions): ExecaSyncReturnValue;
/**
* Normalize a command across OS and spawn it (synchronously)
* @param command - Program to execute
* @param args - List of arguments to pass to the program
* @param options - Execa sync options for command execution
* @returns ExecaSyncReturnValue with command results
*/
spawnSync(command: string, args?: readonly string[], options?: SyncOptions): ExecaSyncReturnValue;Usage Examples:
export default class MyGenerator extends Generator {
initializing() {
// Check if tools are available synchronously
try {
const gitResult = this.spawnCommandSync('git --version', {
stdio: 'pipe'
});
this.log(`Git available: ${gitResult.stdout}`);
} catch (error) {
this.log('Git not available, skipping git operations');
this.options.skipGit = true;
}
// Get current directory info
const lsResult = this.spawnSync('ls', ['-la'], {
stdio: 'pipe',
cwd: this.destinationRoot()
});
if (lsResult.stdout.includes('package.json')) {
this.log('Existing package.json found');
}
}
configuring() {
// Synchronous operations for validation
if (this.answers.framework === 'react') {
try {
const nodeVersion = this.spawnCommandSync('node --version', {
stdio: 'pipe'
});
const version = nodeVersion.stdout.slice(1); // Remove 'v' prefix
if (parseInt(version.split('.')[0]) < 16) {
throw new Error('React requires Node.js 16 or higher');
}
} catch (error) {
this.log('Warning: Could not verify Node.js version');
}
}
}
}Capture and process command output for decision making.
Usage Examples:
export default class MyGenerator extends Generator {
async prompting() {
// Get git user info for defaults
let defaultAuthor = 'Anonymous';
let defaultEmail = '';
try {
const nameResult = await this.spawn('git', ['config', 'user.name'], {
stdio: 'pipe'
});
defaultAuthor = nameResult.stdout.trim();
const emailResult = await this.spawn('git', ['config', 'user.email'], {
stdio: 'pipe'
});
defaultEmail = emailResult.stdout.trim();
} catch (error) {
// Git not configured, use defaults
}
this.answers = await this.prompt([
{
name: 'author',
message: 'Author name:',
default: defaultAuthor
},
{
name: 'email',
message: 'Author email:',
default: defaultEmail
}
]);
}
async writing() {
// Check if dependencies are already installed
try {
const result = await this.spawn('npm', ['list', '--depth=0'], {
stdio: 'pipe',
cwd: this.destinationPath()
});
if (result.stdout.includes('express')) {
this.log('Express already installed, skipping');
return;
}
} catch (error) {
// No package.json or no dependencies, continue
}
await this.addDependencies(['express']);
}
}Handle command failures and validate execution environments.
Usage Examples:
export default class MyGenerator extends Generator {
async initializing() {
// Validate required tools
const requiredTools = [
{ cmd: 'node --version', name: 'Node.js' },
{ cmd: 'npm --version', name: 'npm' }
];
if (this.answers.useGit) {
requiredTools.push({ cmd: 'git --version', name: 'Git' });
}
for (const tool of requiredTools) {
try {
await this.spawnCommand(tool.cmd, { stdio: 'pipe' });
this.log(`✓ ${tool.name} is available`);
} catch (error) {
throw new Error(`${tool.name} is required but not available`);
}
}
}
async install() {
// Install with retry logic
const maxRetries = 3;
let retries = 0;
while (retries < maxRetries) {
try {
await this.spawnCommand('npm install', {
cwd: this.destinationPath()
});
this.log('Dependencies installed successfully');
break;
} catch (error) {
retries++;
this.log(`Install failed (attempt ${retries}/${maxRetries})`);
if (retries >= maxRetries) {
if (this.options.forceInstall) {
throw error; // Fail hard if force-install is set
} else {
this.log('Skipping install due to errors. Run npm install manually.');
break;
}
}
// Wait before retry
await new Promise(resolve => setTimeout(resolve, 1000));
}
}
}
}Complex command execution scenarios with streaming and process control.
Usage Examples:
export default class MyGenerator extends Generator {
async install() {
// Stream command output in real-time
const child = this.spawn('npm', ['install', '--verbose'], {
stdio: ['ignore', 'pipe', 'pipe']
});
child.stdout.on('data', (data) => {
this.log(`npm: ${data.toString().trim()}`);
});
child.stderr.on('data', (data) => {
this.log(`npm error: ${data.toString().trim()}`);
});
await child;
}
async writing() {
// Parallel command execution
const commands = [
this.spawn('npm', ['run', 'lint']),
this.spawn('npm', ['run', 'test']),
this.spawn('npm', ['run', 'build'])
];
try {
await Promise.all(commands);
this.log('All commands completed successfully');
} catch (error) {
this.log('Some commands failed, check output above');
}
}
async end() {
// Interactive command execution
if (this.answers.openEditor) {
const editor = process.env.EDITOR || 'code';
await this.spawn(editor, ['.'], {
stdio: 'inherit', // Allow user interaction
cwd: this.destinationPath()
});
}
// Conditional post-install commands
if (this.answers.framework === 'react') {
await this.spawnCommand('npm run start', {
detached: true, // Run in background
stdio: 'ignore'
});
this.log('Development server started at http://localhost:3000');
}
}
}Handle cross-platform compatibility for different operating systems.
Usage Examples:
export default class MyGenerator extends Generator {
async writing() {
// Platform-specific commands
const isWindows = process.platform === 'win32';
if (this.answers.createDesktopShortcut) {
if (isWindows) {
// Windows-specific shortcut creation
await this.spawnCommand('powershell', [
'-Command',
`$WshShell = New-Object -comObject WScript.Shell; $Shortcut = $WshShell.CreateShortcut("$Home\\Desktop\\${this.answers.name}.lnk"); $Shortcut.TargetPath = "${this.destinationPath('run.bat')}"; $Shortcut.Save()`
]);
} else {
// Unix-like systems
await this.spawn('ln', ['-sf',
this.destinationPath('run.sh'),
`${process.env.HOME}/Desktop/${this.answers.name}`
]);
}
}
// Cross-platform file permissions
if (!isWindows && this.answers.createExecutable) {
await this.spawn('chmod', ['+x', this.destinationPath('bin/cli.js')]);
}
}
async install() {
// Platform-specific package managers
const isWindows = process.platform === 'win32';
const packageManager = this.answers.packageManager;
const command = isWindows && packageManager === 'npm'
? 'npm.cmd'
: packageManager;
await this.spawn(command, ['install'], {
cwd: this.destinationPath()
});
}
}// Execa child process (async)
interface ExecaChildProcess<StdoutStderrType = string> extends Promise<ExecaReturnValue<StdoutStderrType>> {
pid?: number;
stdout: NodeJS.ReadableStream;
stderr: NodeJS.ReadableStream;
stdin: NodeJS.WritableStream;
kill(signal?: string): void;
}
// Execa return value (async)
interface ExecaReturnValue<StdoutStderrType = string> {
stdout: StdoutStderrType;
stderr: StdoutStderrType;
exitCode: number;
command: string;
killed: boolean;
signal?: string;
}
// Execa sync return value
interface ExecaSyncReturnValue<StdoutStderrType = string> {
stdout: StdoutStderrType;
stderr: StdoutStderrType;
exitCode: number;
command: string;
killed: boolean;
signal?: string;
}
// Execa options (async)
interface ExecaOptions<EncodingType = string> {
cwd?: string;
env?: Record<string, string>;
stdio?: 'pipe' | 'inherit' | 'ignore' | readonly StdioOption[];
timeout?: number;
killSignal?: string;
encoding?: EncodingType;
shell?: boolean | string;
windowsHide?: boolean;
}
// Execa sync options
interface SyncOptions<EncodingType = string> {
cwd?: string;
env?: Record<string, string>;
stdio?: 'pipe' | 'inherit' | 'ignore' | readonly StdioOption[];
timeout?: number;
killSignal?: string;
encoding?: EncodingType;
shell?: boolean | string;
windowsHide?: boolean;
}Install with Tessl CLI
npx tessl i tessl/npm-yeoman-generator