UNPKG

xpm

Version:

The xPack project manager command line tool

331 lines (303 loc) 11.6 kB
/* * This file is part of the xPack project (http://xpack.github.io). * Copyright (c) 2021-2026 Liviu Ionescu. All rights reserved. * * Permission to use, copy, modify, and/or distribute this software * for any purpose is hereby granted, under the terms of the MIT license. * * If a copy of the license was not distributed with this file, it can * be obtained from https://opensource.org/license/mit. */ // ---------------------------------------------------------------------------- import { Logger } from '@xpack/logger' // ---------------------------------------------------------------------------- import { LiquidSubstitutionsVariables } from '../data/substitutions-variables.js' import { processMatrixForExpansion } from '../functions/matrix-expander.js' import { performSubstitutions } from '../functions/perform-substitutions.js' import { getErrorMessage } from '../functions/utils.js' import { CombinationsGenerator } from './combinations-generator.js' import { ConfigurationError } from './errors.js' import { LiquidEngine } from './liquid-engine.js' import { JsonTemplateMatrix } from '../types/json.js' // ============================================================================ /** * Configuration parameters for constructing a template expander instance. * * @remarks * This interface defines the required configuration for creating an * instance of {@link TemplateExpander}. All properties are mandatory. * * The parameters provide the Liquid templating engine, substitution * variables hierarchy, and logger for diagnostic output during template * expansion operations. */ export interface TemplateExpanderConstructorParameters { /** * The Liquid templating engine for variable substitution. */ engine: LiquidEngine /** * The variables available for substitution in templates. */ substitutionsVariables: LiquidSubstitutionsVariables /** * The logger instance for output and diagnostics. */ log: Logger } /** * Callback function type for creating instances from template expansions. * * @remarks * This function is invoked for each combination generated during template * expansion. It receives the expanded name, matrix combination parameters, * and the template content, and must return a new instance of type * <code>TInstance</code>. * * The callback is responsible for: * * <ol> * <li>Creating the appropriate instance type (e.g., <code>Action</code> or * <code>BuildConfiguration</code>).</li> * <li>Passing the expanded name to the instance constructor.</li> * <li>Storing the matrix parameters for later template evaluation.</li> * <li>Associating the template content with the instance.</li> * </ol> * * @typeParam TTemplate - The type of the template content (e.g., * <code>JsonActionContent</code> or * <code>JsonBuildConfigurationContent</code>). * @typeParam TInstance - The type of instance to create (e.g., * <code>Action</code> or <code>BuildConfiguration</code>). * * @param expandedName - The name after Liquid substitution with matrix * parameters (e.g., <code>test-x64</code> from * <code>test-\{\{ matrix.arch \}\}</code>). * @param combination - The matrix parameter values for this combination * (e.g., <code>\{ arch: 'x64', platform: 'linux' \}</code>). * @param templateContent - The template content to associate with the * instance. * @param originalTemplateName - The original template name before expansion * (e.g., <code>test-\{\{ matrix.arch \}\}</code>). * @returns The newly created instance. */ export type InstanceFactoryCallback<TTemplate, TInstance> = ( expandedName: string, combination: Record<string, string>, templateContent: TTemplate, originalTemplateName: string ) => TInstance /** * A generic template expansion engine for matrix-based template processing. * * @remarks * This class provides shared functionality for expanding template names * and creating multiple instances from a single template definition with * matrix parameters. It eliminates code duplication between * {@link Actions} and {@link BuildConfigurations} classes by extracting * the common template expansion logic. * * Template expansion process: * * <ol> * <li><b>Matrix processing:</b> Validates matrix structure and performs * Liquid substitutions on matrix values.</li> * <li><b>Combination generation:</b> Computes the Cartesian product of all * matrix parameter values.</li> * <li><b>Name expansion:</b> For each combination, substitutes matrix values * into the template name.</li> * <li><b>Instance creation:</b> Invokes the factory callback to create * instances with expanded names and matrix parameters.</li> * </ol> * * The class is generic to support different template and instance types * whilst maintaining type safety throughout the expansion process. * * @typeParam TTemplate - The type of template content (e.g., * <code>JsonActionContent</code> or * <code>JsonBuildConfigurationContent</code>). * @typeParam TInstance - The type of instance to create (e.g., * <code>Action</code> or <code>BuildConfiguration</code>). */ export class TemplateExpander<TTemplate, TInstance> { // -------------------------------------------------------------------------- // Public Members. /** * The Liquid templating engine for variable substitution. */ readonly engine: LiquidEngine /** * The variables available for substitution in templates. */ readonly substitutionsVariables: LiquidSubstitutionsVariables /** * The logger instance for output and diagnostics. */ readonly log: Logger // -------------------------------------------------------------------------- // Constructor. /** * Constructs a template expander instance. * * @param engine - The Liquid templating engine for variable substitution. * @param substitutionsVariables - The variables available for substitution * in templates. * @param log - The logger instance for output and diagnostics. */ constructor({ engine, substitutionsVariables, log, }: TemplateExpanderConstructorParameters) { this.engine = engine this.substitutionsVariables = substitutionsVariables this.log = log } // -------------------------------------------------------------------------- // Public Methods. /** * Expands a template into multiple instances based on matrix parameters. * * @remarks * This method orchestrates the template expansion process by validating * the matrix, generating all parameter combinations, expanding the * template name for each combination, and creating instances via the * factory callback. * * Processing steps: * * <ol> * <li>Processes the matrix to extract and validate parameter keys and * values.</li> * <li>Generates all combinations using the Cartesian product.</li> * <li>For each combination: * <ol> * <li>Performs Liquid substitution on the template name with matrix * parameters.</li> * <li>Invokes the factory callback to create an instance.</li> * <li>Stores the instance in the result map.</li> * </ol> * </li> * </ol> * * The factory callback is responsible for creating the appropriate * instance type and associating it with the expanded name and matrix * parameters. * * @param templateName - The template name containing Liquid variables * (e.g., <code>test-\{\{ matrix.arch \}\}</code>). * @param matrix - The matrix object containing parameter definitions. * @param templateContent - The template content to pass to the factory. * @param templateType - A descriptive name for the template type (e.g., * "action" or "buildConfiguration"), used in error messages. * @param instanceFactory - A callback function that creates instances from * expanded names and matrix combinations. * @returns A promise that resolves to a map of expanded names to their * corresponding instances. * * @throws {@link ConfigurationError} * If the matrix structure is invalid, template name substitution fails, * or the factory callback throws an error. */ async expandTemplate({ templateName, matrix, templateContent, templateType, instanceFactory, }: { templateName: string matrix: JsonTemplateMatrix templateContent: TTemplate templateType: string instanceFactory: InstanceFactoryCallback<TTemplate, TInstance> }): Promise<Map<string, TInstance>> { const log = this.log log.trace(`${TemplateExpander.name}.expandTemplate(${templateName})`) const instances = new Map<string, TInstance>() // Process matrix for expansion const { matrixKeys, matrixValues } = await processMatrixForExpansion({ matrix, templateName, templateType: templateType as 'action' | 'buildConfiguration', engine: this.engine, substitutionsVariables: this.substitutionsVariables, log: this.log, }) // Generate all combinations (Cartesian product) const combinationsGenerator = new CombinationsGenerator({ matrixKeys, matrixValues, log: this.log, }) // Use generator pattern for memory efficiency // Expand template for each combination for (const combination of combinationsGenerator.generate()) { const expandedName = await this._expandName({ templateName, combination, templateType, }) const instance = instanceFactory( expandedName, combination, templateContent, templateName ) instances.set(expandedName, instance) } return instances } // -------------------------------------------------------------------------- // Protected Methods. /** * Expands a template name with matrix parameter substitutions. * * @remarks * This helper method performs Liquid template substitution on the template * name using the specific matrix combination values. It wraps the * substitution operation with error handling to provide context about * which template failed. * * The method adds the matrix parameters to the substitution variables * hierarchy under the <code>matrix</code> namespace, making them * accessible in Liquid templates via syntax like * <code>\{\{ matrix.arch \}\}</code>. * * @param templateName - The template name containing Liquid variables. * @param combination - The matrix parameter values for this combination. * @param templateType - A descriptive name for the template type, used in * error messages. * @returns A promise that resolves to the expanded name after substitution. * * @throws {@link ConfigurationError} * If Liquid substitution fails, with context about the template name. */ protected async _expandName({ templateName, combination, templateType, }: { templateName: string combination: Record<string, string> templateType: string }): Promise<string> { try { return await performSubstitutions({ input: templateName, engine: this.engine, substitutionsVariables: { ...this.substitutionsVariables, matrix: combination, }, log: this.log, }) } catch (error) { const message = getErrorMessage(error) + ` in ${templateType} "${templateName}" name substitution` throw new ConfigurationError(message) } } } // ----------------------------------------------------------------------------