configforge
v1.8.0
Published
Universal config converter framework with exceptional developer experience
Maintainers
Readme
ConfigForge
A TypeScript library for converting, mapping, and transforming config data in JSON and YAML.
ConfigForge is a universal config converter library with a fluent API
ConfigForge makes it easy to convert configuration files between different formats and structures. Just define your mappings and let ConfigForge handle the rest.
🚀 Quick Start
Installation
npm install configforgeBasic Usage
const { forge } = require('configforge');
// Your source configuration
const sourceConfig = {
name: 'MyApp',
version: '1.0',
author: {
firstName: 'John',
lastName: 'Doe',
},
items: ['apple', 'banana', 'cherry'],
};
// Create converter and define mappings
const converter = forge()
.from('source')
.to('target')
.map('name', 'appName')
.map('version', 'appVersion')
.map('author.firstName', 'authorName')
.map('items[0]', 'firstItem');
// Convert the data
const result = await converter.convert(sourceConfig);
console.log(result.data);
// Output:
// {
// appName: 'MyApp',
// appVersion: '1.0',
// authorName: 'John',
// firstItem: 'apple'
// }String Input Support ⭐ NEW!
ConfigForge now supports converting raw YAML and JSON strings directly:
const { forge } = require('configforge');
// Raw YAML string
const yamlString = `
name: MyApp
version: 2.0
database:
host: localhost
port: 5432
`;
// Raw JSON string
const jsonString = `{
"name": "MyApp",
"version": "2.0",
"database": {
"host": "localhost",
"port": 5432
}
}`;
const converter = forge()
.map('name', 'appName')
.map('version', 'appVersion')
.map('database.host', 'dbHost');
// Method 1: Using convertString() (recommended)
const yamlResult = await converter.convertString(yamlString);
const jsonResult = await converter.convertString(jsonString);
// Method 2: Using convert() with isRawContent option
const result = await converter.convert(yamlString, { isRawContent: true });
console.log(result.data);
// Output:
// {
// appName: 'MyApp',
// appVersion: '2.0',
// dbHost: 'localhost'
// }📖 How It Works
1. Create a Converter
const converter = forge()
.from('sourceSchema') // Define source
.to('targetSchema'); // Define target2. Map Fields
// Simple field mapping
.map('oldField', 'newField')
// Nested object mapping
.map('user.profile.name', 'displayName')
// Array element mapping
.map('items[0]', 'firstItem')
.map('items[1]', 'secondItem')3. Add Transformations
// Transform values during mapping
.map('name', 'displayName', (value) => value.toUpperCase())
// Access source data in transforms
.map('firstName', 'fullName', (firstName, ctx) => {
return `${firstName} ${ctx.source.lastName}`;
})4. Add Conditional Logic
// Function conditions - only apply when condition returns true
.when(source => source.user?.type === 'admin')
.map('user.permissions', 'adminPermissions')
.map('user.role', 'adminRole')
.end()
// String conditions - only apply when field exists and is truthy
.when('user.isActive')
.map('user.lastLogin', 'lastActiveLogin')
.end()5. Merge Multiple Fields
// Combine multiple source fields into one target field
.merge(['firstName', 'lastName'], 'fullName', (first, last) => `${first} ${last}`)
// Merge with transformation
.merge(['basePrice', 'tax'], 'totalPrice',
(price, tax) => price + tax,
total => `$${total.toFixed(2)}`
)
// Merge complex data
.merge(['user.profile', 'user.settings'], 'userInfo', (profile, settings) => ({
...profile,
...settings
}))6. Set Default Values
// Set static default values
.defaults({
version: '1.0.0',
enabled: true,
environment: 'production'
})
// Set dynamic default values using functions
.defaults({
timestamp: () => new Date().toISOString(),
id: () => `user_${Date.now()}`,
sessionId: () => Math.random().toString(36).substr(2, 9)
})7. Add Lifecycle Hooks
// Before hooks - run before conversion starts
.before((sourceData) => {
console.log('Starting conversion...');
// Modify source data if needed
return {
...sourceData,
processed: true
};
})
// After hooks - run after conversion completes
.after((targetData) => {
console.log('Conversion completed!');
// Add computed fields or modify target data
return {
...targetData,
processedAt: new Date().toISOString(),
checksum: calculateChecksum(targetData)
};
})
// Multiple hooks execute in order
.before(validateInput)
.before(preprocessData)
.after(addMetadata)
.after(logResults)
// Async hooks are supported
.before(async (data) => {
const enrichedData = await fetchAdditionalData(data.userId);
return { ...data, ...enrichedData };
})8. Add Validation
const { validators } = require('configforge');
// Add field validation
.validate('email', validators.email)
.validate('age', validators.all(
validators.required,
validators.type('number'),
validators.range(18, 120)
))
.validate('username', validators.all(
validators.required,
validators.minLength(3),
validators.pattern(/^[a-zA-Z0-9_]+$/, 'Only letters, numbers, and underscores allowed')
))
// Custom validation with context
.validate('password', (value, context) => {
if (context.source.confirmPassword !== value) {
return 'Passwords do not match';
}
return validators.minLength(8)(value, context);
})9. Convert Data
// Convert object data
const result = await converter.convert(sourceConfig);
// Convert from file
const result = await converter.convert('./config.yml');
// Convert raw string content (YAML or JSON) ⭐ NEW!
const yamlString = `
name: MyApp
version: 1.0.0
database:
host: localhost
port: 5432
`;
const result = await converter.convertString(yamlString);
// Alternative: Use convert() with isRawContent option
const result = await converter.convert(yamlString, { isRawContent: true });
// Synchronous conversion (objects only)
const result = converter.convertSync(sourceConfig);🔧 Working with Results
const result = await converter.convert(data);
// Access converted data
console.log(result.data);
// Check conversion statistics
console.log(result.stats);
// {
// fieldsProcessed: 10,
// fieldsMapped: 5,
// fieldsUnmapped: 5,
// transformsApplied: 2,
// duration: 3
// }
// See unmapped fields
console.log(result.unmapped);
// ['author.lastName', 'items[1]', 'items[2]']
// Handle errors
if (result.errors.length > 0) {
// Technical errors (with stack traces)
console.log(result.errors);
// User-friendly errors (clean messages) ⭐ NEW!
console.log(result.getUserErrors());
// ['Failed to parse config.yml: Map keys must be unique at line 22']
}
// Pretty print report
result.print();
// Save to file
await result.save('./output.json'); // Saves as JSON
await result.save('./output.yml'); // Saves as YAML
// Get as string
const jsonString = result.toJSON();
const yamlString = result.toYAML();📝 Real Examples
Example 1: Simple Config Transformation
const { forge } = require('configforge');
const oldConfig = {
app_name: 'MyApp',
app_version: '2.1.0',
database: {
host: 'localhost',
port: 5432,
},
};
const converter = forge()
.from('old')
.to('new')
.map('app_name', 'name')
.map('app_version', 'version')
.map('database.host', 'db.hostname')
.map('database.port', 'db.port')
.defaults({
environment: 'development',
});
const result = await converter.convert(oldConfig);
console.log(result.data);
// {
// name: 'MyApp',
// version: '2.1.0',
// db: {
// hostname: 'localhost',
// port: 5432
// },
// environment: 'development'
// }Example 2: With Transformations
const userConfig = {
user: {
first_name: 'john',
last_name: 'doe',
email: '[email protected]',
},
settings: {
theme: 'dark',
notifications: 'true',
},
};
const converter = forge()
.from('user')
.to('profile')
.map(
'user.first_name',
'name',
name => name.charAt(0).toUpperCase() + name.slice(1)
)
.map('user.email', 'email', email => email.toLowerCase())
.map('user.first_name', 'displayName', (firstName, ctx) => {
const lastName = ctx.source.user.last_name;
return `${firstName} ${lastName}`.replace(/\b\w/g, l => l.toUpperCase());
})
.map(
'settings.notifications',
'notificationsEnabled',
value => value === 'true'
);
const result = await converter.convert(userConfig);
console.log(result.data);
// {
// name: 'John',
// email: '[email protected]',
// displayName: 'John Doe',
// notificationsEnabled: true
// }Example 3: Array Processing with forEach() ⭐ ENHANCED!
// Simple fruit inventory
const fruitData = {
storeId: 'STORE-001',
fruits: [
{
name: 'Apple',
color: 'red',
price: 1.5,
quantity: 100,
},
{
name: 'Banana',
color: 'yellow',
price: 0.75,
quantity: 80,
},
{
name: 'Orange',
color: 'orange',
price: 1.25,
quantity: 60,
},
],
};
// Method 1: Basic forEach (maps to same field name)
const converter1 = forge()
.from('inventory')
.to('catalog')
.map('storeId', 'storeId')
.forEach('fruits', (fruit, index) => {
// index is always a number for arrays ⭐ NEW!
return {
id: index + 1, // Now works correctly!
fruitName: fruit.name,
displayColor: fruit.color,
pricePerItem: fruit.price,
inStock: fruit.quantity,
totalValue: fruit.price * fruit.quantity,
isExpensive: fruit.price > 1.0,
};
});
// Method 2: forEach with target renaming ⭐ NEW!
const converter2 = forge()
.from('inventory')
.to('catalog')
.map('storeId', 'storeId')
.forEach('fruits', 'products', (fruit, index) => {
// Rename 'fruits' to 'products' in output
return {
id: index + 1,
name: fruit.name,
price: fruit.price,
available: fruit.quantity > 0,
category: fruit.price > 1.0 ? 'premium' : 'budget',
};
});
const result1 = await converter1.convert(fruitData);
console.log('Basic forEach result:', result1.data);
// {
// storeId: 'STORE-001',
// fruits: [
// {
// id: 1,
// fruitName: 'Apple',
// displayColor: 'red',
// pricePerItem: 1.50,
// inStock: 100,
// totalValue: 150,
// isExpensive: true
// },
// // ... more fruits
// ]
// }
const result2 = await converter2.convert(fruitData);
console.log('Renamed forEach result:', result2.data);
// {
// storeId: 'STORE-001',
// products: [ // ← Renamed from 'fruits' to 'products'
// {
// id: 1,
// name: 'Apple',
// price: 1.50,
// available: true,
// category: 'premium'
// },
// // ... more products
// ]
// }Example 4: Array to Object Transformation with Enhanced forEach() ⭐ NEW!
// NPC data from Citizens plugin (array format)
const citizensData = {
sourcePlugin: 'Citizens',
npc: [
{
uuid: '9e1d174d-bc6c-495e-bd70-17e60327b716',
name: 'Guard Captain',
level: 25,
location: { x: 100, y: 64, z: 200 },
},
{
uuid: '1731110a-3586-48cd-859c-a9d3ea2c7797',
name: 'Village Merchant',
level: 15,
location: { x: 150, y: 64, z: 180 },
},
{
uuid: '63ac9d5b-9d5e-461a-9bf7-656361692f8',
name: 'Wise Wizard',
level: 50,
location: { x: 80, y: 70, z: 220 },
},
],
};
// Convert array to object with UUID keys using enhanced forEach
const converter = forge()
.from('Citizens')
.to('FancyNPCs')
.map('sourcePlugin', 'plugin')
.forEach(
'npc',
'npcs',
(npc, index) => {
// Return object where UUID becomes the key
return {
[npc.uuid]: {
id: index + 1,
displayName: npc.name,
level: npc.level,
position: {
world: 'world',
x: npc.location.x,
y: npc.location.y,
z: npc.location.z,
},
settings: {
invulnerable: npc.level > 30,
glowing: npc.level >= 50,
},
},
};
},
{ output: 'object' }
); // ⭐ NEW: Enhanced forEach with object output
const result = await converter.convert(citizensData);
console.log(result.data);
// {
// plugin: 'Citizens',
// npcs: {
// '9e1d174d-bc6c-495e-bd70-17e60327b716': {
// id: 1,
// displayName: 'Guard Captain',
// level: 25,
// position: { world: 'world', x: 100, y: 64, z: 200 },
// settings: { invulnerable: false, glowing: false }
// },
// '1731110a-3586-48cd-859c-a9d3ea2c7797': {
// id: 2,
// displayName: 'Village Merchant',
// level: 15,
// position: { world: 'world', x: 150, y: 64, z: 180 },
// settings: { invulnerable: false, glowing: false }
// },
// '63ac9d5b-9d5e-461a-9bf7-656361692f8': {
// id: 3,
// displayName: 'Wise Wizard',
// level: 50,
// position: { world: 'world', x: 80, y: 70, z: 220 },
// settings: { invulnerable: true, glowing: true }
// }
// }
// }
// Key differences between forEach output options:
//
// forEach(..., { output: 'array' }) - Default behavior:
// npcs: [
// { id: 1, name: 'Guard' }, // npcs[0]
// { id: 2, name: 'Merchant' } // npcs[1]
// ]
//
// forEach(..., { output: 'object' }) - Object transformation:
// npcs: {
// 'uuid-1': { id: 1, name: 'Guard' }, // npcs['uuid-1']
// 'uuid-2': { id: 2, name: 'Merchant' } // npcs['uuid-2']
// }
// Advanced usage with filtering
const activeUsersConverter = forge().forEach(
'users',
'activeUsers',
(user, index) => {
// Filter out inactive users by returning null
if (!user.active) return null;
return {
[user.id]: {
name: user.name,
lastLogin: user.lastLogin,
index: index + 1,
},
};
},
{ output: 'object' }
);
// Works with objects too - transforms object keys
const servicesConverter = forge().forEach(
'config',
'services',
(service, key) => {
return {
[`${key}_service`]: {
type: key,
endpoint: `${service.host}:${service.port}`,
config: service,
},
};
},
{ output: 'object' }
);Example 5: Object Flattening with forEach() ⭐ NEW!
// Problem: You have nested configuration with wrapper objects that you want to remove
const announcementsConfig = {
plugin: 'AutoAnnounce',
version: '2.1.0',
Announcements: {
store: {
interval: 61,
sound: 'ENTITY_EXPERIENCE_ORB_PICKUP',
lines: [
'<center>&6Visit our store!</center>',
'<center>&eGet 20% off today!</center>',
],
},
discord: {
interval: 120,
sound: 'ENTITY_PLAYER_LEVELUP',
lines: [
'<center>&9Join our Discord!</center>',
'<center>&b/discord for the link</center>',
],
},
events: {
interval: 300,
sound: 'ENTITY_FIREWORK_ROCKET_LAUNCH',
lines: [
'<center>&dUpcoming Events!</center>',
'<center>&fCheck /events</center>',
],
},
},
};
// Goal: Remove the "Announcements" wrapper and promote child objects to root level
const converter = forge()
.from('announcements')
.to('schedules')
.map('plugin', 'sourcePlugin')
.map('version', 'configVersion')
.forEach(
'Announcements',
(announcement, key) => {
return {
[key]: {
Enabled: true,
MinPlayers: 1,
FeedBack: false,
Repeat: true,
Delay: announcement.interval,
Commands: announcement.lines.map(line => {
// Clean up HTML tags and convert to broadcast commands
const cleanLine = line.replace(/<\/?center>/g, '');
return `broadcast! ${cleanLine}`;
}),
},
};
},
{ flatten: true, output: 'object' } // ⭐ NEW: Flatten removes the wrapper!
);
const result = await converter.convert(announcementsConfig);
console.log(result.data);
// Without flatten (old behavior):
// {
// sourcePlugin: 'AutoAnnounce',
// configVersion: '2.1.0',
// Announcements: { ← Wrapper object remains
// store: { Enabled: true, MinPlayers: 1, ... },
// discord: { Enabled: true, MinPlayers: 1, ... },
// events: { Enabled: true, MinPlayers: 1, ... }
// }
// }
// With flatten: true (new behavior):
// {
// sourcePlugin: 'AutoAnnounce',
// configVersion: '2.1.0',
// store: { ← Direct at root level!
// Enabled: true,
// MinPlayers: 1,
// FeedBack: false,
// Repeat: true,
// Delay: 61,
// Commands: [
// 'broadcast! &6Visit our store!',
// 'broadcast! &eGet 20% off today!'
// ]
// },
// discord: { ← Direct at root level!
// Enabled: true,
// MinPlayers: 1,
// FeedBack: false,
// Repeat: true,
// Delay: 120,
// Commands: [
// 'broadcast! &9Join our Discord!',
// 'broadcast! &b/discord for the link'
// ]
// },
// events: { ← Direct at root level!
// Enabled: true,
// MinPlayers: 1,
// FeedBack: false,
// Repeat: true,
// Delay: 300,
// Commands: [
// 'broadcast! &dUpcoming Events!',
// 'broadcast! &fCheck /events'
// ]
// }
// }
// Advanced: Flatten with filtering
const activeOnlyConverter = forge().forEach(
'services',
(service, name) => {
// Filter out disabled services by returning null
if (!service.enabled) {
return null;
}
return {
[name]: {
port: service.port,
status: 'running',
healthCheck: `http://localhost:${service.port}/health`,
},
};
},
{ flatten: true, output: 'object' }
);
// Flatten works with arrays too!
const userArrayConverter = forge().forEach(
'users',
(user, index) => {
return {
[user.id]: {
name: user.name,
position: index + 1,
active: user.status === 'active',
},
};
},
{ flatten: true, output: 'object' }
);
// Input: { users: [{ id: 'u1', name: 'John' }, { id: 'u2', name: 'Jane' }] }
// Output: { u1: { name: 'John', position: 1, active: true }, u2: { name: 'Jane', position: 2, active: true } }Example 6: Conditional Mapping with when()
// Different types of pets need different mappings
const petData = {
type: 'dog',
name: 'Buddy',
age: 3,
dogInfo: {
breed: 'Golden Retriever',
tricks: ['sit', 'stay', 'fetch'],
},
catInfo: {
breed: 'Persian',
indoor: true,
},
birdInfo: {
species: 'Parrot',
canTalk: true,
},
};
const converter = forge()
.from('petData')
.to('petProfile')
.map('name', 'petName')
.map('age', 'petAge')
.map('type', 'animalType')
// Only map dog-specific info for dogs
.when(source => source.type === 'dog')
.map('dogInfo.breed', 'breed')
.map('dogInfo.tricks', 'knownTricks')
.end()
// Only map cat-specific info for cats
.when(source => source.type === 'cat')
.map('catInfo.breed', 'breed')
.map('catInfo.indoor', 'isIndoorCat')
.end()
// Only map bird-specific info for birds
.when(source => source.type === 'bird')
.map('birdInfo.species', 'species')
.map('birdInfo.canTalk', 'canSpeak')
.end();
const result = await converter.convert(petData);
console.log(result.data);
// {
// petName: 'Buddy',
// petAge: 3,
// animalType: 'dog',
// breed: 'Golden Retriever',
// knownTricks: ['sit', 'stay', 'fetch']
// }Example 5: Merge Multiple Fields with merge()
// Simple student report card
const studentData = {
student: {
firstName: 'Alice',
lastName: 'Smith',
},
grades: {
math: 85,
english: 92,
science: 78,
},
activities: ['soccer', 'chess', 'art'],
info: {
grade: '5th',
teacher: 'Ms. Johnson',
},
};
const converter = forge()
.from('report')
.to('summary')
// Merge student name fields
.merge(
['student.firstName', 'student.lastName'],
'studentName',
(first, last) => `${first} ${last}`
)
// Calculate total with transformation
.merge(
['order.basePrice', 'order.tax', 'order.shipping'],
'subtotal',
(base, tax, shipping) => base + tax + shipping
)
.merge(
['order.basePrice', 'order.tax', 'order.shipping', 'order.discount'],
'total',
(base, tax, shipping, discount) => base + tax + shipping - discount,
total => `$${total.toFixed(2)}`
) // Transform to currency format
// Merge arrays and objects
.merge(['items', 'metadata'], 'orderSummary', (items, meta) => ({
itemCount: items.length,
items: items.join(', '),
...meta,
}))
// Simple field mappings
.map('customer.email', 'billingEmail');
const result = await converter.convert(orderData);
console.log(result.data);
// {
// customerName: 'John Doe',
// subtotal: 121.49,
// total: '$106.49',
// orderSummary: {
// itemCount: 3,
// items: 'laptop, mouse, keyboard',
// orderDate: '2023-12-01',
// source: 'web'
// },
// billingEmail: '[email protected]'
// }Example 6: Data Validation
const { forge, validators } = require('configforge');
// User registration form data
const formData = {
personalInfo: {
firstName: 'John',
lastName: 'Doe',
email: '[email protected]',
age: 28,
},
accountInfo: {
username: 'johndoe123',
password: 'SecurePass123!',
confirmPassword: 'SecurePass123!',
},
preferences: {
newsletter: 'yes',
theme: 'dark',
language: 'en',
},
};
const converter = forge()
.from('form')
.to('user')
// Map fields
.merge(
['personalInfo.firstName', 'personalInfo.lastName'],
'fullName',
(first, last) => `${first} ${last}`
)
.map('personalInfo.email', 'email')
.map('personalInfo.age', 'age')
.map('accountInfo.username', 'username')
.map('accountInfo.password', 'password')
.map(
'preferences.newsletter',
'subscribeToNewsletter',
value => value === 'yes'
)
.map('preferences.theme', 'theme')
.map('preferences.language', 'language')
// Add validation rules
.validate(
'fullName',
validators.all(
validators.required,
validators.minLength(2),
validators.maxLength(100)
)
)
.validate('email', validators.all(validators.required, validators.email))
.validate(
'age',
validators.all(
validators.required,
validators.type('number'),
validators.range(13, 120)
)
)
.validate(
'username',
validators.all(
validators.required,
validators.minLength(3),
validators.maxLength(20),
validators.pattern(
/^[a-zA-Z0-9_]+$/,
'Username can only contain letters, numbers, and underscores'
)
)
)
.validate(
'password',
validators.all(
validators.required,
validators.minLength(8),
validators.pattern(
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/,
'Password must contain lowercase, uppercase, and number'
)
)
)
.validate('theme', validators.oneOf(['light', 'dark', 'auto']))
.validate('language', validators.oneOf(['en', 'es', 'fr', 'de']))
// Custom validation
.validate('password', (value, context) => {
if (context.source.accountInfo.confirmPassword !== value) {
return 'Passwords do not match';
}
return true;
});
const result = await converter.convert(formData);
if (result.errors.length > 0) {
console.log('Validation errors:');
result.errors.forEach(error => {
console.log(`- ${error.path}: ${error.message}`);
});
} else {
console.log('User data is valid!');
console.log(result.data);
// {
// fullName: 'John Doe',
// email: '[email protected]',
// age: 28,
// username: 'johndoe123',
// password: 'SecurePass123!',
// subscribeToNewsletter: true,
// theme: 'dark',
// language: 'en'
// }
}Example 7: Defaults and Hooks
// Student data with missing fields
const studentData = {
student: {
firstName: 'Alice',
lastName: 'Smith',
// age is missing
},
grades: {
math: 85,
english: 92,
// science grade is missing
},
// activities array is missing
info: {
grade: '5th',
teacher: 'Ms. Johnson',
// school year is missing
},
};
const converter = forge()
.from('studentReport')
.to('completeProfile')
// Basic mappings
.map('student.firstName', 'firstName')
.map('student.lastName', 'lastName')
.map('student.age', 'age')
.map('grades.math', 'mathGrade')
.map('grades.english', 'englishGrade')
.map('grades.science', 'scienceGrade')
.map('activities', 'extracurriculars')
.map('info.grade', 'gradeLevel')
.map('info.teacher', 'teacher')
.map('info.schoolYear', 'academicYear')
// Set default values for missing fields
.defaults({
age: 10, // Default age for 5th graders
scienceGrade: 80, // Default science grade
extracurriculars: ['reading'], // Default activity
academicYear: () => {
// Dynamic default - current school year
const now = new Date();
const year = now.getFullYear();
const month = now.getMonth();
return month >= 8 ? `${year}-${year + 1}` : `${year - 1}-${year}`;
},
status: 'active',
lastUpdated: () => new Date().toISOString(),
})
// Add before hook to log conversion start
.before(data => {
console.log('🔄 Starting conversion...');
console.log(
`Processing student: ${data.student?.firstName} ${data.student?.lastName}`
);
return data; // Return the data unchanged
})
// Add after hook to calculate grade average
.after(data => {
console.log('✅ Conversion completed!');
// Create full name from first and last name
if (data.firstName && data.lastName) {
data.fullName = `${data.firstName} ${data.lastName}`;
}
// Calculate and add grade average
const { mathGrade, englishGrade, scienceGrade } = data;
if (mathGrade && englishGrade && scienceGrade) {
data.gradeAverage = Math.round(
(mathGrade + englishGrade + scienceGrade) / 3
);
console.log(`Calculated grade average: ${data.gradeAverage}`);
}
return data; // Return the modified data
});
const result = await converter.convert(studentData);
console.log(result.data);
// {
// firstName: 'Alice',
// lastName: 'Smith',
// mathGrade: 85,
// englishGrade: 92,
// gradeLevel: '5th',
// teacher: 'Ms. Johnson',
// age: 10,
// scienceGrade: 80,
// extracurriculars: ['reading'],
// academicYear: '2024-2025',
// status: 'active',
// lastUpdated: '2024-12-23T06:22:05.491Z',
// fullName: 'Alice Smith',
// gradeAverage: 86
// }Example 8: String Input Support
const { forge } = require('configforge');
// Example: Converting raw YAML string content
const yamlConfigString = `
# Server Configuration
server:
host: localhost
port: 8080
ssl: true
# Database Configuration
database:
type: postgresql
host: db.example.com
port: 5432
name: myapp
credentials:
username: admin
password: secret123
# Features
features:
- authentication
- logging
- monitoring
`;
// Example: Converting raw JSON string content
const jsonConfigString = `{
"api": {
"version": "v2",
"baseUrl": "https://api.example.com",
"timeout": 30000
},
"cache": {
"enabled": true,
"ttl": 3600,
"provider": "redis"
},
"logging": {
"level": "info",
"format": "json"
}
}`;
// Create converter for YAML string
const yamlConverter = forge()
.from('legacy-config')
.to('standard-config')
.map('server.host', 'app.host')
.map('server.port', 'app.port')
.map('server.ssl', 'app.secure')
.map('database.host', 'db.host')
.map('database.port', 'db.port')
.map('database.name', 'db.database')
.map('database.credentials.username', 'db.user')
.map('database.credentials.password', 'db.password')
.map('features', 'app.enabledFeatures')
.defaults({
'app.environment': 'production',
'app.debug': false,
});
// Method 1: Using convertString() - recommended for string content
const yamlResult = await yamlConverter.convertString(yamlConfigString);
console.log('YAML Result:', yamlResult.data);
// {
// app: {
// host: 'localhost',
// port: 8080,
// secure: true,
// enabledFeatures: ['authentication', 'logging', 'monitoring'],
// environment: 'production',
// debug: false
// },
// db: {
// host: 'db.example.com',
// port: 5432,
// database: 'myapp',
// user: 'admin',
// password: 'secret123'
// }
// }
// Method 2: Using convert() with isRawContent option
const alternativeResult = await yamlConverter.convert(yamlConfigString, {
isRawContent: true,
});
console.log('Alternative Result:', alternativeResult.data);
// Create converter for JSON string
const jsonConverter = forge()
.from('api-config')
.to('service-config')
.map('api.version', 'service.apiVersion')
.map('api.baseUrl', 'service.endpoint')
.map('api.timeout', 'service.requestTimeout')
.map('cache.enabled', 'service.caching.enabled')
.map('cache.ttl', 'service.caching.duration')
.map('logging.level', 'service.log.level')
.defaults({
'service.retries': 3,
'service.caching.provider': 'memory',
});
// Convert JSON string content
const jsonResult = await jsonConverter.convertString(jsonConfigString);
console.log('JSON Result:', jsonResult.data);
// {
// service: {
// apiVersion: 'v2',
// endpoint: 'https://api.example.com',
// requestTimeout: 30000,
// caching: {
// enabled: true,
// duration: 3600,
// provider: 'memory'
// },
// log: {
// level: 'info'
// },
// retries: 3
// }
// }
// Format auto-detection works for both YAML and JSON
const mixedConverter = forge()
.map('name', 'appName')
.map('version', 'appVersion');
// Auto-detects YAML format
const yamlData = await mixedConverter.convertString(`
name: MyApp
version: 1.0.0
`);
// Auto-detects JSON format
const jsonData = await mixedConverter.convertString(`{
"name": "MyApp",
"version": "1.0.0"
}`);
// Error handling for malformed content
try {
const malformedResult = await converter.convertString(`{
"name": "Test"
"missing": "comma"
}`);
} catch (error) {
console.log('Parse error handled gracefully:', error.message);
}Example 9: File Conversion
// Convert YAML file to JSON structure
const converter = forge()
.from('yaml')
.to('json')
.map('server.host', 'hostname')
.map('server.port', 'port')
.map('database.url', 'dbUrl');
// Read and convert file
const result = await converter.convert('./config.yml');
// Save as JSON
await result.save('./config.json');Example 9: Plugin System with Built-in Plugins
const {
forge,
createValidationPlugin,
createAuditPlugin,
createBackupPlugin,
} = require('configforge');
// Create converter with plugin support
const converter = forge()
.from('userForm')
.to('userProfile')
.map('personalInfo.firstName', 'firstName')
.map('personalInfo.lastName', 'lastName')
.map('personalInfo.email', 'email')
.map('accountInfo.username', 'username')
.merge(
['personalInfo.firstName', 'personalInfo.lastName'],
'fullName',
(first, last) => `${first} ${last}`
);
// Add built-in plugins
const validationPlugin = createValidationPlugin({
strictMode: true,
failOnWarnings: false,
});
const auditPlugin = createAuditPlugin({
logFile: './conversion-audit.log',
logLevel: 'detailed',
includeData: false,
});
const backupPlugin = createBackupPlugin({
backupDir: './backups',
keepVersions: 5,
timestampFormat: 'YYYY-MM-DD_HH-mm-ss',
});
// Install plugins
await converter.use(validationPlugin);
await converter.use(auditPlugin);
await converter.use(backupPlugin);
// Convert with plugin enhancements
const userData = {
personalInfo: {
firstName: 'John',
lastName: 'Doe',
email: '[email protected]',
},
accountInfo: {
username: 'johndoe123',
},
};
const result = await converter.convert(userData);
// Plugins automatically:
// - Validate data quality (ValidationPlugin)
// - Log conversion activities (AuditPlugin)
// - Create backups of source data (BackupPlugin)
// - Handle errors gracefully with detailed reporting
console.log(result.data);
// {
// firstName: 'John',
// lastName: 'Doe',
// email: '[email protected]',
// username: 'johndoe123',
// fullName: 'John Doe'
// }
// Check plugin results
console.log('Validation results:', result.context?.validationResults);
console.log('Backup created:', result.context?.backupPath);Example 10: Multi-File Processing ⭐ NEW!
ConfigForge makes it easy to work with multiple files at once. Think of it like organizing your music collection - you might have individual song files that you want to combine into a playlist, or a big playlist that you want to split into smaller themed playlists.
const { forge } = require('configforge');
// Example 1: Combining Multiple Files (Many-to-One)
// Like combining individual recipe cards into a cookbook
// You have separate recipe files:
const recipeFiles = [
{
filename: 'chocolate-cake.yml',
content: `
name: Chocolate Cake
category: dessert
servings: 8
ingredients:
- flour: 2 cups
- sugar: 1.5 cups
- cocoa: 0.5 cups
cookTime: 45
difficulty: medium
`,
},
{
filename: 'pasta-salad.yml',
content: `
name: Pasta Salad
category: main
servings: 6
ingredients:
- pasta: 1 lb
- tomatoes: 2 cups
- cheese: 1 cup
cookTime: 20
difficulty: easy
`,
},
{
filename: 'fruit-smoothie.yml',
content: `
name: Fruit Smoothie
category: drink
servings: 2
ingredients:
- banana: 1
- berries: 1 cup
- yogurt: 0.5 cups
cookTime: 5
difficulty: easy
`,
},
];
// Create a converter to combine recipes into a cookbook
const cookbookConverter = forge()
.from('*.yml')
.to('cookbook.yml')
// Use filename as the recipe key (removes .yml automatically)
.useFilenameAsKey()
// Map recipe fields to cookbook format
.map('name', 'title')
.map('category', 'type')
.map('servings', 'serves')
.map('ingredients', 'ingredientList')
.map('cookTime', 'prepTime')
.map('difficulty', 'skillLevel')
// Add some defaults for the cookbook
.defaults({
tested: true,
addedDate: () => new Date().toISOString().split('T')[0], // Just the date
});
// Convert all recipe files into one cookbook
const cookbook = await cookbookConverter.convert(recipeFiles);
console.log(
'📚 Created cookbook with',
Object.keys(cookbook.data).length,
'recipes'
);
console.log(cookbook.data);
// {
// 'chocolate-cake': {
// title: 'Chocolate Cake',
// type: 'dessert',
// serves: 8,
// ingredientList: [{ flour: '2 cups' }, { sugar: '1.5 cups' }, { cocoa: '0.5 cups' }],
// prepTime: 45,
// skillLevel: 'medium',
// tested: true,
// addedDate: '2024-01-22'
// },
// 'pasta-salad': {
// title: 'Pasta Salad',
// type: 'main',
// serves: 6,
// ingredientList: [{ pasta: '1 lb' }, { tomatoes: '2 cups' }, { cheese: '1 cup' }],
// prepTime: 20,
// skillLevel: 'easy',
// tested: true,
// addedDate: '2024-01-22'
// },
// 'fruit-smoothie': {
// title: 'Fruit Smoothie',
// type: 'drink',
// serves: 2,
// ingredientList: [{ banana: 1 }, { berries: '1 cup' }, { yogurt: '0.5 cups' }],
// prepTime: 5,
// skillLevel: 'easy',
// tested: true,
// addedDate: '2024-01-22'
// }
// }
// Example 2: Splitting One File into Many (One-to-Many)
// Like taking a big address book and creating separate contact cards
const addressBookData = {
filename: 'contacts.yml',
content: `
contacts:
john-doe:
name: John Doe
email: [email protected]
phone: 555-0123
category: friend
city: New York
jane-smith:
name: Jane Smith
email: [email protected]
phone: 555-0456
category: work
city: Boston
mom:
name: Mary Johnson
email: [email protected]
phone: 555-0789
category: family
city: Chicago
`,
};
// Create converter to split contacts into individual cards
const contactConverter = forge()
.from('*.yml')
.to('json')
// Transform each contact
.map('name', 'fullName')
.map('email', 'emailAddress')
.map('phone', 'phoneNumber')
.map('category', 'group')
.map('city', 'location')
// Add some useful defaults
.defaults({
favorite: false,
lastContacted: null,
})
// Split the contacts into separate files
.split(data => {
const result = {};
// Take each contact and make it a separate file
if (data.contacts) {
for (const [contactId, contactInfo] of Object.entries(data.contacts)) {
// Create filename based on contact name and category
const filename = `${contactInfo.category}-${contactId}.yml`;
result[filename] = contactInfo;
}
}
return result;
});
const splitContacts = await contactConverter.convert([addressBookData]);
// Get the individual contact files
const contactFiles = splitContacts.toFiles();
console.log(`📇 Split into ${contactFiles.length} contact cards:`);
contactFiles.forEach(file => {
console.log(`📄 ${file.filename}`);
console.log(file.content.substring(0, 100) + '...');
});
// Output:
// 📇 Split into 3 contact cards:
// 📄 friend-john-doe.yml
// fullName: John Doe
// emailAddress: [email protected]
// phoneNumber: 555-0123
// group: friend
// location: New York...
//
// 📄 work-jane-smith.yml
// fullName: Jane Smith
// emailAddress: [email protected]
// phoneNumber: 555-0456
// group: work
// location: Boston...
//
// 📄 family-mom.yml
// fullName: Mary Johnson
// emailAddress: [email protected]
// phoneNumber: 555-0789
// group: family
// location: Chicago...
// Example 3: Handling Conflicts (When Files Have Same Names)
// Like when you have two recipes both called "cake.yml"
const conflictingFiles = [
{
filename: 'cake.yml',
content: 'name: Chocolate Cake\ntype: dessert\nrating: 5',
},
{
filename: 'cake.yml', // Same filename!
content: 'name: Vanilla Cake\ntype: dessert\nrating: 4',
},
];
// Option 1: Merge conflicts (combine the values)
const mergeConverter = forge()
.from('*.yml')
.to('json')
.useFilenameAsKey()
.onKeyConflict('merge'); // Combine conflicting values into arrays
const mergedResult = await mergeConverter.convert(conflictingFiles);
console.log('🔀 Merged conflicts:', mergedResult.data);
// {
// cake: {
// name: ['Chocolate Cake', 'Vanilla Cake'], // Both names in an array
// type: 'dessert', // Same value, no conflict
// rating: [5, 4] // Both ratings in an array
// }
// }
// Option 2: Error on conflicts (be strict)
const strictConverter = forge()
.from('*.yml')
.to('json')
.useFilenameAsKey()
.onKeyConflict('error'); // Stop and report the problem
try {
await strictConverter.convert(conflictingFiles);
} catch (error) {
console.log('⚠️ Conflict detected:', error.message);
// "Key conflict detected: 'cake' appears in multiple files"
}
// Real-world example: Simple server config merger
const serverConfigs = [
{
filename: 'database.yml',
content: 'host: db.example.com\nport: 5432\nname: myapp',
},
{
filename: 'cache.yml',
content: 'host: cache.example.com\nport: 6379\nttl: 3600',
},
];
const serverConverter = forge()
.from('*.yml')
.to('config.yml')
.useFilenameAsKey()
.map('host', 'hostname')
.map('port', 'port')
.defaults({
enabled: true,
timeout: 30,
});
const serverConfig = await serverConverter.convert(serverConfigs);
console.log('🖥️ Server configuration:', serverConfig.data);
// {
// database: {
// hostname: 'db.example.com',
// port: 5432,
// name: 'myapp',
// enabled: true,
// timeout: 30
// },
// cache: {
// hostname: 'cache.example.com',
// port: 6379,
// ttl: 3600,
// enabled: true,
// timeout: 30
// }
// }Example 11: Batch Processing and Performance Features
const {
forge,
BatchProcessor,
IncrementalProcessor,
PerformanceMonitor,
} = require('configforge');
// Create converter for processing multiple config files
const converter = forge()
.from('legacy-config')
.to('modern-config')
.map('app_name', 'application.name')
.map('app_version', 'application.version')
.map('database.host', 'db.hostname')
.map('database.port', 'db.port')
.map('server.port', 'application.port')
.defaults({
environment: 'production',
createdAt: () => new Date().toISOString(),
});
// 1. Batch Processing - Convert multiple files efficiently
const batchResult = await converter.convertBatch(
['./configs/app1.yml', './configs/app2.yml', './configs/app3.yml'],
{
parallel: true,
workers: 3,
errorStrategy: 'continue',
progressCallback: progress => {
console.log(`Progress: ${progress.completed}/${progress.total} files`);
},
}
);
console.log(
`Processed ${batchResult.stats.successfulFiles} files successfully`
);
console.log(`Failed: ${batchResult.stats.failedFiles} files`);
console.log(`Total duration: ${batchResult.stats.totalDuration}ms`);
// 2. Incremental Processing - Only process changed files
const incrementalResult = await converter.convertIncremental(
'./config.yml',
'./output/config.json',
'./.cache' // Cache directory
);
if (incrementalResult) {
console.log('File was processed (changed since last run)');
} else {
console.log('File skipped (no changes detected)');
}
// 3. Incremental Batch Processing
const incrementalBatchResult = await converter.convertIncrementalBatch(
['./configs/app1.yml', './configs/app2.yml', './configs/app3.yml'],
'./output',
{
cacheDir: './.cache',
getOutputPath: inputPath => {
const fileName = inputPath.split('/').pop().replace('.yml', '.json');
return `./output/${fileName}`;
},
}
);
console.log(`Processed: ${incrementalBatchResult.processed.length} files`);
console.log(
`Skipped: ${incrementalBatchResult.skipped.length} files (unchanged)`
);
// 4. Performance Monitoring
const monitor = new PerformanceMonitor();
monitor.start();
// Track individual steps
monitor.startStep('parse');
const result = await converter.convert('./large-config.yml');
monitor.endStep('parse');
// Record file sizes for throughput calculation
monitor.recordSizes(1024 * 1024, 800 * 1024); // 1MB input, 800KB output
// Get detailed performance metrics
const metrics = monitor.finish(result.stats);
const report = monitor.createReport(metrics);
console.log(report);
// === Performance Report ===
//
// Timing Breakdown:
// Parse: 15.23ms
// Map: 8.45ms
// Transform: 3.21ms
// Validate: 2.10ms
// Serialize: 5.67ms
// Total: 34.66ms
//
// Throughput:
// Fields/sec: 2890
// Transforms/sec: 1445
// Bytes/sec: 29.64 MB/s
//
// Memory Usage:
// Heap Used: 45.23 MB
// Heap Total: 67.89 MB
// RSS: 89.12 MB
//
// File Sizes:
// Input: 1.00 MB
// Output: 800.00 KB
// Ratio: 80.0%
// 5. Performance Benchmarking
const { PerformanceBenchmark } = require('configforge');
const sequentialOperation = async () => {
return converter.convertBatch(['./config1.yml', './config2.yml'], {
parallel: false,
});
};
const parallelOperation = async () => {
return converter.convertBatch(['./config1.yml', './config2.yml'], {
parallel: true,
workers: 2,
});
};
const benchmarks = [
{ name: 'sequential-processing', operation: sequentialOperation },
{ name: 'parallel-processing', operation: parallelOperation },
];
const benchmarkResults = await PerformanceBenchmark.compareBenchmarks(
benchmarks,
10
);
const comparisonReport =
PerformanceBenchmark.createComparisonReport(benchmarkResults);
console.log(comparisonReport);
// === Benchmark Comparison ===
//
// Results (sorted by average time):
// parallel-processing:
// Average: 45.23ms (fastest)
// Range: 42.10ms - 48.90ms
// Std Dev: 2.15ms
// Ops/sec: 22
//
// sequential-processing:
// Average: 78.45ms (1.73x slower)
// Range: 75.20ms - 82.10ms
// Std Dev: 2.89ms
// Ops/sec: 13
// 6. Advanced Batch Processing with Custom Options
const advancedBatchResult = await converter.convertBatch(
[
'./configs/*.yml', // Glob patterns supported
],
{
parallel: true,
workers: 4,
errorStrategy: 'fail-fast', // Stop on first error
progressCallback: progress => {
const percentage = Math.round(
(progress.completed / progress.total) * 100
);
console.log(`[${percentage}%] Processing: ${progress.current}`);
if (progress.failed > 0) {
console.log(`⚠️ ${progress.failed} files failed`);
}
},
}
);
// Save all results to output directory
const batchProcessor = new BatchProcessor(converter);
await batchProcessor.saveBatch(advancedBatchResult.results, './output', [
'./configs/app1.yml',
'./configs/app2.yml',
]);
// 7. Cache Management for Incremental Processing
const incrementalProcessor = new IncrementalProcessor(converter, './.cache');
// Get cache statistics
const cacheStats = incrementalProcessor.getCacheStats();
console.log(`Cache entries: ${cacheStats.totalEntries}`);
console.log(`Successful: ${cacheStats.successfulEntries}`);
console.log(`Failed: ${cacheStats.failedEntries}`);
// Clean up stale cache entries
const removedEntries = await incrementalProcessor.cleanupCache();
console.log(`Removed ${removedEntries} stale cache entries`);
// Clear entire cache
await incrementalProcessor.clearCache();
console.log('Cache cleared');Example 11: CLI Generation System
const { forge, CLIGenerator } = require('configforge');
// Create your converter
const converter = forge()
.from('legacy')
.to('modern')
.map('app_name', 'name')
.map('app_version', 'version')
.map('database.host', 'db.hostname')
.map('database.port', 'db.port')
.defaults({
environment: 'production',
});
// Generate CLI for your converter
const cli = CLIGenerator.forConverter(converter, {
name: 'config-converter',
version: '1.0.0',
description: 'Convert legacy config to modern format',
});
// Add custom commands
cli.addCommand({
name: 'migrate',
description: 'Migrate all configs in a directory',
options: [
{
flags: '-d, --directory <dir>',
description: 'Directory containing config files',
},
],
action: async options => {
// Custom migration logic
console.log(`Migrating configs in ${options.directory}`);
},
});
// Parse command line arguments
cli.parse();
// Now you can use your CLI:
// $ config-converter convert input.yml output.json
// $ config-converter validate config.yml
// $ config-converter profile save my-config
// $ config-converter profile list
// $ config-converter migrate -d ./configs🎯 Key Features
- ✅ Simple API: Just map fields and convert
- ✅ String Input Support: Convert raw YAML/JSON strings directly with
convertString()⭐ NEW! - ✅ Multi-File Processing: Process multiple input files into single output (many-to-one) or split single input into multiple outputs (one-to-many) ⭐ NEW!
- ✅ Split Functionality: Break single input files into multiple outputs with configurable split functions ⭐ NEW!
- ✅ Conflict Resolution: Handle field conflicts in multi-file processing with configurable strategies (error, overwrite, merge, suffix) ⭐ NEW!
- ✅ Enhanced Result Handling: Comprehensive result objects with
getSummary(),toFiles(), and enhancedprint()methods ⭐ NEW! - ✅ Pipeline Architecture: Robust internal processing with configurable steps and error handling
- ✅ No direct Mapper usage needed: The
convert()method handles everything - ✅ Nested object support: Use dot notation like
user.profile.name - ✅ Array access: Use bracket notation like
items[0] - ✅ Transformations: Transform values during mapping
- ✅ Conditional mapping: Use
when()for conditional logic - ✅ Array/object processing: Use
forEach()with flexible output options for iteration and transformation ⭐ ENHANCED! - ✅ Object Flattening: Remove wrapper objects with
forEach(..., { flatten: true })⭐ NEW! - ✅ Smart Field Tracking: Automatically tracks fields accessed in
forEachoperations for accurate mapping statistics ⭐ NEW! - ✅ Multi-field merging: Use
merge()to combine multiple sources into one target - ✅ Default values: Set fallback values
- ✅ Comprehensive Error Handling: Advanced error reporting with context and suggestions ⭐ ENHANCED!
- ✅ CLI Generation System: Create command-line interfaces for converters ⭐ ENHANCED!
- ✅ Plugin System: Extensible architecture with built-in plugins for validation, auditing, and backups ⭐ ENHANCED!
- ✅ Batch Processing: Efficiently process multiple files with parallel support and progress tracking ⭐ ENHANCED!
- ✅ Incremental Processing: Only process changed files using content hashing and modification times ⭐ ENHANCED!
- ✅ Performance Monitoring: Detailed performance tracking, benchmarking, and reporting ⭐ ENHANCED!
- ✅ File support: Convert YAML, JSON files directly
- ✅ Statistics: Get detailed conversion reports
- ✅ TypeScript: Full type safety
🔄 Current Implementation Status
✅ Working Features:
- Basic field mapping with
map() - Nested object and array access
- Value transformations
- String input support with
convertString()andconvert()withisRawContentoption ⭐ NEW! - Enhanced result handling with
getSummary(),toFiles(), and improvedprint()methods ⭐ NEW! - Multi-file processing with
convertMany()(many-to-one) andconvertSplit()(one-to-many) ⭐ NEW! - Complete pipeline integration with multi-file processing ⭐ NEW!
- Multi-file conflict resolution with configurable strategies (error, overwrite, merge, suffix) ⭐ NEW!
- Split functionality with
split()method for one-to-many processing ⭐ NEW! - Default values with
defaults()⭐ ENHANCED! - Array/object processing with
forEach()andmapToObject()⭐ ENHANCED! - Object flattening with
forEach(..., { flatten: true })⭐ NEW! - Conditional mapping with
when() - Multi-field merging with
merge() - Field validation with
validate() - Lifecycle hooks with
before()andafter()⭐ NEW! - Advanced error handling and reporting system ⭐ NEW!
- CLI generation system with command-line interfaces ⭐ ENHANCED!
- Plugin system foundation with built-in plugins ⭐ NEW!
- Batch processing with parallel support and progress tracking ⭐ ENHANCED!
- Incremental processing with content hashing and modification times ⭐ ENHANCED!
- Performance monitoring with detailed benchmarking and reporting ⭐ ENHANCED!
- File parsing (YAML, JSON)
- Conversion statistics and reporting
- Async and sync conversion support
🚧 Coming Soon:
- Advanced plugin ecosystem
💡 Tips
- You don't need to use the Mapper class directly - just use
converter.convert() - Use dot notation for nested objects:
'user.profile.name' - Use bracket notation for arrays:
'items[0]','items[1]' - Transform functions get the value and context:
(value, ctx) => { ... } - Use conditional mapping with
when()for type-specific logic:when(source => source.accountType === 'premium') - Use merge() to combine multiple fields:
merge(['field1', 'field2'], 'result', (a, b) => a + b) - Use validation to ensure data quality:
validate('email', validators.email) - Combine validators with
validators.all()for multiple rules - Always call
.end()after conditional mappings to return to the main converter - Check
result.errorsto see validation failures - Check
result.unmappedto see which fields weren't mapped - Use
result.print()for a nice conversion report - Use
defaults()to provide fallback values for missing fields:defaults({ status: 'active' }) - Use function defaults for dynamic values:
defaults({ timestamp: () => new Date().toISOString() }) - Use
before()hooks to preprocess source data before conversion - Use
after()hooks to postprocess target data after conversion - Hooks can be async - just use
async (data) => { ... }and they'll be awaited - Multiple hooks execute in order - add as many as you need for complex workflows
- Error handling provides helpful suggestions - when field mapping fails, you'll get suggestions for similar field names
- Errors include rich context - see exactly where and why conversions failed with detailed error information
- Use error categories to filter and handle different types of errors (validation, mapping, parsing, etc.)
- Create CLIs for converters - use
CLIGenerator.forConverter(converter)to generate command-line interfaces - Use CLI profiles - save converter configurations as profiles for reuse:
cli profile save my-converter - CLI supports batch processing - convert multiple files at once with pattern matching and parallel processing
- Use built-in plugins - leverage ValidationPlugin, AuditPlugin, and BackupPlugin for enhanced functionality
- Plugin hooks execute in order - plugins can intercept and modify data at different conversion stages
- Error hooks provide recovery - plugins can handle errors gracefully and provide fallback behavior
- Use batch processing for multiple files -
convertBatch()processes multiple files efficiently with parallel support - Enable parallel processing - set
parallel: trueand configureworkersfor faster batch processing - Use incremental processing -
convertIncremental()only processes files that have changed since last run - Monitor performance - use
PerformanceMonitorto track conversion speed, memory usage, and throughput - Benchmark different approaches - use
PerformanceBenchmarkto compare sequential vs parallel processing - Configure error strategies - use
errorStrategy: 'continue'to process all files even if some fail, or'fail-fast'to stop on first error - Use
forEach()with{ output: 'object' }to transform arrays into keyed objects - perfect for converting indexed arrays to UUID-keyed objects forEach()with object output merges returned objects - return{ [key]: value }to create dynamic keys from your data- Filter with
forEach()- returnnullfrom the mapping function to exclude items from the result - Use
result.getSummary()- get detailed statistics about conversion success, file counts, errors, and duration - Use
result.toFiles()- convert any result to FileObject arrays for consistent multi-file handling - Enhanced
print()methods - result printing now includes emojis and better formatting for easier debugging - Multi-file results provide file-specific statistics - track success/failure per file in batch operations
- Split results include generated file lists -
getSummary().generatedFilesshows all files created from split operations - Pipeline integration works seamlessly with multi-file processing - all pipeline steps (parse, map, transform, validate, hooks) work correctly across multiple files
- Multi-file context is available in all pipeline steps - access filename, fileIndex, and other multi-file metadata in transforms and hooks
- Error collection spans multiple files - validation errors and processing errors are collected across all files with proper context
- Choose forEach output type - use
{ output: 'array' }(default) to maintain structure,{ output: 'object' }to create keyed objects - Track progress - provide
progressCallbackto monitor batch processing progress in real-time - Cache management - use
IncrementalProcessor.getCacheStats()andcleanupCache()to manage incremental processing cache - Save batch results - use
BatchProcessor.saveBatch()to save all conversion results to an output directory - Use forge() with options - pass
ForgeOptionstoforge({ strict: true, parallel: true })to configure converter behavior globally - Use convertString() for raw content - when you have YAML or JSON as a string, use
convertString()instead ofconvert()⭐ NEW! - Use convert() with isRawContent option - alternatively, use
convert(stringContent, { isRawContent: true })for string input ⭐ NEW! - Format auto-detection works with strings - ConfigForge automatically detects YAML vs JSON format in string content ⭐ NEW!
- String input handles parsing errors gracefully - malformed YAML/JSON strings will produce helpful error messages ⭐ NEW!
- Use
{ flatten: true }to remove wrapper objects - combine with{ output: 'object' }to promote nested objects to root level ⭐ NEW! - Flatten only works with object output -
{ flatten: true }requires{ output: 'object' }to prevent data loss ⭐ NEW! - Flatten works with both arrays and objects - remove wrappers from any source structure type ⭐ NEW!
- Combine flatten with filtering - return
nullin forEach to exclude items, flatten still works correctly ⭐ NEW! - forEach field tracking is automatic - when you access fields inside forEach callbacks, they're automatically marked as mapped in statistics ⭐ NEW!
- forEach with target renaming - use
forEach('source', 'target', mapFn)to rename arrays/objects in output:forEach('npc', 'npcs', mapFn)⭐ NEW! - Array indices are always numbers - in forEach for arrays, the index parameter is guaranteed to be a number, so
index + 1works correctly ⭐ NEW! - Object keys in forEach - for objects, forEach passes the key as the second parameter for backward compatibility ⭐ NEW!
- Access object keys in context - when processing objects with forEach, the key is also available in
context.metadata.objectKey⭐ NEW! - Use convertMany() for many-to-one processing - merge multiple input files into a single output with
convertMany(inputPaths, options)⭐ NEW! - Use convertSplit() for one-to-many processing - split a single input file into multiple outputs with
convertSplit(inputPath, splitFn)⭐ NEW! - Choose merge strategies wisely - use
'merge'for object merging,'array'for simple arrays, or'custom'for complex logic ⭐ NEW! - Custom key extractors for file naming - use
keyExtractor: (filePath) => customKeyto control how filenames become object keys ⭐ NEW! - Multi-file processing is fully tested and stable - both
convertMany()andconvertSplit()have comprehensive test coverage and error handling ⭐ ENHANCED! - Handle file errors gracefully - set
continueOnError: trueto process all files even if some fail, orfalseto stop on first error ⭐ NEW! - Perfect for plugin conversions - multi-file processing is ideal for converting between plugin formats like DecentHolograms ↔ CMI ⭐ NEW!
- Use rootKey for wrapping - set
rootKey: 'holograms'to wrap merged data in a parent object ⭐ NEW! - Check fileResults for debugging -
result.fileResultsshows success/failure status for each processed file ⭐ NEW! - Monitor failed files -
result.failedFilescontains paths of files that couldn't be processed ⭐ NEW! - Split functions control output structure - return
{ key1: data1, key2: data2 }from split functions to create multiple output files ⭐ NEW! - Handle field conflicts in multi-file processing - use
onKeyConflict()to configure how to handle conflicting field names across files ⭐ NEW! - Choose conflict resolution strategy - use
'error'to fail on conflicts,'overwrite'for last-wins,'merge'to combine into arrays, or'suffix'to add unique suffixes ⭐ NEW! - Merge strategy combines conflicting fields - when using
onKeyConflict('merge'), fields with the same name across files are combined into arrays ⭐ NEW! - Custom conflict suffixes - use
onKeyConflict('suffix', '_backup')to customize the suffix added to conflicting field names ⭐ NEW! - Force same key for conflict testing - use
useKey('fixedKey')to force all files to use the same key, creating conflicts for testing resolution strategies ⭐ NEW! - Use split() for one-to-many processing - break single input files into multiple outputs with
split(data => ({ file1: data.part1, file2: data.part2 }))⭐ NEW! - Split functions control output structure - return an object where keys become filenames and values become file content ⭐ NEW!
- Split works with all mappings and transforms - apply field mappings, transformations, defaults, and hooks to each split output ⭐ NEW!
- Split handles errors gracefully - if split function fails or individual conversions error, detailed error information is provided ⭐ NEW!
- Combine split with filename mapping - use
mapFilename()to include source filename information in each split output ⭐ NEW! - Many-to-one processing creates single merged file - when using
.to('filename.yml')with multi-file input,toFiles()creates one merged file with that name ⭐ NEW! - Legacy multi-file behavior preserved - when no target schema is specified,
toFiles()creates separate files for each input (backward compatibility) ⭐ NEW! - One-to-one conversion without key strategy returns direct data - single file input without
.useFilenameAsKey()or.useKey()returns converted data directly, not wrapped with filename key ⭐ NEW!
That's it! ConfigForge makes config conversion simple and straightforward. No complex setup, no direct class manipulation - just define your mappings and convert.
License
This package is licensed under the PolyForm Noncommercial License 1.0.0.
See the LICENSE file for full terms.
