369 lines
10 KiB
Markdown
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. |