LogoCyanPrint
ProcessorsExplanation

Why Processors Exist

Understanding the purpose and use cases for custom processors

Why Processors Exist

Processors exist because different projects need different file transformation strategies. The default processor covers common cases, but custom processors enable specialized needs.

The Default Processor

CyanPrint includes a default processor (cyan/default - following the namespace/processor-name format) that uses Eta templating:

# var__projectName__
Created by var__author__ on var__date__.

This works well for simple variable substitution. But what if you need more?

When Default Isn't Enough

Different Templating Engines

Your team might prefer different syntax:

EngineSyntaxUse Case
Eta (default)var__name__General purpose
Jinja{{ name }}Python ecosystems
Go Templates{{ .Name }}Helm, Kubernetes
Handlebars{{name}}JavaScript projects
Mustache{{name}}Logic-less templates

The alternative engines listed above (Jinja, Go Templates, Handlebars, Mustache) and the processor names custom/jinja, custom/go-templates, and custom/handlebars shown in the diagram below are illustrative examples. You would need to create custom processors to use alternative templating engines.

Custom Logic

Some transformations go beyond simple variable substitution:

processor.ts
import { StartProcessorWithLambda, GlobType } from '@atomicloud/cyan-sdk';
// GraphQL schema -> TypeScript types (illustrative example)
StartProcessorWithLambda(async (input, fileHelper) => {
const schemas = fileHelper.read({
Reads all GraphQL schema files from the 'schemas' directory
root: 'schemas',
glob: '**/*.graphql',
exclude: [],
type: GlobType.Template
});
for (const schema of schemas) {
// generateTypeScript is a user-defined function
const types = generateTypeScript(schema.content);
schema.relative = schema.relative.replace('.graphql', '.generated.ts');
Changes output file extension
schema.content = types;
schema.writeFile();
}
return { directory: input.writeDir };
});
processor.py
from atomicloud_cyan_sdk import start_processor_with_fn, GlobType
# GraphQL schema -> TypeScript types (illustrative example)
def start_processor_with_fn(input, file_helper):
schemas = file_helper.read({
Reads all GraphQL schema files from the 'schemas' directory
'root': 'schemas',
'glob': '**/*.graphql',
'exclude': [],
'type': GlobType.Template
})
for schema in schemas:
# generate_type_script is a user-defined function
types = generate_type_script(schema.content)
schema.relative = schema.relative.replace('.graphql', '.generated.ts')
Changes output file extension
schema.content = types
schema.write_file()
return {'directory': input.write_dir}
Processor.cs
using Atomicloud.CyanSdk;
// GraphQL schema -> TypeScript types (illustrative example)
[ProcessorMain]
public async Task<ProcessorResult> RunAsync(ProcessorInput input, CyanFileHelper fileHelper)
{
var schemas = fileHelper.Read(new FileGlob
Reads all GraphQL schema files from the 'schemas' directory
{
Root = "schemas",
Glob = "**/*.graphql",
Exclude = Array.Empty<string>(),
Type = GlobType.Template
});
foreach (var schema in schemas)
{
// GenerateTypeScript is a user-defined function
var types = GenerateTypeScript(schema.Content);
schema.Relative = schema.Relative.Replace(".graphql", ".generated.ts");
Changes output file extension
schema.Content = types;
schema.WriteFile();
}
return new ProcessorResult { Directory = input.WriteDir };
}

AST Transformations

processor.ts
import { StartProcessorWithLambda, GlobType } from '@atomicloud/cyan-sdk';
// Transform TypeScript using AST (illustrative example)
StartProcessorWithLambda(async (input, fileHelper) => {
const files = fileHelper.read({
root: 'src',
glob: '**/*.ts',
exclude: [],
type: GlobType.Template
});
for (const file of files) {
// parseTypeScript, addDeprecationComments, printTypeScript are user-defined
const ast = parseTypeScript(file.content);
Parse source to AST
addDeprecationComments(ast);
file.content = printTypeScript(ast);
Convert AST back to code
file.writeFile();
}
return { directory: input.writeDir };
});
processor.py
from atomicloud_cyan_sdk import start_processor_with_fn, GlobType
# Transform TypeScript using AST (illustrative example)
def start_processor_with_fn(input, file_helper):
files = file_helper.read({
'root': 'src',
'glob': '**/*.ts',
'exclude': [],
'type': GlobType.Template
})
for file in files:
# parse_type_script, add_deprecation_comments, print_type_script are user-defined
ast = parse_type_script(file.content)
Parse source to AST
add_deprecation_comments(ast)
file.content = print_type_script(ast)
Convert AST back to code
file.write_file()
return {'directory': input.write_dir}
Processor.cs
using Atomicloud.CyanSdk;
// Transform TypeScript using AST (illustrative example)
[ProcessorMain]
public async Task<ProcessorResult> RunAsync(ProcessorInput input, CyanFileHelper fileHelper)
{
var files = fileHelper.Read(new FileGlob
{
Root = "src",
Glob = "**/*.ts",
Exclude = Array.Empty<string>(),
Type = GlobType.Template
});
foreach (var file in files)
{
// ParseTypeScript, AddDeprecationComments, PrintTypeScript are user-defined
var ast = ParseTypeScript(file.Content);
Parse source to AST
AddDeprecationComments(ast);
file.Content = PrintTypeScript(ast);
Convert AST back to code
file.WriteFile();
}
return new ProcessorResult { Directory = input.WriteDir };
}

Conditional Processing

processor.ts
import { StartProcessorWithLambda } from '@atomicloud/cyan-sdk';
// Process based on config (illustrative example)
StartProcessorWithLambda(async (input, fileHelper) => {
const config = input.config as { env: 'dev' | 'prod' };
const files = fileHelper.resolveAll();
Copies Copy-type files, then returns Template-type files
for (const file of files) {
if (config.env === 'prod') {
// removeDebugCode is a user-defined function
file.content = removeDebugCode(file.content);
}
file.writeFile();
}
return { directory: input.writeDir };
});
processor.py
from atomicloud_cyan_sdk import start_processor_with_fn
# Process based on config (illustrative example)
def start_processor_with_fn(input, file_helper):
config = input.config # type: dict with 'env' key
files = file_helper.resolve_all()
Copies Copy-type files, then returns Template-type files
for file in files:
if config['env'] == 'prod':
# remove_debug_code is a user-defined function
file.content = remove_debug_code(file.content)
file.write_file()
return {'directory': input.write_dir}
Processor.cs
using Atomicloud.CyanSdk;
// Process based on config (illustrative example)
[ProcessorMain]
public async Task<ProcessorResult> RunAsync(ProcessorInput input, CyanFileHelper fileHelper)
{
var config = input.Config as dynamic;
var files = fileHelper.ResolveAll();
Copies Copy-type files, then returns Template-type files
foreach (var file in files)
{
if (config.env == "prod")
{
// RemoveDebugCode is a user-defined function
file.Content = RemoveDebugCode(file.Content);
}
file.WriteFile();
}
return new ProcessorResult { Directory = input.WriteDir };
}

Processing Pipelines

Processors can be chained for complex transformations:

Example: Multi-Stage Pipeline

GlobType.Template (value 0 in Node SDK, 1 in Python SDK) processes files through the templating engine. GlobType.Copy copies files without transformation.

config.ts
import { GlobType } from '@atomicloud/cyan-sdk';
// In template configuration - this is part of a template's return statement
return {
processors: [
Array of processor stages, executed in order
// Stage 1: Preprocess
{
name: 'custom/preprocessor',
files: [{ root: 'src', glob: '**/*', exclude: [], type: GlobType.Template }],
config: { stripComments: true }
},
// Stage 2: Main templating
{
name: 'cyan/default',
files: [{ root: 'src', glob: '**/*', exclude: [], type: GlobType.Template }],
config: { vars: { name, version } }
},
// Stage 3: Postprocess
{
name: 'custom/formatter',
files: [{ root: 'src', glob: '**/*', exclude: [], type: GlobType.Template }],
config: { format: 'prettier' }
}
]
};
config.py
from atomicloud_cyan_sdk import GlobType
# In template configuration - this is part of a template's return statement
return {
'processors': [
Array of processor stages, executed in order
# Stage 1: Preprocess
{
'name': 'custom/preprocessor',
'files': [{'root': 'src', 'glob': '**/*', 'exclude': [], 'type': GlobType.Template}],
'config': {'stripComments': True}
},
# Stage 2: Main templating
{
'name': 'cyan/default',
'files': [{'root': 'src', 'glob': '**/*', 'exclude': [], 'type': GlobType.Template}],
'config': {'vars': {'name': name, 'version': version}}
},
# Stage 3: Postprocess
{
'name': 'custom/formatter',
'files': [{'root': 'src', 'glob': '**/*', 'exclude': [], 'type': GlobType.Template}],
'config': {'format': 'prettier'}
}
]
}
Config.cs
using Atomicloud.CyanSdk;
// In template configuration - this is part of a template's return statement
return new TemplateResult
{
Processors = new[]
Array of processor stages, executed in order
{
// Stage 1: Preprocess
new ProcessorConfig
{
Name = "custom/preprocessor",
Files = new[] { new FileGlob { Root = "src", Glob = "**/*", Exclude = Array.Empty<string>(), Type = GlobType.Template } },
Config = new { StripComments = true }
},
// Stage 2: Main templating
new ProcessorConfig
{
Name = "cyan/default",
Files = new[] { new FileGlob { Root = "src", Glob = "**/*", Exclude = Array.Empty<string>(), Type = GlobType.Template } },
Config = new { Vars = new { Name = name, Version = version } }
},
// Stage 3: Postprocess
new ProcessorConfig
{
Name = "custom/formatter",
Files = new[] { new FileGlob { Root = "src", Glob = "**/*", Exclude = Array.Empty<string>(), Type = GlobType.Template } },
Config = new { Format = "prettier" }
}
}
};

Separation of Concerns

Processors enable clean separation between:

ConcernHandled By
User questionsTemplate (IInquirer)
File transformationProcessor
Post-generation actionsPlugin

Extensibility

Processors make CyanPrint extensible:

  1. Core team provides default processor
  2. Partners create specialized processors
  3. Users create custom processors for specific needs

Processors are the primary extension point for file transformation in CyanPrint. If you need to change how files are processed, you need a processor.

When to Use Custom Processors

Use Custom Processor When:

  • You need a different templating engine
  • You're doing code generation
  • You need AST-level transformations
  • You have conditional processing logic
  • You're transforming binary files
  • You need multi-step processing

Use Default Processor When:

  • Simple variable substitution is enough
  • You want to use Eta templating
  • No special transformation logic needed