Source: hubspot/validate.js

/** @module hubspot/marketplaceValidate */
/// <reference path="../types/types.js" />
import { requestValidation, getValidationStatus, getValidationResults } from '@hubspot/local-dev-lib/api/marketplaceValidation'
import chalk from 'chalk'
import ora from 'ora'
import * as ui from '../utils/ui.js'
import { throwErrorIfMissingScope } from './auth/scopes.js'
import { childTheme } from './theme.js'

let failsLength = 0

/**
 * #### request validation
 * @async
 * @private
 * @param {number} portalId - hubspot account id
 * @param {string} themeName - theme name
 * @param {any} spinner - ora spinner
 * @returns {Promise<any>} portal name|names
 */
async function requestHubspotValidation (portalId, themeName, spinner) {
  try {
    const assetType = 'THEME'
    const requestGroup = 'EXTERNAL_DEVELOPER'
    const requestResult = await requestValidation(portalId, {
      path: themeName,
      assetType,
      requestGroup
    })
    return requestResult
  } catch (error) {
    if (error.statusCode === 409) {
      spinner.fail()
      console.log(error.error.message)
      process.exit(1)
    }
    spinner.fail()
    console.error(error)
    process.exit(1)
  }
}

/**
 * #### check validation status and wait for validation to complete
 * @async
 * @private
 * @param {number} accountId - hubspot account id
 * @param {string} validationId - hubspot validation id
 * @param {any} spinner - ora spinner
 * @returns undefined
 */
async function checkHubspotValidationStatus (accountId, validationId, spinner) {
  try {
    const checkValidationStatus = async () => {
      const validationStatus = await getValidationStatus(accountId, {
        validationId
      })

      if (validationStatus === 'REQUESTED') {
        await new Promise(resolve => setTimeout(resolve, 2000))
        await checkValidationStatus()
      }
    }
    await checkValidationStatus()
  } catch (error) {
    spinner.fail()
    console.error(error)
    process.exit(1)
  }
}

/**
 * #### Fetch validation results
 * @async
 * @private
 * @param {number} accountId - hubspot account id
 * @param {string} validationId - hubspot validation id
 * @returns {Promise<any>} validation results
 */
async function fetchHubspotValidationResults (accountId, validationId) {
  try {
    const validationResults = await getValidationResults(accountId, {
      validationId
    })
    return validationResults
  } catch (error) {
    console.error(error)
    process.exit(1)
  }
}

/**
 * #### Display validation errors
 * @private
 * @param {any} validationResults - validation results
 * @returns undefined
 * @throws {Error} - process.exit(1) if validation errors
 */
function showValidationErrors (validationResults) {
  if (validationResults.errors.length) {
    const { errors } = validationResults
    errors.forEach((/** @type {any} */ error) => {
      console.error(`${chalk.red('[Error]')} ${error.failureReasonType}`)
      console.error(error)
    })
    process.exit(1)
  }
}

/**
 * #### Display validation results
 * @private
 * @param {any} validationResults - validation results
 * @returns undefined
 */
function showValidationResults (validationResults) {
  const showFileInfo = (/** @type {String} */ file, /** @type {String} */ line) => {
    const filePrefix = line ? ` ${chalk.dim('├')}` : ` ${chalk.dim('└')}`
    const fileTemplate = `${filePrefix}${chalk.dim('──')} File: ${chalk.green(file)}`
    const lineTemplate = ` ${chalk.dim('└──')} Line: ${chalk.yellow(line)}`
    if (line) {
      console.log(`${fileTemplate}\n${lineTemplate}`)
    } else if (file) {
      console.log(`${fileTemplate}`)
    }
  }

  if (validationResults) {
    const { status, results } = validationResults
    if (status === 'FAIL') {
      let failedValidations = results.filter((/** @type {{status: string}} */ test) => test.status === 'FAIL')
      let warningValidations = results.filter((/** @type {{status: string}} */ test) => test.status === 'WARN')

      if (childTheme) {
        failedValidations = failedValidations.filter((/** @type {any} */ item) =>
          item.check !== 'theme_has_internal_dependency' &&
          item.check !== 'inherits_brand_color' &&
          item.check !== 'separate_post_and_listing')
        warningValidations = warningValidations.filter((/** @type {any} */ item) =>
          item.check !== 'none_template_type')
      }
      failedValidations.forEach((/** @type {any} */ val) => {
        console.error(`\n${chalk.red.bold('[ERROR]')} ${val.description}`)
        console.warn(`${chalk.dim('doc-link:')} ${val.documentationLink}`)
        showFileInfo(val.file, val.line)
      })
      warningValidations.forEach((/** @type {any} */ val) => {
        console.warn(`\n${chalk.yellow.bold('[WARN]')} ${val.description}`)
        console.warn(`${chalk.dim('doc-link:')} ${val.documentationLink}`)
        showFileInfo(val.file, val.line)
      })
      failsLength = failedValidations.length
    }
    if (status === 'PASS') {
      results.forEach((/** @type {any} */ test) => {
        if (test.status === 'WARN') {
          console.warn(`\n${chalk.yellow.bold('[WARN]')} ${test.description}`)
          console.warn(`${chalk.dim('doc-link:')} ${test.documentationLink}`)
          showFileInfo(test.file, test.line)
        }
      })
    }
  }
}

/**
 * #### Theme marketplace validation
 * @async
 * @param {HUBSPOT_AUTH_CONFIG} config - hubspot authentication config
 * @param {string} themeName - theme name
 * @returns {Promise<Object>} validation results
 */
async function marketplaceValidate (config, themeName) {
  try {
    const timeStart = ui.startTask('marketplaceValidate')
    const portalId = config.portals[0].portalId

    throwErrorIfMissingScope(config, 'design_manager')
    const spinner = ora('Request Marketplace validation').start()
    const validationId = await requestHubspotValidation(portalId, themeName, spinner)
    await checkHubspotValidationStatus(portalId, validationId, spinner)
    spinner.succeed()
    console.log(`Validation ID: ${validationId}`)
    const validationResults = await fetchHubspotValidationResults(portalId, validationId)
    showValidationErrors(validationResults)
    if ('RECOMMENDED' in validationResults.results) {
      showValidationResults(validationResults.results.RECOMMENDED)
    }
    if ('REQUIRED' in validationResults.results) {
      showValidationResults(validationResults.results.REQUIRED)
    }

    const { status } = validationResults.results.REQUIRED
    if (status === 'PASS') {
      console.log(`\n${chalk.green.bold('[PASS]')} no errors found`)
    } else if (status === 'FAIL' && failsLength > 0) {
      console.error(`\n${chalk.red.bold('[FAIL]')} Marketplace Validation failed`)
      process.exitCode = 1
    } else if (status === 'FAIL' && failsLength === 0) {
      console.log(`\n${chalk.green.bold('[PASS]')} no errors found`)
    }

    ui.endTask({ taskName: 'marketplaceValidate', timeStart })
    return validationResults
  } catch (error) {
    console.error(error)
    process.exit(1)
  }
}

export { marketplaceValidate }

Table of contents