LogoCyanPrint

Compose Templates

How to combine multiple templates into one

How to Compose Templates

Compose multiple templates to create powerful combinations while maintaining separation of concerns.

What is Composition?

Template composition allows one template to include and use other templates:

cyan.yaml
templates:
- my-org/base-setup:1
- my-org/docker-config:2

Composition Patterns

Layered Architecture

Build templates in layers:

  1. Base template - Core project structure
  2. Feature templates - Optional features (Docker, CI/CD)
  3. Platform templates - Deployment targets
cyan.yaml
# Full-stack web app template
templates:
- shared/node-base:1 # Node.js project structure
- shared/typescript:2 # TypeScript configuration
- shared/eslint:1 # Linting setup
- frontend/react:3 # React frontend
- backend/express:2 # Express backend
- deploy/docker:1 # Docker configuration

Feature Composition

Let users compose features they need by specifying which templates to include:

cyan.yaml
# user selects features
templates:
- base/project:1
- features/typescript:2
- features/docker:1
- features/ci-github:3

Key Considerations

Namespaced Keys

When composing templates, key collisions cause unexpected behavior. Always namespace your keys.

template.ts
// Template A
const name = await i.text('Name?', 'template-a.project.name', '...');
// Template B (composed)
const name = await i.text('Name?', 'template-b.project.name', '...');
// Both get their own answers

Intentional Key Sharing

Share values intentionally via common key names:

template.ts
// Both templates use the same project name
// Template A
const projectName = await i.text('Project name?', 'global.project.name', '...');
// Template B (composed) - gets the same answer
const projectName = await i.text('Project name?', 'global.project.name', '...');

File Conflicts

Handle file conflicts in composition:

  1. Override - Later templates override earlier ones
  2. Merge - For configs, merge content
  3. Fail - Error on conflict (requires handling)

Composition in Code

Access composed templates in your logic:

template.ts
import { StartTemplateWithLambda, GlobType } from '@atomicloud/cyan-sdk';
StartTemplateWithLambda(async (i, d) => {
// Get answer from a composed template
const baseName = await i.text(
'Project name from base?',
'base-template.project.name',
'...'
);
// Add your own files
return {
processors: [{
name: 'cyan/default',
files: [
{ root: 'templates', glob: '**/*', exclude: [], type: GlobType.Template }
],
config: {
vars: {
name: baseName // Use value from base template
}
}
}]
};
});

Best Practices

  1. Single responsibility - Each template does one thing well
  2. Namespace keys - Avoid accidental collisions
  3. Document dependencies - List required composed templates
  4. Version pin - Specify exact versions in composition

Example: Microservice Template

cyan.yaml
# cyan.yaml for microservice template
name: company/microservice
version: 1
templates:
- company/node-base:2
- company/typescript:1
- company/docker:3
- company/grpc:1
- company/observability:2