UNPKG

xpm

Version:

The xPack project manager command line tool

489 lines 22.4 kB
import assert from 'node:assert'; import * as path from 'node:path'; import * as os from 'node:os'; import { filterPath } from '../functions/filter-paths.js'; import { isJsonObject, isString } from '../functions/is-something.js'; import { getErrorMessage, hasLiquidSyntax } from '../functions/utils.js'; import { performSubstitutions } from '../functions/perform-substitutions.js'; import { Actions } from './actions.js'; import { buildFolderRelativePathPropertyName } from './data-model.js'; import { ConfigurationError } from './errors.js'; import { TemplateExpander } from './template-expander.js'; export class BuildConfigurations { log; engine; substitutionsVariables; jsonBuildConfigurations; _buildConfigurationsMap = new Map(); _jsonBuildConfigurationsNamesMap = new Map(); _namesSet = new Set(); _isInitialised = false; _names = []; constructor({ engine, substitutionsVariables, jsonBuildConfigurations, log, }) { assert(log, 'log is required'); assert(engine, 'engine is required'); assert(substitutionsVariables, 'substitutionsVariables is required'); log.trace(`${BuildConfigurations.name}()`); this.log = log; this.engine = engine; this.substitutionsVariables = substitutionsVariables; this.jsonBuildConfigurations = jsonBuildConfigurations ?? {}; } async initialise() { const log = this.log; if (this._isInitialised) { log.trace(`${BuildConfigurations.name}.initialise() again`); return false; } log.trace(`${BuildConfigurations.name}.initialise()`); for (const [buildConfigurationName, jsonBuildConfiguration,] of Object.entries(this.jsonBuildConfigurations)) { if (hasLiquidSyntax(buildConfigurationName)) { await this._processTemplate({ buildConfigurationName, jsonBuildConfigurationTemplate: jsonBuildConfiguration, }); } else { if (this._namesSet.has(buildConfigurationName)) { throw new ConfigurationError(`build configuration name ` + `"${buildConfigurationName}" already defined`); } else { this._buildConfigurationsMap.set(buildConfigurationName, undefined); this._jsonBuildConfigurationsNamesMap.set(buildConfigurationName, buildConfigurationName); this._namesSet.add(buildConfigurationName); } } } const names = Array.from(this._buildConfigurationsMap.keys()); this._names = names; log.trace(`${BuildConfigurations.name}.initialise() =>`, names); this._isInitialised = true; return true; } get size() { assert(this._isInitialised, 'BuildConfigurations collection must be initialised before ' + 'accessing size'); return this._buildConfigurationsMap.size; } get isEmpty() { assert(this._isInitialised, 'BuildConfigurations collection must be initialised before ' + 'accessing isEmpty'); return this._buildConfigurationsMap.size === 0; } get names() { assert(this._isInitialised, 'BuildConfigurations collection must be initialised before ' + 'accessing names'); return this._names; } getJsonName(buildConfigurationName) { assert(this._isInitialised, 'BuildConfigurations collection must be initialised before ' + 'accessing getJsonName()'); const name = this._jsonBuildConfigurationsNamesMap.get(buildConfigurationName); if (name === undefined) { throw new ConfigurationError(`build configuration "${buildConfigurationName}" does not exist`); } return name; } hasJson(buildConfigurationName) { assert(this._isInitialised, 'BuildConfigurations collection must be initialised before ' + 'accessing hasJson()'); return this._jsonBuildConfigurationsNamesMap.has(buildConfigurationName); } getJson(buildConfigurationName) { assert(this._isInitialised, 'BuildConfigurations collection must be initialised before ' + 'accessing getJson()'); return this.jsonBuildConfigurations[this.getJsonName(buildConfigurationName)]; } isHidden(buildConfigurationName) { assert(this._isInitialised, 'BuildConfigurations collection must be initialised before ' + 'accessing isHidden()'); const jsonBuildConfigurationName = this.getJsonName(buildConfigurationName); if (jsonBuildConfigurationName.includes('{{')) { const jsonBuildConfigurationTemplate = this.jsonBuildConfigurations[jsonBuildConfigurationName]; return jsonBuildConfigurationTemplate.template.hidden ?? false; } const jsonBuildConfigurationContent = this .jsonBuildConfigurations[jsonBuildConfigurationName]; return jsonBuildConfigurationContent.hidden ?? false; } has(buildConfigurationName) { assert(this._isInitialised, 'BuildConfigurations collection must be initialised before ' + 'accessing has()'); return this._buildConfigurationsMap.has(buildConfigurationName); } get(buildConfigurationName) { assert(this._isInitialised, 'BuildConfigurations collection must be initialised before ' + 'accessing get()'); const log = this.log; log.trace(`${BuildConfigurations.name}.get(${buildConfigurationName})`); let buildConfiguration = this._buildConfigurationsMap.get(buildConfigurationName); if (buildConfiguration === undefined) { const jsonBuildConfigurationName = this.getJsonName(buildConfigurationName); const jsonBuildConfiguration = (this .jsonBuildConfigurations[jsonBuildConfigurationName] ?? {}); buildConfiguration = new BuildConfiguration({ buildConfigurationName, jsonBuildConfiguration, parentBuildConfigurations: this, }); this._buildConfigurationsMap.set(buildConfigurationName, buildConfiguration); } return buildConfiguration; } async _processTemplate({ buildConfigurationName, jsonBuildConfigurationTemplate, }) { try { const expandedBuildConfigurationsMap = await this._expandTemplateBuildConfigurations({ buildConfigurationName, jsonBuildConfigurationTemplate, }); for (const [expandedBuildConfigurationName, expandedBuildConfiguration,] of expandedBuildConfigurationsMap) { if (this._namesSet.has(expandedBuildConfigurationName)) { throw new ConfigurationError(`duplicate build configuration name ` + `"${expandedBuildConfigurationName}" ` + `could not be generated from template.`); } else { this._buildConfigurationsMap.set(expandedBuildConfigurationName, expandedBuildConfiguration); this._jsonBuildConfigurationsNamesMap.set(expandedBuildConfigurationName, buildConfigurationName); this._namesSet.add(expandedBuildConfigurationName); } } } catch (error) { const message = getErrorMessage(error) + ` in buildConfiguration "${buildConfigurationName}"`; throw new ConfigurationError(message); } } async _expandTemplateBuildConfigurations({ buildConfigurationName, jsonBuildConfigurationTemplate, }) { const log = this.log; log.trace(`${BuildConfigurations.name}.` + `#expandTemplateBuildConfigurations(${buildConfigurationName})`); if (!isJsonObject(jsonBuildConfigurationTemplate.matrix)) { throw new ConfigurationError(`buildConfiguration "${buildConfigurationName}" ` + `matrix is not an object`); } if (!isJsonObject(jsonBuildConfigurationTemplate.template)) { throw new ConfigurationError(`buildConfiguration "${buildConfigurationName}" ` + `template is not a JSON object`); } const expander = new TemplateExpander({ engine: this.engine, substitutionsVariables: this.substitutionsVariables, log: this.log, }); return await expander.expandTemplate({ templateName: buildConfigurationName, matrix: jsonBuildConfigurationTemplate.matrix, templateContent: jsonBuildConfigurationTemplate.template, templateType: 'buildConfiguration', instanceFactory: (expandedName, combination, templateContent, originalTemplateName) => new BuildConfiguration({ buildConfigurationName: expandedName, templateBuildConfigurationName: originalTemplateName, jsonBuildConfiguration: templateContent, parentBuildConfigurations: this, matrixParameters: { ...combination }, }), }); } } export class BuildConfiguration { name; templateName; parentBuildConfigurations; inheritsNames = []; isHidden; properties = {}; dependencies = {}; devDependencies = {}; jsonBuildConfiguration; isTemplate; _log; _substitutionsVariables; matrixParameters; _actions; _buildFolderRelativePath; _inheritedNamesSet = new Set(); _isInitialised = false; constructor({ buildConfigurationName, templateBuildConfigurationName, jsonBuildConfiguration, parentBuildConfigurations, matrixParameters, }) { assert(buildConfigurationName, 'buildConfigurationName is required'); assert(jsonBuildConfiguration, 'jsonBuildConfiguration is required'); assert(parentBuildConfigurations, 'parentBuildConfigurations is required'); const log = parentBuildConfigurations.log; this._log = log; log.trace(`${BuildConfiguration.name}(${buildConfigurationName})`); this.name = buildConfigurationName; this.jsonBuildConfiguration = jsonBuildConfiguration; this.parentBuildConfigurations = parentBuildConfigurations; if (matrixParameters !== undefined) { this.matrixParameters = matrixParameters; } if (templateBuildConfigurationName !== undefined) { this.templateName = templateBuildConfigurationName; } this._substitutionsVariables = { ...this.parentBuildConfigurations.substitutionsVariables, }; this.isHidden = this.jsonBuildConfiguration.hidden ?? false; this.isTemplate = this.templateName !== undefined; } async initialise() { const log = this._log; log.trace(`${BuildConfiguration.name}.initialise()` + ` @${this.name}`); if (this._isInitialised) { log.trace(`${BuildConfiguration.name}.initialise()` + ` @${this.name} again`); return false; } log.trace(`${BuildConfiguration.name}.initialise()` + ` @${this.name}`); let localJsonBuildConfiguration; if (this.isTemplate) { localJsonBuildConfiguration = await this._substituteTemplate(); } else { localJsonBuildConfiguration = await this._substituteInherits(); } const inheritedActionsMap = await this._processInherits(localJsonBuildConfiguration); this.properties = { ...this.properties, ...localJsonBuildConfiguration.properties, }; assert(this.name, 'buildConfigurationName missing'); this._substitutionsVariables = { ...this.parentBuildConfigurations.substitutionsVariables, properties: { ...this._substitutionsVariables.properties, ...this.properties, }, matrix: this.matrixParameters ?? {}, configuration: { ...localJsonBuildConfiguration, name: this.name, }, }; if (!this.isHidden) { this._buildFolderRelativePath = await this._getBuildFolderRelativePath(); const properties = this._substitutionsVariables.properties; properties.buildFolderRelativePath = this._buildFolderRelativePath; } this.dependencies = { ...this.dependencies, ...localJsonBuildConfiguration.dependencies, }; this.devDependencies = { ...this.devDependencies, ...localJsonBuildConfiguration.devDependencies, }; const unsubstitutedDependencies = { dependencies: this.dependencies, devDependencies: this.devDependencies, }; const stringifiedDependencies = JSON.stringify(unsubstitutedDependencies); if (hasLiquidSyntax(stringifiedDependencies)) { let substitutedDependencies; try { substitutedDependencies = await performSubstitutions({ log, engine: this.parentBuildConfigurations.engine, input: stringifiedDependencies, substitutionsVariables: this._substitutionsVariables, }); } catch (error) { const message = getErrorMessage(error) + ` in buildConfiguration "${this.name}" dependencies`; throw new ConfigurationError(message); } const parsedDependencies = JSON.parse(substitutedDependencies); this.dependencies = parsedDependencies.dependencies ?? {}; this.devDependencies = parsedDependencies.devDependencies ?? {}; } this._actions = new Actions({ log: this._log, engine: this.parentBuildConfigurations.engine, substitutionsVariables: this._substitutionsVariables, inheritedActionsMap, jsonActions: localJsonBuildConfiguration.actions, buildConfiguration: this, }); log.trace(`${BuildConfiguration.name}.initialise() `, `@{this.buildConfigurationName}`); if (!this.isHidden) { log.trace(this.name, 'buildFolderRelativePath =>', this._buildFolderRelativePath); } log.trace(this.name, 'properties => ', this.properties); log.trace(this.name, 'dependencies => ', this.dependencies); log.trace(this.name, 'devDependencies => ', this.devDependencies); this._isInitialised = true; return true; } get actions() { assert(this._isInitialised, 'BuildConfiguration must be initialised before ' + 'accessing actions'); assert(this._actions !== undefined, 'Actions not initialised'); return this._actions; } get buildFolderRelativePath() { assert(this._isInitialised, 'BuildConfiguration must be initialised before ' + 'accessing buildFolderRelativePath'); assert(this._buildFolderRelativePath !== undefined, 'BuildConfiguration _buildFolderRelativePath not initialised'); return this._buildFolderRelativePath; } async _substituteTemplate() { const log = this._log; let localJsonBuildConfiguration; const stringifiedJsonBuildConfiguration = JSON.stringify(this.jsonBuildConfiguration); if (hasLiquidSyntax(stringifiedJsonBuildConfiguration)) { let substitutedJsonBuildConfiguration; try { substitutedJsonBuildConfiguration = await performSubstitutions({ log, engine: this.parentBuildConfigurations.engine, input: stringifiedJsonBuildConfiguration, substitutionsVariables: { ...this._substitutionsVariables, matrix: this.matrixParameters ?? {}, configuration: { ...this.jsonBuildConfiguration, name: this.name, }, }, }); } catch (error) { const message = getErrorMessage(error) + ` in buildConfiguration "${this.name}"`; throw new ConfigurationError(message); } localJsonBuildConfiguration = JSON.parse(substitutedJsonBuildConfiguration); } else { localJsonBuildConfiguration = this.jsonBuildConfiguration; } return localJsonBuildConfiguration; } async _substituteInherits() { const log = this._log; let localJsonBuildConfiguration; const stringifiedJsonInherits = JSON.stringify(this.jsonBuildConfiguration.inherits ?? {}); if (hasLiquidSyntax(stringifiedJsonInherits)) { let substitutedJsonInherits; try { substitutedJsonInherits = await performSubstitutions({ log, engine: this.parentBuildConfigurations.engine, input: stringifiedJsonInherits, substitutionsVariables: { ...this._substitutionsVariables, configuration: { ...this.jsonBuildConfiguration, name: this.name, }, }, }); } catch (error) { const message = getErrorMessage(error) + ` in buildConfiguration "${this.name}" inherits`; throw new ConfigurationError(message); } localJsonBuildConfiguration = { ...this.jsonBuildConfiguration, inherits: JSON.parse(substitutedJsonInherits), }; } else { localJsonBuildConfiguration = this.jsonBuildConfiguration; } return localJsonBuildConfiguration; } _parseInheritsField(localJsonBuildConfiguration) { let jsonInherits = []; if (isString(localJsonBuildConfiguration.inherits)) { jsonInherits = [localJsonBuildConfiguration.inherits]; } else if (Array.isArray(localJsonBuildConfiguration.inherits)) { jsonInherits = localJsonBuildConfiguration.inherits; } else if (isString(localJsonBuildConfiguration.inherit)) { jsonInherits = [localJsonBuildConfiguration.inherit]; } else if (Array.isArray(localJsonBuildConfiguration.inherit)) { jsonInherits = localJsonBuildConfiguration.inherit; } if (jsonInherits.length > 0) { const joinedInherits = jsonInherits.join(os.EOL); return joinedInherits.split(os.EOL); } return jsonInherits; } async _processInheritedConfiguration(inheritedBuildConfigurationName, inheritedActionsMap) { if (inheritedBuildConfigurationName.trim() === '') { return; } if (!this.parentBuildConfigurations.hasJson(inheritedBuildConfigurationName)) { throw new ConfigurationError(`buildConfiguration "${this.name}" ` + `inherits from missing "${inheritedBuildConfigurationName}"`); } if (this._inheritedNamesSet.has(inheritedBuildConfigurationName)) { throw new ConfigurationError(`buildConfiguration "${this.name}" ` + `inherits from circular reference ` + `"${inheritedBuildConfigurationName}"`); } this._inheritedNamesSet.add(inheritedBuildConfigurationName); const inheritedBuildConfiguration = this.parentBuildConfigurations.get(inheritedBuildConfigurationName); await inheritedBuildConfiguration.initialise(); this.properties = { ...this.properties, ...inheritedBuildConfiguration.properties, }; this.dependencies = { ...this.dependencies, ...inheritedBuildConfiguration.dependencies, }; this.devDependencies = { ...this.devDependencies, ...inheritedBuildConfiguration.devDependencies, }; await inheritedBuildConfiguration.actions.initialise(); for (const actionName of inheritedBuildConfiguration.actions.names) { const action = inheritedBuildConfiguration.actions.get(actionName); inheritedActionsMap.set(actionName, action); } } async _processInherits(localJsonBuildConfiguration) { const log = this._log; const inheritsNames = this._parseInheritsField(localJsonBuildConfiguration); this.inheritsNames = inheritsNames; log.trace(this.name, 'inherits from', this.inheritsNames); const inheritedActionsMap = new Map(); for (const inheritedBuildConfigurationName of inheritsNames) { await this._processInheritedConfiguration(inheritedBuildConfigurationName, inheritedActionsMap); } return inheritedActionsMap; } async _getBuildFolderRelativePath() { const log = this._log; let folderPath; if (buildFolderRelativePathPropertyName in this._substitutionsVariables.properties) { folderPath = this._substitutionsVariables.properties[buildFolderRelativePathPropertyName]; if (folderPath !== '') { try { const substitutedFolderPath = await performSubstitutions({ log, engine: this.parentBuildConfigurations.engine, input: folderPath, substitutionsVariables: this._substitutionsVariables, }); return substitutedFolderPath; } catch (error) { const message = getErrorMessage(error) + ` in buildConfiguration "${this.name}"`; throw new ConfigurationError(message); } } } const defaultFolderPath = path.join('build', filterPath(this.name)); return defaultFolderPath; } } //# sourceMappingURL=build-configurations.js.map