Looking at your current `TemplateInterpreter` design, you'll need to extend it to handle multi-file output scenarios. Here are several strategies to consider: ## 1. Multi-Output Template Registry Extend your template registry to support templates that define multiple output files: ```go type MultiFileTemplate struct { Name string OutputPath string Content string Dependencies []string } type MultiFileRegistry struct { templates map[string][]MultiFileTemplate funcMap template.FuncMap } ``` ## 2. Template Manifest Approach Create a manifest system where templates can declare their output structure: ```go type TemplateManifest struct { Name string `yaml:"name"` Description string `yaml:"description"` Outputs []OutputFile `yaml:"outputs"` } type OutputFile struct { Path string `yaml:"path"` Template string `yaml:"template"` Condition string `yaml:"condition,omitempty"` } ``` ## 3. Directory-Based Output Strategy Modify your interpreter to return a map of file paths to content: ```go func (ti *TemplateInterpreter) InterpretMultiFile(masonryInput, templateDir string) (map[string]string, error) { // Returns map[outputPath]content } ``` ## 4. Template Composition with File Directives Add special template functions for file operations: ```go // In your funcMap "writeFile": func(path, content string) string { // Queue file for writing return "" }, "includeFile": func(templateName string, data interface{}) string { // Include another template's output }, ``` ## 5. Build Pipeline Approach Create a build pipeline that processes templates in stages: ```go type BuildPipeline struct { stages []BuildStage } type BuildStage struct { Name string Templates []string OutputDir string Condition func(ast lang.AST) bool } ``` ## Recommended Implementation I'd suggest starting with a **Template Manifest + Multi-Output** approach. This gives you flexibility while maintaining the simplicity of your current design: ```go type MultiFileInterpreter struct { registry *TemplateRegistry outputs map[string]string } func (mfi *MultiFileInterpreter) InterpretToFiles(masonryInput, templateDir, manifestFile string) (map[string]string, error) { // Load manifest // Process each output file defined in manifest // Return map of filepath -> content } ``` This approach allows you to: - Keep your existing single-file templates working - Define complex multi-file outputs via manifest files - Maintain template reusability across different output scenarios - Support conditional file generation based on AST content Would you like me to elaborate on any of these strategies or help implement a specific approach? Yeah, can you show me how a Template Manifest with Multi-output might work? I'd like to also see what it would look like for an end user to set up and get working. I'll show you how a Template Manifest with Multi-output approach would work, including the implementation and user experience. ## Implementation First, let's define the manifest structure: ```go type TemplateManifest struct { Name string `yaml:"name"` Description string `yaml:"description"` Outputs []OutputFile `yaml:"outputs"` } type OutputFile struct { Path string `yaml:"path"` Template string `yaml:"template"` Condition string `yaml:"condition,omitempty"` } ``` Now let's extend your `TemplateInterpreter` to support multi-file output: ```go // InterpretToFiles processes a manifest and returns multiple output files func (ti *TemplateInterpreter) InterpretToFiles(masonryFile, templateDir, manifestFile string) (map[string]string, error) { // Load templates from directory err := ti.registry.LoadFromDirectory(templateDir) if err != nil { return nil, fmt.Errorf("error loading templates: %w", err) } // Load manifest manifestPath := filepath.Join(templateDir, manifestFile) manifestData, err := os.ReadFile(manifestPath) if err != nil { return nil, fmt.Errorf("error reading manifest: %w", err) } var manifest TemplateManifest err = yaml.Unmarshal(manifestData, &manifest) if err != nil { return nil, fmt.Errorf("error parsing manifest: %w", err) } // Parse Masonry input masonryInput, err := os.ReadFile(masonryFile) if err != nil { return nil, fmt.Errorf("error reading Masonry file: %w", err) } ast, err := lang.ParseInput(string(masonryInput)) if err != nil { return nil, fmt.Errorf("error parsing Masonry input: %w", err) } // Process each output file outputs := make(map[string]string) for _, output := range manifest.Outputs { // Check condition if specified if output.Condition != "" && !ti.evaluateCondition(output.Condition, ast) { continue } // Execute template content, err := ti.executeTemplate(output.Template, ast) if err != nil { return nil, fmt.Errorf("error executing template %s: %w", output.Template, err) } outputs[output.Path] = content } return outputs, nil } func (ti *TemplateInterpreter) executeTemplate(templateName string, ast lang.AST) (string, error) { if tmpl, exists := ti.registry.templates[templateName]; exists { data := struct { AST lang.AST Registry *TemplateRegistry }{ AST: ast, Registry: ti.registry, } var buf bytes.Buffer err := tmpl.Execute(&buf, data) return buf.String(), err } return "", fmt.Errorf("template %s not found", templateName) } func (ti *TemplateInterpreter) evaluateCondition(condition string, ast lang.AST) bool { // Simple condition evaluation - you can expand this switch condition { case "has_api": return len(ast.APIs) > 0 case "has_ui": return len(ast.UIs) > 0 case "has_database": return len(ast.Databases) > 0 default: return true } } ``` You'll also need to add the yaml import: ```go import "gopkg.in/yaml.v3" ``` ## End User Experience Here's how an end user would set up a React component generator: ### 1. Project Structure ``` templates/react-components/ ├── manifest.yaml ├── component.tmpl ├── component-test.tmpl ├── index.tmpl └── styles.tmpl ``` ### 2. Manifest File (`manifest.yaml`) ```yaml name: "React Component Generator" description: "Generates React components with tests and styles" outputs: - path: "src/components/{{.AST.UIs.0.Name}}/{{.AST.UIs.0.Name}}.tsx" template: "component" condition: "has_ui" - path: "src/components/{{.AST.UIs.0.Name}}/{{.AST.UIs.0.Name}}.test.tsx" template: "component-test" condition: "has_ui" - path: "src/components/{{.AST.UIs.0.Name}}/index.ts" template: "index" condition: "has_ui" - path: "src/components/{{.AST.UIs.0.Name}}/{{.AST.UIs.0.Name}}.module.css" template: "styles" condition: "has_ui" ``` ### 3. Template Files **`component.tmpl`:** ```typescript import React from 'react'; import styles from './{{.AST.UIs.0.Name}}.module.css'; interface {{.AST.UIs.0.Name}}Props { {{- range .AST.UIs.0.Fields}} {{.Name}}: {{goType .Type}}; {{- end}} } export const {{.AST.UIs.0.Name}}: React.FC<{{.AST.UIs.0.Name}}Props> = ({ {{- range $i, $field := .AST.UIs.0.Fields}} {{if $i}},{{end}} {{$field.Name}} {{- end}} }) => { return (

{{.AST.UIs.0.Name}}

{{- range .AST.UIs.0.Fields}}
{{.Name}}: {{{.Name}}}
{{- end}}
); }; ``` **`component-test.tmpl`:** ```typescript import { render, screen } from '@testing-library/react'; import { {{.AST.UIs.0.Name}} } from './{{.AST.UIs.0.Name}}'; describe('{{.AST.UIs.0.Name}}', () => { it('renders without crashing', () => { render(<{{.AST.UIs.0.Name}}{{range .AST.UIs.0.Fields}} {{.Name}}="test"{{end}} />); expect(screen.getByText('{{.AST.UIs.0.Name}}')).toBeInTheDocument(); }); }); ``` ### 4. Masonry Input (`user-profile.masonry`) ``` ui UserProfile { field name: string field email: string field age: int } ``` ### 5. CLI Usage ```bash # Build the CLI go build -o masonry.exe ./cmd/cli/ # Generate files using manifest ./masonry.exe generate --input user-profile.masonry --template-dir templates/react-components --manifest manifest.yaml --output-dir ./generated ``` ### 6. CLI Command Implementation Add this to your CLI: ```go func generateMultiFileCommand() *cobra.Command { var inputFile, templateDir, manifestFile, outputDir string cmd := &cobra.Command{ Use: "generate-multi", Short: "Generate multiple files using a template manifest", RunE: func(cmd *cobra.Command, args []string) error { interpreter := interpreter.NewTemplateInterpreter() outputs, err := interpreter.InterpretToFiles(inputFile, templateDir, manifestFile) if err != nil { return err } // Write all output files for filePath, content := range outputs { fullPath := filepath.Join(outputDir, filePath) // Create directory if it doesn't exist dir := filepath.Dir(fullPath) err := os.MkdirAll(dir, 0755) if err != nil { return fmt.Errorf("error creating directory %s: %w", dir, err) } // Write file err = os.WriteFile(fullPath, []byte(content), 0644) if err != nil { return fmt.Errorf("error writing file %s: %w", fullPath, err) } fmt.Printf("Generated: %s\n", fullPath) } return nil }, } cmd.Flags().StringVarP(&inputFile, "input", "i", "", "Input Masonry file") cmd.Flags().StringVarP(&templateDir, "template-dir", "t", "", "Template directory") cmd.Flags().StringVarP(&manifestFile, "manifest", "m", "manifest.yaml", "Manifest file name") cmd.Flags().StringVarP(&outputDir, "output-dir", "o", "./generated", "Output directory") cmd.MarkFlagRequired("input") cmd.MarkFlagRequired("template-dir") return cmd } ``` This approach gives users a clean, declarative way to define complex multi-file outputs while maintaining the flexibility of your template system. The manifest acts as a blueprint that maps Masonry definitions to specific output files with conditional generation based on the AST content.