fsBuilder Service

The fsBuilder service provides utilities for generating files and directories during code generation and scaffolding operations. It's primarily used by command generators to create project structure, files, and boilerplate code with user confirmation and conflict resolution.

Interface

The service exposes three main methods:

  • generateDir(dirPath, fn) - Creates a directory and executes a function within its context
  • generateFile(name, options, fn) - Generates a file with template content
  • inProjectRootDir(fn) - Executes a function in the project root directory context

Methods

generateDir(dirPath, fn = () => {})

Creates a directory structure and temporarily changes the working directory to execute the provided function.

Parameters:

  • dirPath (string) - Absolute or relative path to create
  • fn (function) - Optional function to execute within the directory context

Behavior:

  • Converts relative paths to absolute paths using process.cwd()
  • Creates parent directories recursively if they don't exist
  • Changes working directory to the created directory
  • Executes the provided function
  • Restores the previous working directory

generateFile(name, options, fn)

Generates a file with content produced by a template function, handling conflicts and user confirmation.

Parameters:

  • name (string) - File path/name (can include subdirectories)
  • options (object) - Optional configuration:
    • skipIfExists (boolean) - Skip generation if file already exists
    • force (boolean) - Force overwrite without confirmation
  • fn (function) - Template function that receives rendering helpers

Behavior:

  • Creates parent directories automatically if the name includes subdirectories
  • Skips generation if file exists and content is identical
  • Prompts for confirmation before overwriting existing files (unless force or skipIfExists is set)
  • Uses the text rendering system with line(), indent(), and echo() helpers

inProjectRootDir(fn)

Executes a function in the context of the project root directory.

Parameters:

  • fn (function) - Function to execute in project root context

Behavior:

  • Temporarily changes to the project root directory
  • Executes the provided function
  • Restores the previous working directory

Template Functions

When using generateFile(), the template function receives an object with rendering helpers:

  • line(content = '') - Adds a line of content (with newline)
  • indent(fn) - Indents content generated by the nested function (4 spaces)
  • echo(content) - Adds content without automatic newlines

Usage Examples

Basic File Generation

const { generateFile } = this.fsBuilder;

await generateFile('config.js', ({ line, indent }) => {
    line('export default {');
    indent(({ line }) => {
        line("name: 'MyProject',");
        line("version: '1.0.0'");
    });
    line('};');
});

Generating Files with Directory Structure

const { generateFile } = this.fsBuilder;

await generateFile('lib/services/my_service.js', ({ line, indent }) => {
    line('export default {');
    indent(({ line, indent }) => {
        line('create(){');
        indent(({ line }) => {
            line("return 'My Service Implementation';");
        });
        line('}');
    });
    line('};');
});

Working in Project Root

const { inProjectRootDir, generateFile } = this.fsBuilder;

await inProjectRootDir(async () => {
    await generateFile('package.json', ({ echo }) => {
        echo(JSON.stringify({
            name: 'my-project',
            version: '1.0.0'
        }, null, 2));
    });
});

Creating Directory Structure

const { generateDir, generateFile } = this.fsBuilder;

await generateDir('my-project', async () => {
    await generateFile('README.md', ({ line }) => {
        line('# My Project');
        line();
        line('Description of my project.');
    });
    
    await generateFile('lib/index.js', ({ line }) => {
        line("export default 'Hello World';");
    });
});

Handling File Conflicts

const { generateFile } = this.fsBuilder;

// Skip if file already exists
await generateFile('config.js', { skipIfExists: true }, ({ line }) => {
    line('// Default config');
});

// Force overwrite without confirmation
await generateFile('temp.js', { force: true }, ({ line }) => {
    line('// Temporary file');
});

Complex Template with JSON Output

const { generateFile } = this.fsBuilder;

await generateFile('pinstripe.config.js', ({ line, indent }) => {
    line();
    line('const environment = process.env.NODE_ENV || "development";');
    line();
    line('let database;');
    line('if(environment == "production"){');
    indent(({ line, indent }) => {
        line('database = {');
        indent(({ line }) => {
            line('adapter: "mysql",');
            line('host: "localhost",');
            line('user: "root",');
            line('password: "",');
            line('database: `myapp_${environment}`');
        });
        line('};');
    });
    line('} else {');
    indent(({ line, indent }) => {
        line('database = {');
        indent(({ line }) => {
            line('adapter: "sqlite",');
            line('filename: `${environment}.db`');
        });
        line('};');
    });
    line('}');
    line();
    line('export default {');
    indent(({ line }) => {
        line('database');
    });
    line('};');
});

Service Generation Pattern

export default {
    async run(){
        const { name = '' } = this.params;
        const { inProjectRootDir, generateFile } = this.fsBuilder;
        
        await inProjectRootDir(async () => {
            // Create file importer if it doesn't exist
            await generateFile(`lib/services/_file_importer.js`, { skipIfExists: true }, ({ line }) => {
                line();
                line(`export { ServiceFactory as default } from 'pinstripe';`);
                line();
            });

            // Generate the actual service file
            await generateFile(`lib/services/${this.inflector.snakeify(name)}.js`, ({ line, indent }) => {
                line(`export default {`);
                indent(({ line, indent }) => {
                    line('create(){');
                    indent(({ line }) => {
                        line(`return 'Example ${this.inflector.camelize(name)} service'`);
                    });
                    line('}');
                });
                line('};');
            });
        });
    }
};

User Interaction

The service includes a confirmation system for file conflicts:

  • Y (or Enter) - Proceed with the operation
  • N - Skip the current file
  • A - Abort the entire process (exits the program)

The confirmation prompt appears as: Are you sure you want to update /path/to/file.js? Y/n/a:

Common Patterns

  1. File Importer Pattern: Many generators create a _file_importer.js with skipIfExists: true to ensure it exists without overwriting custom implementations.

  2. Directory + File Generation: Creating a directory and then generating files within it is a common pattern for project scaffolding.

  3. Project Root Context: Most file generation happens within inProjectRootDir() to ensure files are created in the correct project location.

  4. Template Inheritance: Using line() and indent() helpers to create properly formatted code with consistent indentation.