UNPKG

xpm

Version:

The xPack project manager command line tool

596 lines (507 loc) 17 kB
/* * This file is part of the xPack project (http://xpack.github.io). * Copyright (c) 2017-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. */ 'use strict' // ---------------------------------------------------------------------------- /** * The `xpm run command [-- <args>]` command implementation. */ // ---------------------------------------------------------------------------- // https://nodejs.org/docs/latest/api/ import assert from 'assert' import fs from 'fs/promises' // import os from 'os' import path from 'path' // import util from 'util' // ---------------------------------------------------------------------------- // ES6: `import { CliCommand, CliExitCodes, CliError } from 'cli-start-options' // import { CliCommand, CliExitCodes, CliHelp, // CliError, CliErrorInput, CliOptions } from '@ilg/cli-start-options' import cliStartOptionsCsj from '@ilg/cli-start-options' // https://www.npmjs.com/package/@xpack/xpm-lib import * as xpmLib from '@xpack/xpm-lib' // ---------------------------------------------------------------------------- import { GlobalConfig } from '../classes/global-config.js' import { Spawn } from '../functions/spawn.js' import { convertXpmError } from '../functions/convert-xpm-errors.js' // ---------------------------------------------------------------------------- const { CliCommand, CliExitCodes, CliHelp, CliError, CliErrorInput, CliOptions, } = cliStartOptionsCsj /// ============================================================================ export class RunAction extends CliCommand { // -------------------------------------------------------------------------- /** * @summary Constructor, to set help definitions. * * @param {Object} context Reference to a context. */ constructor(context) { super(context) // Title displayed with the help message. this.title = 'xPack project manager - ' + 'run package/configuration specific action' this.optionGroups = [ { title: 'Run options', postOptions: '[<action>] [-- <action_args>]', // Extra arguments. optionDefs: [ { options: ['-c', '--config'], init: ({ config }) => { config.configurationName = undefined }, action: ({ config }, val) => { config.configurationName = val.trim() }, msg: 'Run the configuration specific action', param: 'config_name', isOptional: true, }, { options: ['-n', '--dry-run'], init: ({ config }) => { config.isDryRun = false }, action: ({ config }) => { config.isDryRun = true }, msg: 'Pretend to run the action', isOptional: true, }, { options: ['-a', '--all-configs'], init: ({ config }) => { config.isAllConfigs = false }, action: ({ config }) => { config.isAllConfigs = true }, msg: 'Run the action for all configurations', isOptional: true, }, { options: ['--ignore-errors'], init: ({ config }) => { config.isIgnoreErrors = false }, action: ({ config }) => { config.isIgnoreErrors = true }, msg: 'Ignore script errors', isOptional: true, }, ], }, ] } doOutputHelpArgsDetails(more) { const log = this.context.log if (!more.isFirstPass) { log.always('where:') log.always( `${CliHelp.padRight(' <action>', more.width)} ` + 'The name of the action/script (optional)' ) log.always( `${CliHelp.padRight(' <action_args>...', more.width)} ` + 'Extra arguments for the action (optional, multiple)' ) } } /** * @summary Execute the `install` command. * * @param {string[]} args Command line arguments. * @returns {number} Return code. * * @override */ async doRun(args) { const log = this.log const context = this.context const config = context.config log.trace(`${this.constructor.name}.doRun()`) log.verbose(this.title) context.globalConfig = new GlobalConfig() await context.globalConfig.checkDeprecatedFolders(log) const xpmPackage = new xpmLib.Package({ log, packageFolderPath: config.cwd, }) this.xpmPackage = xpmPackage try { // Read `package.json`; throw if not valid. this.jsonPackage = await xpmPackage.readPackageDotJson({ withThrow: true, }) } catch (error) { throw new CliErrorInput(error.message) } if (!xpmPackage.isNpmPackage()) { throw new CliErrorInput( 'current folder is not a valid package, check for package.json' ) } if (!xpmPackage.isXpmPackage()) { throw new CliErrorInput( 'current folder is not an xpm package, ' + 'check for the "xpack" property in package.json' ) } try { await xpmPackage.checkMinimumXpmRequired({ xpmRootFolderPath: context.rootPath, }) } catch (error) { throw convertXpmError(error) } const xpmDataModel = new xpmLib.DataModel({ log, jsonPackage: this.jsonPackage, }) this.xpmDataModel = xpmDataModel let exitCode = CliExitCodes.SUCCESS try { if (args.length === 0) { // Show the existing scripts await this.showScripts() } else { if (config.isAllConfigs) { const buildConfigurations = xpmDataModel.buildConfigurations await buildConfigurations.initialise() for (const buildConfigurationName of buildConfigurations.names) { const buildConfiguration = buildConfigurations.get( buildConfigurationName ) await buildConfiguration.initialise() if (buildConfiguration.isHidden) { // Ignore hidden configurations. continue } const actions = buildConfiguration.actions await actions.initialise() if (actions.isEmpty) { // Ignore configurations without actions. continue } if (!actions.has(args[0])) { // Ignore actions not defined by this configuration. continue } exitCode = await this.executeAction({ name: args[0], args: args.slice(1), buildConfigurationName, }) if (exitCode !== CliExitCodes.SUCCESS && !config.isIgnoreErrors) { break } } } else { // Run the args[0] script; pass the other args. exitCode = await this.executeAction({ name: args[0], args: args.slice(1), buildConfigurationName: config.configurationName, }) } } } catch (error) { throw convertXpmError(error) } if (log.isVerbose) { this.outputDoneDuration() } return exitCode } async showScripts() { const log = this.log log.trace(`${this.constructor.name}.showScripts()`) assert(this.jsonPackage, 'missing mandatory package.json') const jsonPackage = this.jsonPackage let hasActions = false log.verbose() log.verbose(`Scripts included in xpm package '${jsonPackage.name}':`) if (jsonPackage.scripts) { for (const [key, value] of Object.entries(jsonPackage.scripts)) { log.output(`- ${key}`) this.showCommands(value) hasActions = true } } const xpmDataModel = this.xpmDataModel const actions = xpmDataModel.actions await actions.initialise() if (!actions.isEmpty) { for (const actionName of actions.names) { const action = actions.get(actionName) await action.initialise() log.output(`- ${actionName}`) this.showCommands(action.commands) hasActions = true } } const buildConfigurations = xpmDataModel.buildConfigurations await buildConfigurations.initialise() if (!buildConfigurations.isEmpty) { for (const buildConfigurationName of buildConfigurations.names) { const buildConfiguration = buildConfigurations.get( buildConfigurationName ) if (buildConfiguration.isHidden) { // Ignore hidden configurations. continue } await buildConfiguration.initialise() const actions = buildConfiguration.actions await actions.initialise() if (!actions.isEmpty) { for (const actionName of actions.names) { const action = actions.get(actionName) await action.initialise() log.output(`- ${buildConfigurationName}/${actionName}`) this.showCommands(action.commands) hasActions = true } } } } if (!hasActions) { log.warn('no "xpack.actions" or "scripts" defined in package.json') } } showCommands(value) { const log = this.log if (xpmLib.isString(value)) { const trimmed = value.trim() if (trimmed.length > 0) { log.output(` ${trimmed}`) } } else if (Array.isArray(value)) { for (const command of value) { const trimmed = command.trim() if (trimmed.length > 0) { log.output(` ${trimmed}`) } } } } async executeAction({ name, args, buildConfigurationName }) { const log = this.log const context = this.context const config = context.config const jsonPackage = this.jsonPackage const xpmDataModel = this.xpmDataModel log.trace(`${this.constructor.name}.executeAction('${name}')`) const ownArguments = CliOptions.filterOwnArguments(args) ownArguments.forEach((opt) => { log.warn(`'${opt}' ignored`) }) let buildConfiguration let actions if (buildConfigurationName) { // --config if (config.isAllConfigs) { if (log.isVerbose) { // When invoked with --all-configs, this method is called // multiple times, so inform the user which configuration // is being processed. log.verbose() log.verbose( `Running action ${name} for package ${jsonPackage.name}, ` + `configuration ${buildConfigurationName}...` ) } else { log.info() log.info(`${jsonPackage.name} --config ${buildConfigurationName}...`) } } const buildConfigurations = xpmDataModel.buildConfigurations await buildConfigurations.initialise() buildConfiguration = buildConfigurations.get(buildConfigurationName) await buildConfiguration.initialise() actions = buildConfiguration.actions await actions.initialise() if (!actions.has(name)) { throw new CliErrorInput( `action "${name}" not found, check the ` + `"xpack.buildConfigurations.${buildConfigurationName}.actions"` + ' property in package.json' ) } } else { actions = xpmDataModel.actions await actions.initialise() if (!actions.has(name)) { throw new CliErrorInput( `action "${name}" not found, check the ` + '"xpack.actions" property in package.json' ) } } const action = actions.get(name) await action.initialise() const commandsArray = action.commands log.trace(`action command(s): '${commandsArray}'`) const otherArguments = CliOptions.filterOtherArguments(args) if (commandsArray.length > 1 && otherArguments.length > 0) { throw new CliError( 'optional arguments not supported for array of commands' ) } const pack = context.package log.verbose() if (buildConfigurationName) { log.debug( `${pack.name}@${pack.version} ${buildConfigurationName}/${name}` ) } else { log.debug(`${pack.name}@${pack.version} ${name}`) } log.trace(`rootPath: '${context.rootPath}'`) log.verbose(`CWD=${config.cwd}`) const opts = {} opts.log = log opts.cwd = config.cwd // Create a copy of the environment. const env = Object.assign({}, process.env) let pathsArray = [] if (process.env.PATH) { process.env.PATH.split(path.delimiter).forEach((path_) => { pathsArray.push(path_) }) } // Prefer xpacks over node_modules ("So the last shall be first"...) pathsArray = await this.prependPathIfPresent( config.cwd, 'node_modules', pathsArray ) pathsArray = await this.prependPathIfPresent( config.cwd, 'xpacks', pathsArray ) // Prefer configuration dependencies over global ones. if (buildConfigurationName) { // The configuration was set before. assert(buildConfiguration) const buildFolderRelativePath = buildConfiguration.buildFolderRelativePath const buildPath = path.join(config.cwd, buildFolderRelativePath) pathsArray = await this.prependPathIfPresent( buildPath, 'node_modules', pathsArray ) pathsArray = await this.prependPathIfPresent( buildPath, 'xpacks', pathsArray ) } env.PATH = pathsArray.join(path.delimiter) opts.env = env log.verbose(`PATH=${env.PATH}`) log.verbose() const spawn = new Spawn() let exitCode = CliExitCodes.SUCCESS if (otherArguments.length > 0) { const commandWithArguments = [commandsArray[0].trim()] .concat(otherArguments) .join(' ') if (log.isVerbose) { log.verbose(`Invoking '${commandWithArguments}'...`) } else { log.info(`> ${commandWithArguments}`) } if (!config.isDryRun) { // code = await spawn.executeShellPromise(commandWithArguments, opts) let result try { result = await spawn.spawnShellPromise(commandWithArguments, opts) exitCode = result.code } catch (error) { log.verbose(error) if (config.isIgnoreErrors) { log.warn(`running '${commandWithArguments}' failed`) exitCode = CliExitCodes.SUCCESS } else { throw new CliError(`running '${commandWithArguments}' failed`) } } } } else { for (const command of commandsArray) { const trimmedCommand = command.trim() if (trimmedCommand) { if (log.isVerbose) { log.verbose(`Invoking '${trimmedCommand}'...`) } else { log.info(`> ${trimmedCommand}`) } if (!config.isDryRun) { // code = await spawn.executeShellPromise(trimmedCommand, opts) let result try { result = await spawn.spawnShellPromise(trimmedCommand, opts) exitCode = result.code } catch (error) { log.verbose(error) if (config.isIgnoreErrors) { log.warn(`running '${trimmedCommand}' failed`) exitCode = CliExitCodes.SUCCESS } else { throw new CliError(`running '${trimmedCommand}' failed`) } } if (!config.isIgnoreErrors) { // Break on first error. if (exitCode !== CliExitCodes.SUCCESS) { // Break on first error. break } } } } } } if (exitCode !== CliExitCodes.SUCCESS) { if (log.isVerbose) { log.verbose(`Returned error code '${exitCode}'... Not good...`) } else { log.info(`> exit(${exitCode})`) } } return exitCode } // -------------------------------------------------------------------------- async prependPathIfPresent(basePath, folderName, pathArray) { assert(basePath) assert(folderName) assert(pathArray) const binPath = path.join(basePath, folderName, '.bin') try { await fs.stat(binPath) // If the folder exists, return a new array with the binPath prepended. return [binPath, ...pathArray] } catch { // If the folder does not exist, return the same array. return pathArray } } // -------------------------------------------------------------------------- } // ----------------------------------------------------------------------------