/* * 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 * TInstance. * * The callback is responsible for: * *
    *
  1. Creating the appropriate instance type (e.g., Action or * BuildConfiguration).
  2. *
  3. Passing the expanded name to the instance constructor.
  4. *
  5. Storing the matrix parameters for later template evaluation.
  6. *
  7. Associating the template content with the instance.
  8. *
* * @typeParam TTemplate - The type of the template content (e.g., * JsonActionContent or * JsonBuildConfigurationContent). * @typeParam TInstance - The type of instance to create (e.g., * Action or BuildConfiguration). * * @param expandedName - The name after Liquid substitution with matrix * parameters (e.g., test-x64 from * test-\{\{ matrix.arch \}\}). * @param combination - The matrix parameter values for this combination * (e.g., \{ arch: 'x64', platform: 'linux' \}). * @param templateContent - The template content to associate with the * instance. * @param originalTemplateName - The original template name before expansion * (e.g., test-\{\{ matrix.arch \}\}). * @returns The newly created instance. */ export type InstanceFactoryCallback = ( expandedName: string, combination: Record, 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: * *
    *
  1. Matrix processing: Validates matrix structure and performs * Liquid substitutions on matrix values.
  2. *
  3. Combination generation: Computes the Cartesian product of all * matrix parameter values.
  4. *
  5. Name expansion: For each combination, substitutes matrix values * into the template name.
  6. *
  7. Instance creation: Invokes the factory callback to create * instances with expanded names and matrix parameters.
  8. *
* * 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., * JsonActionContent or * JsonBuildConfigurationContent). * @typeParam TInstance - The type of instance to create (e.g., * Action or BuildConfiguration). */ export class TemplateExpander { // -------------------------------------------------------------------------- // 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: * *
    *
  1. Processes the matrix to extract and validate parameter keys and * values.
  2. *
  3. Generates all combinations using the Cartesian product.
  4. *
  5. For each combination: *
      *
    1. Performs Liquid substitution on the template name with matrix * parameters.
    2. *
    3. Invokes the factory callback to create an instance.
    4. *
    5. Stores the instance in the result map.
    6. *
    *
  6. *
* * 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., test-\{\{ matrix.arch \}\}). * @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 }): Promise> { const log = this.log log.trace(`${TemplateExpander.name}.expandTemplate(${templateName})`) const instances = new Map() // 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 matrix namespace, making them * accessible in Liquid templates via syntax like * \{\{ matrix.arch \}\}. * * @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 templateType: string }): Promise { 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) } } } // ----------------------------------------------------------------------------