add a rough plan for multi-file output template processing
This commit is contained in:
369
temp_multi-output-template-plan.md
Normal file
369
temp_multi-output-template-plan.md
Normal file
@ -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 (
|
||||
<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.
|
Reference in New Issue
Block a user