diff --git a/temp_multi-output-template-plan.md b/temp_multi-output-template-plan.md new file mode 100644 index 0000000..9c66ede --- /dev/null +++ b/temp_multi-output-template-plan.md @@ -0,0 +1,369 @@ +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 ( +