/*
* 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:
*
*
* - Initializes base substitution variables (platform detection, system
* information, etc.).
* - Adds package-specific variables from
package.json
* metadata.
* - Merges user-defined properties from
xpack.properties.
* - Creates package-level actions accessible across all contexts.
* - Creates build configurations, each inheriting the base substitution
* context and adding configuration-specific variables.
*
*
* 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:
*
*
* - Base variables (xpmLiquidSubstitutionsVariablesBase):
*
* env: Environment variables from process.env
* os: Platform detection (platform, arch, endianness,
* version)
* path: Path utilities (sep, delimiter, cwd)
*
*
* - Package metadata:
*
* package: Complete package.json content
* (name, version,
* dependencies, etc.)
*
*
* - User-defined properties:
*
* properties: Merged from
* xpack.properties if present
*
*
*
*
* 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:
*
*
* - Created during construction but initially unpopulated.
* - Populated during the collection's own initialisation when
*
Actions.initialise() is called.
* - Have access to package-level substitution variables but not
* configuration-specific variables.
* - Suitable for package-wide tasks like testing, documentation
* generation, or global cleanup.
* - Can be used alongside configuration-specific actions, which inherit
* from package-level actions.
*
*/
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:
*
*
* - Created during construction but initially unpopulated.
* - Populated during the collection's own initialisation when
*
BuildConfigurations.initialise() is called.
* - Each configuration inherits the package-level substitution variables
* and extends them with configuration-specific context.
* - Support complex inheritance chains where configurations can inherit
* properties, dependencies, and actions from other configurations.
* - Can be generated from templates with matrix expansion for
* multi-platform or multi-variant builds.
* - Each configuration maintains its own actions collection, inheriting
* package-level actions and adding configuration-specific ones.
*
*/
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:
*
*
* - Strict mode enabled to catch undefined variable references.
* - Custom filters for platform detection (
isPlatform,
* isArch).
* - Custom filters for path sanitization (
filterPath,
* filterPosixPath,
* filterWin32Path).
* - Shared instance reduces memory overhead and ensures consistent
* template evaluation across all package components.
*
*/
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:
*
*
* - Standard npm fields: name, version, dependencies, devDependencies.
* - Required
xpack section containing xpm-specific
* configuration.
* - Optional
xpack.properties for user-defined
* substitution variables.
* - Optional
xpack.actions for package-level executable
* actions.
* - Optional
xpack.buildConfigurations for build configuration
* definitions.
*
*
* 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:
*
*
* - Create
LiquidEngine with custom filters and strict
* configuration.
* - Validate
xpack section exists in
* package.json.
* - Initialize base substitution variables (os, platform, arch, etc.).
* - Add package metadata to substitution context.
* - Merge
xpack.properties if defined, allowing user-defined
* variables.
* - Seal substitution variables to prevent accidental modification.
* - Create package-level actions collection (initially empty, populated
* during initialisation).
* - Create build configurations collection (initially empty, populated
* during initialisation).
*
*
* 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,
})
}
}
// ----------------------------------------------------------------------------