Files
masonry/temp_multi-output-template-plan.md

369 lines
10 KiB
Markdown

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 (
<div className={styles.container}>
<h2>{{.AST.UIs.0.Name}}</h2>
{{- range .AST.UIs.0.Fields}}
<div>{{.Name}}: {{{.Name}}}</div>
{{- end}}
</div>
);
};
```
**`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.