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. Okay, I think this is pretty close. There maybe a couple things missing though. What if I need a file per item in the AST? For example, I have pages defined in the DSL. So each page should be run through a single template, rather than a single template running through the whole AST. Also attributes of the page might provide some instructions for the template interpreter. Since the DSL supports named layouts, the conditions portion would be a good place to check which layout to render. Pages is only one example, But I have entities, endpoints, pages, sections, components and other future structures that will need to be supported. What suggestion do you have to make all this a possibility? You need a more flexible approach that can iterate over AST collections and generate files per item. Here's how to extend the manifest system to support per-item generation: ## Enhanced 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"` Iterator string `yaml:"iterator,omitempty"` // New: what to iterate over ItemContext string `yaml:"item_context,omitempty"` // New: variable name for current item } ``` ## Updated Implementation ```go func (ti *TemplateInterpreter) InterpretToFiles(masonryFile, templateDir, manifestFile string) (map[string]string, error) { // Load templates and parse as before... outputs := make(map[string]string) for _, output := range manifest.Outputs { if output.Iterator != "" { // Handle per-item generation files, err := ti.generatePerItem(output, ast) if err != nil { return nil, err } for path, content := range files { outputs[path] = content } } else { // Handle single file generation (existing logic) if output.Condition != "" && !ti.evaluateCondition(output.Condition, ast) { continue } content, err := ti.executeTemplate(output.Template, ast) if err != nil { return nil, err } outputs[output.Path] = content } } return outputs, nil } func (ti *TemplateInterpreter) generatePerItem(output OutputFile, ast lang.AST) (map[string]string, error) { items := ti.getIteratorItems(output.Iterator, ast) results := make(map[string]string) for _, item := range items { // Check condition with item context if output.Condition != "" && !ti.evaluateItemCondition(output.Condition, item, ast) { continue } // Create template data with item context data := ti.createItemTemplateData(output.ItemContext, item, ast) // Execute path template to get dynamic filename pathContent, err := ti.executePathTemplate(output.Path, data) if err != nil { return nil, err } // Execute content template content, err := ti.executeTemplateWithData(output.Template, data) if err != nil { return nil, err } results[pathContent] = content } return results, nil } func (ti *TemplateInterpreter) getIteratorItems(iterator string, ast lang.AST) []interface{} { switch iterator { case "pages": items := make([]interface{}, len(ast.Pages)) for i, page := range ast.Pages { items[i] = page } return items case "entities": items := make([]interface{}, len(ast.Entities)) for i, entity := range ast.Entities { items[i] = entity } return items case "endpoints": items := make([]interface{}, len(ast.Endpoints)) for i, endpoint := range ast.Endpoints { items[i] = endpoint } return items case "servers": items := make([]interface{}, len(ast.Servers)) for i, server := range ast.Servers { items[i] = server } return items default: return []interface{}{} } } func (ti *TemplateInterpreter) createItemTemplateData(itemContext string, item interface{}, ast lang.AST) interface{} { if itemContext == "" { itemContext = "Item" // default } return map[string]interface{}{ "AST": ast, "Registry": ti.registry, itemContext: item, } } func (ti *TemplateInterpreter) evaluateItemCondition(condition string, item interface{}, ast lang.AST) bool { switch condition { case "layout_admin": if page, ok := item.(*lang.Page); ok { return page.Layout == "admin" } case "layout_public": if page, ok := item.(*lang.Page); ok { return page.Layout == "public" } case "requires_auth": if page, ok := item.(*lang.Page); ok { return page.Auth } if endpoint, ok := item.(*lang.Endpoint); ok { return endpoint.Auth } } return true } ``` ## Example User Experience ### Project Structure ``` templates/nextjs-app/ ├── manifest.yaml ├── page-public.tmpl ├── page-admin.tmpl ├── api-endpoint.tmpl ├── entity-model.tmpl └── component.tmpl ``` ### Manifest (`manifest.yaml`) ```yaml name: "Next.js Full App Generator" description: "Generates complete Next.js application with pages, API routes, and models" outputs: # Generate a page file per page definition - path: "pages/{{.Page.Path}}.tsx" template: "page-public" iterator: "pages" item_context: "Page" condition: "layout_public" - path: "pages/admin/{{.Page.Name | lower}}.tsx" template: "page-admin" iterator: "pages" item_context: "Page" condition: "layout_admin" # Generate API route per endpoint - path: "pages/api{{.Endpoint.Path}}.ts" template: "api-endpoint" iterator: "endpoints" item_context: "Endpoint" # Generate model per entity - path: "lib/models/{{.Entity.Name | lower}}.ts" template: "entity-model" iterator: "entities" item_context: "Entity" # Single files (existing behavior) - path: "lib/database.ts" template: "database-config" condition: "has_entities" ``` ### Template Files **`page-public.tmpl`:** ```typescript import React from 'react'; import Layout from '../components/Layout'; export default function {{.Page.Name}}Page() { return (

{{.Page.Name}}

{{if .Page.Auth}} {/* Protected content */} {{end}} {{range .Page.Elements}} {{if .Section}}
{{if .Section.Label}}

{{.Section.Label | derefString}}

{{end}} {{range .Section.Elements}} {{if .Component}} {/* Component: {{.Component.Type}} */} {{end}} {{end}}
{{else if .Component}} {/* Component: {{.Component.Type}} */} {{end}} {{end}}
); } ``` **`api-endpoint.tmpl`:** ```typescript import { NextApiRequest, NextApiResponse } from 'next'; {{if .Endpoint.Auth}} import { requireAuth } from '../../lib/auth'; {{end}} export default async function handler(req: NextApiRequest, res: NextApiResponse) { {{if .Endpoint.Auth}} const user = await requireAuth(req, res); if (!user) return; {{end}} if (req.method === '{{.Endpoint.Method}}') { {{range .Endpoint.Params}} const {{.Name}} = req.{{if eq .Source "body"}}body{{else if eq .Source "query"}}query{{else}}params{{end}}.{{.Name}}; {{if .Required}} if (!{{.Name}}) { return res.status(400).json({ error: '{{.Name}} is required' }); } {{end}} {{end}} try { {{if .Endpoint.CustomLogic}} // Custom logic: {{.Endpoint.CustomLogic | derefString}} {{else}} // Default CRUD operation for {{.Endpoint.Entity | derefString}} {{end}} res.status(200).json({ success: true }); } catch (error) { res.status(500).json({ error: 'Internal server error' }); } } else { res.setHeader('Allow', ['{{.Endpoint.Method}}']); res.status(405).end(`Method ${req.method} Not Allowed`); } } ``` ### Masonry Input (`blog-app.masonry`) ``` page Home at "/" layout public { title "Welcome to My Blog" section hero type container { component banner for Post } } page AdminDashboard at "/dashboard" layout admin auth { title "Admin Dashboard" section stats type container { component table for Post } } entity Post { title: string required content: text required published: boolean default "false" } endpoint GET "/api/posts" for Post { returns Post as "json" } endpoint POST "/api/posts" for Post auth { param title: string required from body param content: string required from body returns Post as "json" } ``` ### CLI Usage ```bash ./masonry.exe generate-multi --input blog-app.masonry --template-dir templates/nextjs-app --manifest manifest.yaml --output-dir ./my-blog-app ``` This would generate: - `pages/index.tsx` (Home page) - `pages/admin/admindashboard.tsx` (Admin page) - `pages/api/posts.ts` (GET endpoint) - `pages/api/posts.ts` (POST endpoint - you'd need to handle method routing) - `lib/models/post.ts` (Post model) This approach gives you the flexibility to generate files per AST item while still supporting single-file generation and complex conditional logic based on item attributes.