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:
| Engine | Syntax | Use 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:
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' directoryroot: 'schemas',glob: '**/*.graphql',exclude: [],type: GlobType.Template});for (const schema of schemas) {// generateTypeScript is a user-defined functionconst types = generateTypeScript(schema.content);schema.relative = schema.relative.replace('.graphql', '.generated.ts');Changes output file extensionschema.content = types;schema.writeFile();}return { directory: input.writeDir };});
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 functiontypes = generate_type_script(schema.content)schema.relative = schema.relative.replace('.graphql', '.generated.ts')Changes output file extensionschema.content = typesschema.write_file()return {'directory': input.write_dir}
using Atomicloud.CyanSdk;// GraphQL schema -> TypeScript types (illustrative example)[ProcessorMain]public async Task<ProcessorResult> RunAsync(ProcessorInput input, CyanFileHelper fileHelper){var schemas = fileHelper.Read(new FileGlobReads 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 functionvar types = GenerateTypeScript(schema.Content);schema.Relative = schema.Relative.Replace(".graphql", ".generated.ts");Changes output file extensionschema.Content = types;schema.WriteFile();}return new ProcessorResult { Directory = input.WriteDir };}
AST Transformations
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-definedconst ast = parseTypeScript(file.content);Parse source to ASTaddDeprecationComments(ast);file.content = printTypeScript(ast);Convert AST back to codefile.writeFile();}return { directory: input.writeDir };});
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-definedast = parse_type_script(file.content)Parse source to ASTadd_deprecation_comments(ast)file.content = print_type_script(ast)Convert AST back to codefile.write_file()return {'directory': input.write_dir}
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-definedvar ast = ParseTypeScript(file.Content);Parse source to ASTAddDeprecationComments(ast);file.Content = PrintTypeScript(ast);Convert AST back to codefile.WriteFile();}return new ProcessorResult { Directory = input.WriteDir };}
Conditional Processing
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 filesfor (const file of files) {if (config.env === 'prod') {// removeDebugCode is a user-defined functionfile.content = removeDebugCode(file.content);}file.writeFile();}return { directory: input.writeDir };});
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' keyfiles = file_helper.resolve_all()Copies Copy-type files, then returns Template-type filesfor file in files:if config['env'] == 'prod':# remove_debug_code is a user-defined functionfile.content = remove_debug_code(file.content)file.write_file()return {'directory': input.write_dir}
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 filesforeach (var file in files){if (config.env == "prod"){// RemoveDebugCode is a user-defined functionfile.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.
import { GlobType } from '@atomicloud/cyan-sdk';// In template configuration - this is part of a template's return statementreturn {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' }}]};
from atomicloud_cyan_sdk import GlobType# In template configuration - this is part of a template's return statementreturn {'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'}}]}
using Atomicloud.CyanSdk;// In template configuration - this is part of a template's return statementreturn new TemplateResult{Processors = new[]Array of processor stages, executed in order{// Stage 1: Preprocessnew 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 templatingnew 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: Postprocessnew 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:
| Concern | Handled By |
|---|---|
| User questions | Template (IInquirer) |
| File transformation | Processor |
| Post-generation actions | Plugin |
Extensibility
Processors make CyanPrint extensible:
- Core team provides default processor
- Partners create specialized processors
- 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