/* * 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 assert from 'node:assert' import * as os from 'node:os' import { Logger } from '@xpack/logger' // ---------------------------------------------------------------------------- import { LiquidSubstitutionsVariables, liquidSubstitutionsVariablesBase, } from '../data/substitutions-variables.js' import { isJsonObject } from '../functions/is-something.js' import { Actions } from './actions.js' import { BuildConfigurations } from './build-configurations.js' import { LiquidEngine } from './liquid-engine.js' import { JsonXpmPackage } from '../types/json.js' // ============================================================================ /** * The property name used for the build folder relative path. */ export const buildFolderRelativePathPropertyName = 'buildFolderRelativePath' // ============================================================================ /** * Configuration parameters for constructing a data model instance. * * @remarks * This interface defines the required configuration for creating an * instance of {@link DataModel}. Both properties are mandatory. * * The parameters provide the parsed package.json content * containing package metadata and xpm-specific configuration, along * with the logger for diagnostic output during data model initialization * and template processing. */ export interface DataModelConstructorParameters { /** * The JSON package definition. */ jsonPackage: JsonXpmPackage /** * The logger instance for output and diagnostics. */ log: Logger } /** * Represents a lazy-loading data model for an xpm package. * * @remarks * This class prepares substitution variables, creates the Liquid * engine, and exposes actions and build configurations defined in the package. * * The package processor serves as the top-level coordinator for all * Liquid-based template processing in an xpm package. It establishes the * foundation for variable substitution throughout the package hierarchy: * *
    *
  1. Initializes base substitution variables (platform detection, system * information, etc.).
  2. *
  3. Adds package-specific variables from package.json * metadata.
  4. *
  5. Merges user-defined properties from xpack.properties.
  6. *
  7. Creates package-level actions accessible across all contexts.
  8. *
  9. Creates build configurations, each inheriting the base substitution * context and adding configuration-specific variables.
  10. *
* * This hierarchical structure ensures that templates at any level have * access to appropriate variables while maintaining clear scoping rules. * Package-level actions are available globally, while configuration-level * actions are scoped to their respective configurations. */ export class DataModel { // -------------------------------------------------------------------------- // Public Members. /** * The variables available for Liquid substitutions. * * @remarks * This sealed object provides the base substitution context inherited by * all actions and build configurations within the package. * * Variable hierarchy: * *
    *
  1. Base variables (xpmLiquidSubstitutionsVariablesBase): * *
  2. *
  3. Package metadata: * *
  4. *
  5. User-defined properties: * *
  6. *
* * The object is sealed after initialization to prevent accidental * modification. Child components (actions and configurations) extend this * context with their own scoped variables (configuration, matrix) without * modifying the original sealed object. */ readonly substitutionsVariables: LiquidSubstitutionsVariables /** * The actions collection for this package. * * @remarks * This collection manages package-level actions defined in * `xpack.actions`, which are globally accessible and not tied to specific * build configurations. * * Package-level actions characteristics: * *
    *
  1. Created during construction but initially unpopulated.
  2. *
  3. Populated during the collection's own initialisation when * Actions.initialise() is called.
  4. *
  5. Have access to package-level substitution variables but not * configuration-specific variables.
  6. *
  7. Suitable for package-wide tasks like testing, documentation * generation, or global cleanup.
  8. *
  9. Can be used alongside configuration-specific actions, which inherit * from package-level actions.
  10. *
*/ readonly actions: Actions /** * The build configurations collection for this package. * * @remarks * This collection manages all build configurations defined in * `xpack.buildConfigurations`, supporting inheritance, template expansion, * and configuration-specific properties and dependencies. * * Build configurations characteristics: * *
    *
  1. Created during construction but initially unpopulated.
  2. *
  3. Populated during the collection's own initialisation when * BuildConfigurations.initialise() is called.
  4. *
  5. Each configuration inherits the package-level substitution variables * and extends them with configuration-specific context.
  6. *
  7. Support complex inheritance chains where configurations can inherit * properties, dependencies, and actions from other configurations.
  8. *
  9. Can be generated from templates with matrix expansion for * multi-platform or multi-variant builds.
  10. *
  11. Each configuration maintains its own actions collection, inheriting * package-level actions and adding configuration-specific ones.
  12. *
*/ readonly buildConfigurations: BuildConfigurations /** * The logger instance for output and diagnostics. * * @remarks * This logger provides trace-level diagnostics for the entire package * processing hierarchy, including Liquid engine creation, variable * initialization, action collection setup, and build configuration * preparation. It's passed down to child components (actions and build * configurations) to maintain consistent logging throughout the package * lifecycle. */ protected readonly _log: Logger // -------------------------------------------------------------------------- // Private Members. /** * The Liquid engine used for substitutions. * * @remarks * This LiquidEngine instance is configured with strict mode and custom * filters for xpm-specific operations. It's shared across all actions and * build configurations within the package, ensuring consistent template * processing behavior. * * Engine characteristics: * *
    *
  1. Strict mode enabled to catch undefined variable references.
  2. *
  3. Custom filters for platform detection (isPlatform, * isArch).
  4. *
  5. Custom filters for path sanitization (filterPath, * filterPosixPath, * filterWin32Path).
  6. *
  7. Shared instance reduces memory overhead and ensures consistent * template evaluation across all package components.
  8. *
*/ protected readonly _engine: LiquidEngine /** * The JSON package definition. * * @remarks * This object contains the complete `package.json` content, including both * standard npm fields and xpm-specific extensions in the `xpack` section. * * Required structure: * *
    *
  1. Standard npm fields: name, version, dependencies, devDependencies.
  2. *
  3. Required xpack section containing xpm-specific * configuration.
  4. *
  5. Optional xpack.properties for user-defined * substitution variables.
  6. *
  7. Optional xpack.actions for package-level executable * actions.
  8. *
  9. Optional xpack.buildConfigurations for build configuration * definitions.
  10. *
* * The package definition is validated during construction, requiring the * `xpack` section to be present and be a valid JSON object. */ protected readonly _jsonPackage: JsonXpmPackage // -------------------------------------------------------------------------- // Constructor. /** * Constructs a Liquid package processor. * * @remarks * The constructor initializes the Liquid engine and prepares the * substitution variables context that will be inherited by all actions * and build configurations. * * Initialization sequence: * *
    *
  1. Create LiquidEngine with custom filters and strict * configuration.
  2. *
  3. Validate xpack section exists in * package.json.
  4. *
  5. Initialize base substitution variables (os, platform, arch, etc.).
  6. *
  7. Add package metadata to substitution context.
  8. *
  9. Merge xpack.properties if defined, allowing user-defined * variables.
  10. *
  11. Seal substitution variables to prevent accidental modification.
  12. *
  13. Create package-level actions collection (initially empty, populated * during initialisation).
  14. *
  15. Create build configurations collection (initially empty, populated * during initialisation).
  16. *
* * The substitution variables object is sealed to ensure immutability of * the base context. Individual actions and configurations will extend this * context with their own scoped variables without modifying the original. * * @param jsonPackage - The JSON package definition. * @param log - The logger instance for output and diagnostics. */ constructor({ jsonPackage, log }: DataModelConstructorParameters) { log.trace(`${DataModel.name}()`) this._log = log this._engine = new LiquidEngine() assert( isJsonObject(jsonPackage.xpack), 'xpack section missing in package.json' ) this._jsonPackage = jsonPackage // os.version() available since 12.x assert( typeof os.version === 'function', 'Mandatory os.version available only since 12.x' ) this.substitutionsVariables = { ...liquidSubstitutionsVariablesBase, package: jsonPackage, } if (isJsonObject(jsonPackage.xpack.properties)) { this.substitutionsVariables.properties = { ...jsonPackage.xpack.properties, } } // Prevent adding/removing properties. Object.seal(this.substitutionsVariables) // Empty actions. this.actions = new Actions({ log: this._log, engine: this._engine, substitutionsVariables: this.substitutionsVariables, jsonActions: this._jsonPackage.xpack.actions, }) // Empty build configurations. this.buildConfigurations = new BuildConfigurations({ log: this._log, engine: this._engine, substitutionsVariables: this.substitutionsVariables, jsonBuildConfigurations: this._jsonPackage.xpack.buildConfigurations, }) } } // ----------------------------------------------------------------------------