prices-as-code
Version:
Prices as Code (PaC) - Define your product pricing schemas with type-safe definitions
265 lines (263 loc) • 10.8 kB
JavaScript
import fs from 'fs';
import path from 'path';
import yaml from 'js-yaml';
import { ConfigSchema } from './types.js';
import { pathToFileURL } from 'url';
import * as esbuild from 'esbuild';
/**
* Reads configuration from a file (TypeScript or YAML)
*/
export async function readConfigFromFile(configPath) {
try {
const extension = path.extname(configPath).toLowerCase();
if (extension === '.ts' || extension === '.js' || extension === '.mjs') {
// Load from TypeScript/JavaScript module
console.log(`📦 Loading configuration from TypeScript/JavaScript module: ${configPath}`);
return await loadTsConfig(configPath);
}
else if (extension === '.yml' || extension === '.yaml') {
// Load from YAML file
console.log(`📦 Loading configuration from YAML file: ${configPath}`);
return loadYamlConfig(configPath);
}
else if (extension === '.json') {
// Load from JSON file
console.log(`📦 Loading configuration from JSON file: ${configPath}`);
return loadJsonConfig(configPath);
}
else {
throw new Error(`Unsupported file format: ${extension}. Please use .ts, .js, .mjs, .json, .yml, or .yaml`);
}
}
catch (error) {
// Improve error messages for common issues
if (error instanceof Error) {
if (error.message.includes('Cannot find module') && error.message.includes(configPath)) {
throw new Error(`Configuration file not found or inaccessible: ${configPath}`);
}
if (error.message.includes('Unexpected token') || error.message.includes('Invalid YAML')) {
throw new Error(`Invalid syntax in configuration file: ${configPath}. Please check your file format.`);
}
// Add a hint for schema validation errors
if (error.message.includes('validation failed')) {
console.error(`❌ Error loading configuration from ${configPath}:`, error);
throw error;
}
}
// Log the original error and rethrow
console.error(`❌ Error loading configuration from ${configPath}:`, error);
throw error;
}
}
/**
* Loads a configuration from a TypeScript/JavaScript module
*/
async function loadTsConfig(configPath) {
try {
// Check if we're trying to load a TypeScript file directly
if (configPath.endsWith('.ts')) {
try {
// Use esbuild to transpile the TypeScript file to JavaScript
console.log(`🔄 Transpiling TypeScript file: ${configPath}`);
// Get a temporary file name for the transpiled JavaScript
const tempJsPath = `${configPath}.temp.js`;
// Read the TypeScript file
const content = fs.readFileSync(configPath, 'utf8');
// Transpile the TypeScript to JavaScript using esbuild
const result = await esbuild.transform(content, {
loader: 'ts',
target: 'es2020',
format: 'esm'
});
// Write the transpiled JavaScript to a temporary file
fs.writeFileSync(tempJsPath, result.code, 'utf8');
try {
// Try to import the transpiled JavaScript
const moduleUrl = pathToFileURL(path.resolve(tempJsPath)).href;
// Dynamically import the transpiled module
const module = await import(moduleUrl);
// Get the default export or named export 'config'
const config = module.default || module.config;
if (!config) {
throw new Error(`TypeScript module does not export a default configuration or 'config' export`);
}
// Clean up the temporary file
fs.unlinkSync(tempJsPath);
// Validate with Zod schema
return ConfigSchema.parse(config);
}
catch (error) {
// Clean up the temporary file if it exists
if (fs.existsSync(tempJsPath)) {
fs.unlinkSync(tempJsPath);
}
const importError = error;
console.error(`❌ Error importing transpiled TypeScript module: ${importError}`);
throw new Error(`Failed to load TypeScript configuration: ${importError.message}`);
}
}
catch (error) {
console.error(`❌ Error processing TypeScript file ${configPath}:`, error);
throw error;
}
}
// For JavaScript files, use the regular ES module import approach
// Convert path to URL for ES modules
const moduleUrl = pathToFileURL(path.resolve(configPath)).href;
// Dynamically import the module
const module = await import(moduleUrl);
// Get the default export or named export 'config'
const config = module.default || module.config;
if (!config) {
throw new Error(`Module does not export a default configuration or 'config' export`);
}
// Validate with Zod schema
return ConfigSchema.parse(config);
}
catch (error) {
console.error(`❌ Error loading TypeScript/JavaScript config from ${configPath}:`, error);
throw error;
}
}
/**
* Loads a configuration from a JSON file
*/
export function loadJsonConfig(configPath) {
try {
const fileContents = fs.readFileSync(configPath, 'utf8');
const config = JSON.parse(fileContents);
// Ensure products and prices arrays exist
if (!config.products)
config.products = [];
if (!config.prices)
config.prices = [];
// Validate with Zod schema
return ConfigSchema.parse(config);
}
catch (error) {
if (error instanceof SyntaxError) {
console.error(`❌ Error parsing JSON in ${configPath}:`, error.message);
throw new Error(`Invalid JSON in configuration file: ${configPath}. ${error.message}`);
}
console.error(`❌ Error loading JSON config from ${configPath}:`, error);
throw error;
}
}
/**
* Loads a configuration from a YAML file
*/
export function loadYamlConfig(configPath) {
try {
const fileContents = fs.readFileSync(configPath, 'utf8');
const config = yaml.load(fileContents);
// Ensure products and prices arrays exist
if (!config.products)
config.products = [];
if (!config.prices)
config.prices = [];
// Validate with Zod schema
return ConfigSchema.parse(config);
}
catch (error) {
// For validation errors, format them nicely before rethrowing
if (error &&
typeof error === 'object' &&
'name' in error &&
error.name === 'ZodError' &&
'issues' in error) {
const zodError = error;
const issues = zodError.issues || [];
if (issues.length > 0) {
// Check for common patterns in validation errors
if (issues.some(issue => issue.code === 'invalid_union_discriminator' &&
typeof issue.message === 'string' &&
issue.message.includes('Expected \'stripe\''))) {
const friendlyError = new Error(`Configuration validation failed: Your products and prices are missing the 'provider' field. ` +
`Each product and price must have a 'provider' field set to 'stripe'. ` +
`Please update your configuration file to include this field.`);
throw friendlyError;
}
}
}
// For other errors, log and rethrow
console.error(`❌ Error loading YAML config from ${configPath}:`, error);
throw error;
}
}
/**
* Writes configuration to a file (TypeScript or YAML)
*/
export async function writeConfigToFile(configPath, config) {
try {
const extension = path.extname(configPath).toLowerCase();
if (extension === '.ts' || extension === '.js' || extension === '.mjs') {
// Write to TypeScript/JavaScript file
await writeTsConfig(configPath, config);
}
else if (extension === '.yml' || extension === '.yaml') {
// Write to YAML file
saveYamlConfig(configPath, config);
}
else if (extension === '.json') {
// Write to JSON file
saveJsonConfig(configPath, config);
}
else {
throw new Error(`Unsupported file format for writing: ${extension}. Please use .ts, .js, .json, .yml, or .yaml`);
}
console.log(`✅ Configuration saved to ${configPath}`);
}
catch (error) {
console.error(`❌ Error saving config to ${configPath}:`, error);
throw error;
}
}
/**
* Writes configuration to a TypeScript or JavaScript file
*/
async function writeTsConfig(configPath, config) {
try {
// Determine if we're writing to a TypeScript or JavaScript file
const isTypeScript = configPath.toLowerCase().endsWith('.ts');
// Create a nicely formatted representation
const content = `/**
* This file is auto-generated by prices-as-code.
* Manual changes may be overwritten.
*/
${isTypeScript ? 'import { Config } from \'prices-as-code\';\n\n' : ''}
export const config${isTypeScript ? ': Config' : ''} = ${JSON.stringify(config, null, 2)};
export default config;
`;
fs.writeFileSync(configPath, content, 'utf8');
}
catch (error) {
console.error(`❌ Error saving ${configPath.toLowerCase().endsWith('.ts') ? 'TypeScript' : 'JavaScript'} config to ${configPath}:`, error);
throw error;
}
}
/**
* Writes configuration to a YAML file
*/
export function saveYamlConfig(configPath, config) {
try {
const yamlContent = yaml.dump(config, { noRefs: true });
fs.writeFileSync(configPath, yamlContent, 'utf8');
}
catch (error) {
console.error(`❌ Error saving YAML config to ${configPath}:`, error);
throw error;
}
}
/**
* Writes configuration to a JSON file
*/
export function saveJsonConfig(configPath, config) {
try {
const jsonContent = JSON.stringify(config, null, 2);
fs.writeFileSync(configPath, jsonContent, 'utf8');
}
catch (error) {
console.error(`❌ Error saving JSON config to ${configPath}:`, error);
throw error;
}
}