From 1367aee80368588327e34dcc5e5ef02802c0b98f Mon Sep 17 00:00:00 2001 From: Branden Rodgers Date: Fri, 10 Jan 2025 14:57:28 -0500 Subject: [PATCH 01/11] v7.0.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cbe773169..e2af1bba7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@hubspot/cli", - "version": "7.0.1-beta.0", + "version": "7.0.1", "description": "The official CLI for developing on HubSpot", "license": "Apache-2.0", "repository": "https://github.com/HubSpot/hubspot-cli", From 5b6b57ee85f86469b98ce3f9e526495ae0d7c91b Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Mon, 13 Jan 2025 11:32:46 -0500 Subject: [PATCH 02/11] Chore: convert lib/sandboxes and sandboxSync to TS (#1328) --- commands/sandbox/create.ts | 17 +-- lang/en.lyaml | 2 + lib/buildAccount.ts | 9 +- lib/localDev.ts | 10 +- lib/sandboxSync.ts | 76 +++++----- lib/sandboxes.ts | 276 +++++++++---------------------------- types/Sandboxes.ts | 3 + 7 files changed, 126 insertions(+), 267 deletions(-) create mode 100644 types/Sandboxes.ts diff --git a/commands/sandbox/create.ts b/commands/sandbox/create.ts index b516e307d..33dbb1314 100644 --- a/commands/sandbox/create.ts +++ b/commands/sandbox/create.ts @@ -10,9 +10,9 @@ const { EXIT_CODES } = require('../../lib/enums/exitCodes'); const { getAccountConfig, getEnv } = require('@hubspot/local-dev-lib/config'); const { uiFeatureHighlight, uiBetaTag } = require('../../lib/ui'); const { - sandboxTypeMap, + SANDBOX_TYPE_MAP, getAvailableSyncTypes, - syncTypes, + SYNC_TYPES, validateSandboxUsageLimits, } = require('../../lib/sandboxes'); const { getValidEnv } = require('@hubspot/local-dev-lib/environment'); @@ -65,7 +65,7 @@ exports.handler = async options => { let typePrompt; let namePrompt; - if ((type && !sandboxTypeMap[type.toLowerCase()]) || !type) { + if ((type && !SANDBOX_TYPE_MAP[type.toLowerCase()]) || !type) { if (!force) { typePrompt = await sandboxTypePrompt(); } else { @@ -74,7 +74,7 @@ exports.handler = async options => { } } const sandboxType = type - ? sandboxTypeMap[type.toLowerCase()] + ? SANDBOX_TYPE_MAP[type.toLowerCase()] : typePrompt.type; // Check usage limits and exit if parent portal has no available sandboxes for the selected type @@ -141,12 +141,7 @@ exports.handler = async options => { const sandboxAccountConfig = getAccountConfig(result.sandbox.sandboxHubId); // For v1 sandboxes, keep sync here. Once we migrate to v2, this will be handled by BE automatically const handleSyncSandbox = async syncTasks => { - await syncSandbox({ - accountConfig: sandboxAccountConfig, - parentAccountConfig: accountConfig, - env, - syncTasks, - }); + await syncSandbox(sandboxAccountConfig, accountConfig, env, syncTasks); }; try { let availableSyncTasks = await getAvailableSyncTypes( @@ -156,7 +151,7 @@ exports.handler = async options => { if (!contactRecordsSyncPromptResult) { availableSyncTasks = availableSyncTasks.filter( - t => t.type !== syncTypes.OBJECT_RECORDS + t => t.type !== SYNC_TYPES.OBJECT_RECORDS ); } await handleSyncSandbox(availableSyncTasks); diff --git a/lang/en.lyaml b/lang/en.lyaml index 295c67f75..0b4f5c7e4 100644 --- a/lang/en.lyaml +++ b/lang/en.lyaml @@ -1420,6 +1420,7 @@ en: failure: invalidUser: "Couldn't create {{#bold}}{{ accountName }}{{/bold}} because your account has been removed from {{#bold}}{{ parentAccountName }}{{/bold}} or your permission set doesn't allow you to create the sandbox. To update your permissions, contact a super admin in {{#bold}}{{ parentAccountName }}{{/bold}}." 403Gating: "Couldn't create {{#bold}}{{ accountName }}{{/bold}} because {{#bold}}{{ parentAccountName }}{{/bold}} does not have access to development sandboxes. To opt in to the CRM Development Beta and use development sandboxes, visit https://app.hubspot.com/l/product-updates/in-beta?update=13899236." + usageLimitsFetch: "Unable to fetch sandbox usage limits. Please try again." limit: developer: one: "{{#bold}}{{ accountName }}{{/bold}} reached the limit of {{ limit }} development sandbox. @@ -1479,6 +1480,7 @@ en: syncInProgress: "Couldn't run the sync because there's another sync in progress. Wait for the current sync to finish and then try again. To check the sync status, visit the sync activity log: {{ url }}." notSuperAdmin: "Couldn't run the sync because you are not a super admin in {{ account }}. Ask the account owner for super admin access to the sandbox." objectNotFound: "Couldn't sync the sandbox because {{#bold}}{{ account }}{{/bold}} may have been deleted through the UI. Run {{#bold}}hs sandbox delete{{/bold}} to remove this account from the config. " + syncTypeFetch: "Unable to fetch available sandbox sync types. Please try again." errorHandlers: index: errorOccurred: "Error: {{ error }}" diff --git a/lib/buildAccount.ts b/lib/buildAccount.ts index 874f05a2a..e6e236678 100644 --- a/lib/buildAccount.ts +++ b/lib/buildAccount.ts @@ -27,7 +27,10 @@ const { HUBSPOT_ACCOUNT_TYPES, } = require('@hubspot/local-dev-lib/constants/config'); const { createSandbox } = require('@hubspot/local-dev-lib/api/sandboxHubs'); -const { sandboxApiTypeMap, handleSandboxCreateError } = require('./sandboxes'); +const { + SANDBOX_API_TYPE_MAP, + handleSandboxCreateError, +} = require('./sandboxes'); const { handleDeveloperTestAccountCreateError, } = require('./developerTestAccounts'); @@ -132,7 +135,7 @@ async function buildNewAccount({ let resultAccountId; try { if (isSandbox) { - const sandboxApiType = sandboxApiTypeMap[accountType]; // API expects sandbox type as 1 or 2. + const sandboxApiType = SANDBOX_API_TYPE_MAP[accountType]; // API expects sandbox type as 1 or 2. const { data } = await createSandbox(accountId, name, sandboxApiType); result = { name, ...data }; @@ -159,7 +162,7 @@ async function buildNewAccount({ }); if (isSandbox) { - handleSandboxCreateError({ err, env, accountConfig, name, accountId }); + handleSandboxCreateError(err, env, name, accountId); } if (isDeveloperTestAccount) { handleDeveloperTestAccountCreateError(err, env, accountId, portalLimit); diff --git a/lib/localDev.ts b/lib/localDev.ts index 0f9d7ebd2..20d5b02b9 100644 --- a/lib/localDev.ts +++ b/lib/localDev.ts @@ -223,13 +223,13 @@ const createSandboxForLocalDev = async (accountId, accountConfig, env) => { sandboxAccountConfig ); // For v1 sandboxes, keep sync here. Once we migrate to v2, this will be handled by BE automatically - await syncSandbox({ - accountConfig: sandboxAccountConfig, - parentAccountConfig: accountConfig, + await syncSandbox( + sandboxAccountConfig, + accountConfig, env, syncTasks, - slimInfoMessage: true, - }); + true + ); return targetAccountId; } catch (err) { logError(err); diff --git a/lib/sandboxSync.ts b/lib/sandboxSync.ts index 0b6587d3b..2fc927f5e 100644 --- a/lib/sandboxSync.ts +++ b/lib/sandboxSync.ts @@ -1,50 +1,50 @@ -// @ts-nocheck -const SpinniesManager = require('./ui/SpinniesManager'); -const { getHubSpotWebsiteOrigin } = require('@hubspot/local-dev-lib/urls'); -const { logger } = require('@hubspot/local-dev-lib/logger'); -const { i18n } = require('./lang'); -const { getAvailableSyncTypes } = require('./sandboxes'); -const { initiateSync } = require('@hubspot/local-dev-lib/api/sandboxSync'); -const { - debugError, - logError, - ApiErrorContext, -} = require('./errorHandlers/index'); -const { isSpecifiedError } = require('@hubspot/local-dev-lib/errors/index'); -const { getSandboxTypeAsString } = require('./sandboxes'); -const { getAccountId } = require('@hubspot/local-dev-lib/config'); -const { - getAccountIdentifier, -} = require('@hubspot/local-dev-lib/config/getAccountIdentifier'); -const { +import SpinniesManager from './ui/SpinniesManager'; +import { getHubSpotWebsiteOrigin } from '@hubspot/local-dev-lib/urls'; +import { logger } from '@hubspot/local-dev-lib/logger'; +import { initiateSync } from '@hubspot/local-dev-lib/api/sandboxSync'; +import { isSpecifiedError } from '@hubspot/local-dev-lib/errors/index'; +import { getAccountId } from '@hubspot/local-dev-lib/config'; +import { getAccountIdentifier } from '@hubspot/local-dev-lib/config/getAccountIdentifier'; +import { CLIAccount } from '@hubspot/local-dev-lib/types/Accounts'; +import { Environment } from '@hubspot/local-dev-lib/types/Config'; + +import { i18n } from './lang'; +import { getAvailableSyncTypes } from './sandboxes'; +import { debugError, logError, ApiErrorContext } from './errorHandlers/index'; +import { getSandboxTypeAsString } from './sandboxes'; +import { uiAccountDescription, uiLine, uiLink, uiCommandDisabledBanner, -} = require('./ui'); -const { isDevelopmentSandbox } = require('./accountTypes'); +} from './ui'; +import { isDevelopmentSandbox } from './accountTypes'; +import { SandboxSyncTask } from '../types/Sandboxes'; const i18nKey = 'lib.sandbox.sync'; -/** - * @param {Object} accountConfig - Account config of sandbox portal - * @param {Object} parentAccountConfig - Account config of parent portal - * @param {String} env - Environment (QA/Prod) - * @param {Array} syncTasks - Array of available sync tasks - * @returns - */ -const syncSandbox = async ({ - accountConfig, - parentAccountConfig, - env, - syncTasks, - slimInfoMessage = false, -}) => { +export async function syncSandbox( + accountConfig: CLIAccount, + parentAccountConfig: CLIAccount, + env: Environment, + syncTasks: Array, + slimInfoMessage = false +) { const id = getAccountIdentifier(accountConfig); const accountId = getAccountId(id); const parentId = getAccountIdentifier(parentAccountConfig); const parentAccountId = getAccountId(parentId); const isDevSandbox = isDevelopmentSandbox(accountConfig); + + if (!accountId || !parentAccountId) { + throw new Error( + i18n(`${i18nKey}.failure.invalidUser`, { + accountName: uiAccountDescription(accountId), + parentAccountName: uiAccountDescription(parentAccountId), + }) + ); + } + SpinniesManager.init({ succeedColor: 'white', }); @@ -190,8 +190,4 @@ const syncSandbox = async ({ uiLine(); logger.log(); } -}; - -module.exports = { - syncSandbox, -}; +} diff --git a/lib/sandboxes.ts b/lib/sandboxes.ts index 2f0599870..feb561dd8 100644 --- a/lib/sandboxes.ts +++ b/lib/sandboxes.ts @@ -1,59 +1,55 @@ -// @ts-nocheck -const { i18n } = require('./lang'); -const { logger } = require('@hubspot/local-dev-lib/logger'); -const { - getSandboxUsageLimits, -} = require('@hubspot/local-dev-lib/api/sandboxHubs'); -const { fetchTypes } = require('@hubspot/local-dev-lib/api/sandboxSync'); -const { - getAccountId, - getEnv, - getConfigAccounts, -} = require('@hubspot/local-dev-lib/config'); -const { promptUser } = require('./prompts/promptUtils'); -const { isDevelopmentSandbox } = require('./accountTypes'); -const { getHubSpotWebsiteOrigin } = require('@hubspot/local-dev-lib/urls'); -const { - HUBSPOT_ACCOUNT_TYPES, -} = require('@hubspot/local-dev-lib/constants/config'); -const { - getAccountIdentifier, -} = require('@hubspot/local-dev-lib/config/getAccountIdentifier'); -const { uiAccountDescription } = require('./ui'); -const { +import { logger } from '@hubspot/local-dev-lib/logger'; +import { getSandboxUsageLimits } from '@hubspot/local-dev-lib/api/sandboxHubs'; +import { fetchTypes } from '@hubspot/local-dev-lib/api/sandboxSync'; +import { getAccountId, getConfigAccounts } from '@hubspot/local-dev-lib/config'; +import { getHubSpotWebsiteOrigin } from '@hubspot/local-dev-lib/urls'; +import { HUBSPOT_ACCOUNT_TYPES } from '@hubspot/local-dev-lib/constants/config'; +import { getAccountIdentifier } from '@hubspot/local-dev-lib/config/getAccountIdentifier'; +import { isMissingScopeError, isSpecifiedError, -} = require('@hubspot/local-dev-lib/errors/index'); -const { getValidEnv } = require('@hubspot/local-dev-lib/environment'); -const { logError } = require('./errorHandlers/index'); +} from '@hubspot/local-dev-lib/errors/index'; +import { AccountType, CLIAccount } from '@hubspot/local-dev-lib/types/Accounts'; +import { Environment } from '@hubspot/local-dev-lib/types/Config'; -const syncTypes = { +import { i18n } from './lang'; +import { uiAccountDescription } from './ui'; +import { logError } from './errorHandlers/index'; +import { SandboxSyncTask } from '../types/Sandboxes'; + +const i18nKey = 'lib.sandbox'; + +export const SYNC_TYPES = { OBJECT_RECORDS: 'object-records', -}; +} as const; -const sandboxTypeMap = { +export const SANDBOX_TYPE_MAP = { dev: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX, developer: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX, development: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX, standard: HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX, -}; +} as const; -const sandboxApiTypeMap = { +export const SANDBOX_API_TYPE_MAP = { [HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX]: 1, [HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX]: 2, -}; +} as const; -const getSandboxTypeAsString = accountType => { +export function getSandboxTypeAsString(accountType?: AccountType): string { if (accountType === HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX) { return 'development'; // Only place we're using this specific name } return 'standard'; -}; +} -function getHasSandboxesByType(parentAccountConfig, type) { +function getHasSandboxesByType( + parentAccountConfig: CLIAccount, + type: AccountType +): boolean { const id = getAccountIdentifier(parentAccountConfig); const parentPortalId = getAccountId(id); - const accountsList = getConfigAccounts(); + const accountsList = getConfigAccounts() || []; + for (const portal of accountsList) { if ( (portal.parentAccountId !== null || @@ -68,79 +64,46 @@ function getHasSandboxesByType(parentAccountConfig, type) { return false; } -function getSandboxLimit(error) { - // Error context should contain a limit property with a list of one number. That number is the current limit - const limit = error.context && error.context.limit && error.context.limit[0]; - return limit ? parseInt(limit, 10) : 1; // Default to 1 -} - // Fetches available sync types for a given sandbox portal -async function getAvailableSyncTypes(parentAccountConfig, config) { +export async function getAvailableSyncTypes( + parentAccountConfig: CLIAccount, + config: CLIAccount +): Promise> { const parentId = getAccountIdentifier(parentAccountConfig); const parentPortalId = getAccountId(parentId); const id = getAccountIdentifier(config); const portalId = getAccountId(id); + + if (!parentPortalId || !portalId) { + throw new Error(i18n(`${i18nKey}.sync.failure.syncTypeFetch`)); + } + const { data: { results: syncTypes }, } = await fetchTypes(parentPortalId, portalId); if (!syncTypes) { - throw new Error( - 'Unable to fetch available sandbox sync types. Please try again.' - ); + throw new Error(i18n(`${i18nKey}.sync.failure.syncTypeFetch`)); } return syncTypes.map(t => ({ type: t.name })); } -/** - * @param {Object} accountConfig - Account config of sandbox portal - * @param {Array} availableSyncTasks - Array of available sync tasks - * @param {Boolean} skipPrompt - Option to skip contact records prompt and return all available sync tasks - * @returns {Array} Adjusted available sync task items - */ -const getSyncTypesWithContactRecordsPrompt = async ( - accountConfig, - syncTasks, - skipPrompt = false -) => { - // TODO: remove this entire helper once hs sandbox sync is fully deprecated - const isDevSandbox = isDevelopmentSandbox(accountConfig); - if (isDevSandbox) { - // Disable dev sandbox from syncing contacts - return syncTasks.filter(t => t.type !== syncTypes.OBJECT_RECORDS); - } - if ( - syncTasks && - syncTasks.some(t => t.type === syncTypes.OBJECT_RECORDS) && - !skipPrompt - ) { - const { contactRecordsSyncPrompt } = await promptUser([ - { - name: 'contactRecordsSyncPrompt', - type: 'confirm', - message: i18n('lib.sandbox.sync.confirm.syncContactRecords.standard'), - }, - ]); - if (!contactRecordsSyncPrompt) { - return syncTasks.filter(t => t.type !== syncTypes.OBJECT_RECORDS); - } - } - return syncTasks; -}; - -/** - * @param {Object} accountConfig - Account config of sandbox portal - * @param {String} sandboxType - Sandbox type for limit validation - * @param {String} env - Environment - * @returns {null} - */ -const validateSandboxUsageLimits = async (accountConfig, sandboxType, env) => { +export async function validateSandboxUsageLimits( + accountConfig: CLIAccount, + sandboxType: AccountType, + env: Environment +): Promise { const id = getAccountIdentifier(accountConfig); const accountId = getAccountId(id); + + if (!accountId) { + throw new Error(i18n(`${i18nKey}.create.failure.usageLimitFetch`)); + } + const { data: { usage }, } = await getSandboxUsageLimits(accountId); if (!usage) { - throw new Error('Unable to fetch sandbox usage limits. Please try again.'); + throw new Error(i18n(`${i18nKey}.create.failure.usageLimitFetch`)); } if (sandboxType === HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX) { if (usage['DEVELOPER'].available === 0) { @@ -153,7 +116,7 @@ const validateSandboxUsageLimits = async (accountConfig, sandboxType, env) => { if (hasDevelopmentSandboxes) { throw new Error( i18n( - `lib.sandbox.create.failure.alreadyInConfig.developer.${ + `${i18nKey}.create.failure.alreadyInConfig.developer.${ plural ? 'other' : 'one' }`, { @@ -166,7 +129,7 @@ const validateSandboxUsageLimits = async (accountConfig, sandboxType, env) => { const baseUrl = getHubSpotWebsiteOrigin(env); throw new Error( i18n( - `lib.sandbox.create.failure.limit.developer.${ + `${i18nKey}.create.failure.limit.developer.${ plural ? 'other' : 'one' }`, { @@ -190,7 +153,7 @@ const validateSandboxUsageLimits = async (accountConfig, sandboxType, env) => { if (hasStandardSandboxes) { throw new Error( i18n( - `lib.sandbox.create.failure.alreadyInConfig.standard.${ + `${i18nKey}.create.failure.alreadyInConfig.standard.${ plural ? 'other' : 'one' }`, { @@ -203,7 +166,7 @@ const validateSandboxUsageLimits = async (accountConfig, sandboxType, env) => { const baseUrl = getHubSpotWebsiteOrigin(env); throw new Error( i18n( - `lib.sandbox.create.failure.limit.standard.${ + `${i18nKey}.create.failure.limit.standard.${ plural ? 'other' : 'one' }`, { @@ -216,25 +179,24 @@ const validateSandboxUsageLimits = async (accountConfig, sandboxType, env) => { } } } -}; +} -function handleSandboxCreateError({ - err, - env, - accountId, - name, - accountConfig, -}) { +export function handleSandboxCreateError( + err: unknown, + env: Environment, + name: string, + accountId: number +) { if (isMissingScopeError(err)) { logger.error( - i18n('lib.sandboxes.create.failure.scopes.message', { + i18n(`${i18nKey}.create.failure.scopes.message`, { accountName: uiAccountDescription(accountId), }) ); const websiteOrigin = getHubSpotWebsiteOrigin(env); const url = `${websiteOrigin}/personal-access-key/${accountId}`; logger.info( - i18n('lib.sandboxes.create.failure.scopes.instructions', { + i18n(`${i18nKey}.create.failure.scopes.instructions`, { accountName: uiAccountDescription(accountId), url, }) @@ -248,7 +210,7 @@ function handleSandboxCreateError({ ) { logger.log(''); logger.error( - i18n('lib.sandboxes.create.failure.invalidUser', { + i18n(`${i18nKey}.create.failure.invalidUser`, { accountName: name, parentAccountName: uiAccountDescription(accountId), }) @@ -263,117 +225,15 @@ function handleSandboxCreateError({ ) { logger.log(''); logger.error( - i18n('lib.sandboxes.create.failure.403Gating', { + i18n(`${i18nKey}.create.failure.403Gating`, { accountName: name, parentAccountName: uiAccountDescription(accountId), accountId, }) ); logger.log(''); - } else if ( - isSpecifiedError(err, { - statusCode: 400, - category: 'VALIDATION_ERROR', - subCategory: - 'SandboxErrors.NUM_DEVELOPMENT_SANDBOXES_LIMIT_EXCEEDED_ERROR', - }) && - err.error && - err.error.message - ) { - logger.log(''); - const devSandboxLimit = getSandboxLimit(err.error); - const plural = devSandboxLimit !== 1; - const hasDevelopmentSandboxes = getHasSandboxesByType( - accountConfig, - HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX - ); - if (hasDevelopmentSandboxes) { - logger.error( - i18n( - `lib.sandboxes.create.failure.alreadyInConfig.developer.${ - plural ? 'other' : 'one' - }`, - { - accountName: uiAccountDescription(accountId), - limit: devSandboxLimit, - } - ) - ); - } else { - const baseUrl = getHubSpotWebsiteOrigin(getValidEnv(getEnv(accountId))); - logger.error( - i18n( - `lib.sandboxes.create.failure.limit.developer.${ - plural ? 'other' : 'one' - }`, - { - accountName: uiAccountDescription(accountId), - limit: devSandboxLimit, - link: `${baseUrl}/sandboxes-developer/${accountId}/development`, - } - ) - ); - } - logger.log(''); - } else if ( - isSpecifiedError(err, { - statusCode: 400, - category: 'VALIDATION_ERROR', - subCategory: 'SandboxErrors.NUM_STANDARD_SANDBOXES_LIMIT_EXCEEDED_ERROR', - }) && - err.error && - err.error.message - ) { - logger.log(''); - const standardSandboxLimit = getSandboxLimit(err.error); - const plural = standardSandboxLimit !== 1; - const hasStandardSandboxes = getHasSandboxesByType( - accountConfig, - HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX - ); - if (hasStandardSandboxes) { - logger.error( - i18n( - `lib.sandboxes.create.failure.alreadyInConfig.standard.${ - plural ? 'other' : 'one' - }`, - { - accountName: uiAccountDescription(accountId), - limit: standardSandboxLimit, - } - ) - ); - } else { - const baseUrl = getHubSpotWebsiteOrigin(getValidEnv(getEnv(accountId))); - logger.error( - i18n( - `lib.sandboxes.create.failure.limit.standard.${ - plural ? 'other' : 'one' - }`, - { - accountName: uiAccountDescription(accountId), - limit: standardSandboxLimit, - link: `${baseUrl}/sandboxes-developer/${accountId}/standard`, - } - ) - ); - } - logger.log(''); } else { logError(err); } throw err; } - -module.exports = { - sandboxTypeMap, - sandboxApiTypeMap, - syncTypes, - getSandboxTypeAsString, - getHasSandboxesByType, - getSandboxLimit, - validateSandboxUsageLimits, - getAvailableSyncTypes, - getSyncTypesWithContactRecordsPrompt, - handleSandboxCreateError, -}; diff --git a/types/Sandboxes.ts b/types/Sandboxes.ts new file mode 100644 index 000000000..3b58ecfc5 --- /dev/null +++ b/types/Sandboxes.ts @@ -0,0 +1,3 @@ +export type SandboxSyncTask = { + type: string; +}; From 0c4c1c1539c9cb5b9a6bcb618535aab5aa5de9ac Mon Sep 17 00:00:00 2001 From: Branden Rodgers Date: Mon, 13 Jan 2025 12:44:59 -0500 Subject: [PATCH 03/11] chore: Port dependencyManagement to TS (#1334) --- lib/dependencyManagement.ts | 64 +++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/lib/dependencyManagement.ts b/lib/dependencyManagement.ts index 7d124ee3a..e5ef8a62a 100644 --- a/lib/dependencyManagement.ts +++ b/lib/dependencyManagement.ts @@ -1,21 +1,20 @@ -// @ts-nocheck -const { logger } = require('@hubspot/local-dev-lib/logger'); -const { getProjectConfig } = require('./projects'); -const { exec: execAsync } = require('child_process'); -const { walk } = require('@hubspot/local-dev-lib/fs'); -const path = require('path'); -const { uiLink } = require('./ui'); -const util = require('util'); -const { i18n } = require('./lang'); -const SpinniesManager = require('./ui/SpinniesManager'); -const fs = require('fs'); -const pkg = require('../package.json'); -const DEFAULT_PACKAGE_MANAGER = 'npm'; +import { logger } from '@hubspot/local-dev-lib/logger'; +import { getProjectConfig } from './projects'; +import { exec as execAsync } from 'child_process'; +import { walk } from '@hubspot/local-dev-lib/fs'; +import path from 'path'; +import { uiLink } from './ui'; +import util from 'util'; +import { i18n } from './lang'; +import SpinniesManager from './ui/SpinniesManager'; +import fs from 'fs'; +import pkg from '../package.json'; +const DEFAULT_PACKAGE_MANAGER = 'npm'; const i18nKey = `commands.project.subcommands.installDeps`; class NoPackageJsonFilesError extends Error { - constructor(projectName) { + constructor(projectName: string) { super( i18n(`${i18nKey}.noPackageJsonInProject`, { projectName, @@ -28,7 +27,7 @@ class NoPackageJsonFilesError extends Error { } } -export async function isGloballyInstalled(command) { +export async function isGloballyInstalled(command: string): Promise { const exec = util.promisify(execAsync); try { await exec(`${command} --version`); @@ -38,24 +37,36 @@ export async function isGloballyInstalled(command) { } } -export async function getLatestCliVersion(): { latest: string; next: string } { +export async function getLatestCliVersion(): Promise<{ + latest: string; + next: string; +}> { const exec = util.promisify(execAsync); const { stdout } = await exec(`npm info ${pkg.name} dist-tags --json`); const { latest, next } = JSON.parse(stdout); return { latest, next }; } -async function installPackages({ packages, installLocations }) { +export async function installPackages({ + packages, + installLocations, +}: { + packages?: string[]; + installLocations?: string[]; +}): Promise { const installDirs = installLocations || (await getProjectPackageJsonLocations()); await Promise.all( installDirs.map(async dir => { - await installPackagesInDirectory(packages, dir); + await installPackagesInDirectory(dir, packages); }) ); } -async function installPackagesInDirectory(packages, directory) { +async function installPackagesInDirectory( + directory: string, + packages?: string[] +): Promise { const spinner = `installingDependencies-${directory}`; const relativeDir = path.relative(process.cwd(), directory); SpinniesManager.init(); @@ -102,7 +113,7 @@ async function installPackagesInDirectory(packages, directory) { } } -async function getProjectPackageJsonLocations() { +export async function getProjectPackageJsonLocations(): Promise { const projectConfig = await getProjectConfig(); if ( @@ -148,7 +159,7 @@ async function getProjectPackageJsonLocations() { throw new NoPackageJsonFilesError(name); } - const packageParentDirs = []; + const packageParentDirs: string[] = []; packageJsonFiles.forEach(packageJsonFile => { const parentDir = path.dirname(packageJsonFile); packageParentDirs.push(parentDir); @@ -157,19 +168,10 @@ async function getProjectPackageJsonLocations() { return packageParentDirs; } -export async function hasMissingPackages(directory) { +export async function hasMissingPackages(directory: string): Promise { const exec = util.promisify(execAsync); const { stdout } = await exec(`npm install --ignore-scripts --dry-run`, { cwd: directory, }); return !stdout?.includes('up to date in'); } - -module.exports = { - isGloballyInstalled, - installPackages, - DEFAULT_PACKAGE_MANAGER, - getProjectPackageJsonLocations, - getLatestCliVersion, - hasMissingPackages, -}; From d58f2906711ffa0f88bff1d550a859e9e42aec73 Mon Sep 17 00:00:00 2001 From: Joe Yeager Date: Mon, 13 Jan 2025 10:19:16 -0800 Subject: [PATCH 04/11] chore: Build the project before running the tests (#1330) --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index e2af1bba7..06cc0c486 100644 --- a/package.json +++ b/package.json @@ -64,10 +64,10 @@ "list-all-commands": "yarn ts-node ./scripts/get-all-commands.ts", "prettier:write": "prettier --write ./**/*.{ts,js,json}", "test": "jest", - "test-cli": "yarn --cwd 'acceptance-tests' test-ci", - "test-cli-debug": "yarn --cwd 'acceptance-tests' test-debug", - "test-cli-qa": "yarn --cwd 'acceptance-tests' test-qa", - "test-cli-latest": "yarn build-docker && docker container run -it --rm --name=hs-cli-container hs-cli-image yarn --cwd 'acceptance-tests' test-latest", + "test-cli": "yarn build && yarn --cwd 'acceptance-tests' test-ci", + "test-cli-debug": "yarn build && yarn --cwd 'acceptance-tests' test-debug", + "test-cli-qa": "yarn build && yarn --cwd 'acceptance-tests' test-qa", + "test-cli-latest": "yarn build && yarn build-docker && docker container run -it --rm --name=hs-cli-container hs-cli-image yarn --cwd 'acceptance-tests' test-latest", "build-docker": "docker image build --tag hs-cli-image . && docker image prune -f", "circular-deps": "yarn madge --circular .", "release": "yarn ts-node ./scripts/release.ts release", From 196917cc857384ead27da0225fad93f78e38e075 Mon Sep 17 00:00:00 2001 From: Joe Yeager Date: Mon, 13 Jan 2025 10:22:56 -0800 Subject: [PATCH 05/11] chore: Disable secrets test while caching issue is addressed (#1336) --- acceptance-tests/tests/workflows/secretsFlow.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/acceptance-tests/tests/workflows/secretsFlow.spec.ts b/acceptance-tests/tests/workflows/secretsFlow.spec.ts index 5eaaca3f2..ad07caa2a 100644 --- a/acceptance-tests/tests/workflows/secretsFlow.spec.ts +++ b/acceptance-tests/tests/workflows/secretsFlow.spec.ts @@ -25,7 +25,8 @@ async function waitForSecretsListToContainSecret(testState: TestState) { .toContain(SECRET.name); } -describe('Secrets Flow', () => { +// TODO: Re-enable when the caching issue is resolved on the BE +describe.skip('Secrets Flow', () => { let testState: TestState; beforeAll(async () => { From 49b2a42c808229e66cf9fa181c673d3f55f1875a Mon Sep 17 00:00:00 2001 From: Branden Rodgers Date: Mon, 13 Jan 2025 18:14:30 -0500 Subject: [PATCH 06/11] chore: Port DevServerManager to TS (#1335) --- commands/project/dev.ts | 7 +-- commands/theme/preview.ts | 8 +-- lib/DevServerManager.ts | 116 +++++++++++++++++++++++++++++--------- lib/LocalDevManager.ts | 17 +++--- lib/projects/structure.ts | 92 +++++------------------------- types/Projects.ts | 71 +++++++++++++++++++++++ 6 files changed, 185 insertions(+), 126 deletions(-) diff --git a/commands/project/dev.ts b/commands/project/dev.ts index 762028634..b77ce22cf 100644 --- a/commands/project/dev.ts +++ b/commands/project/dev.ts @@ -30,11 +30,10 @@ const { isAppDeveloperAccount, } = require('../../lib/accountTypes'); const { getValidEnv } = require('@hubspot/local-dev-lib/environment'); - +const { ComponentTypes } = require('../../types/Projects'); const { findProjectComponents, getProjectComponentTypes, - COMPONENT_TYPES, } = require('../../lib/projects/structure'); const { confirmDefaultAccountIsTarget, @@ -82,8 +81,8 @@ exports.handler = async options => { const components = await findProjectComponents(projectDir); const runnableComponents = components.filter(component => component.runnable); const componentTypes = getProjectComponentTypes(runnableComponents); - const hasPrivateApps = !!componentTypes[COMPONENT_TYPES.privateApp]; - const hasPublicApps = !!componentTypes[COMPONENT_TYPES.publicApp]; + const hasPrivateApps = !!componentTypes[ComponentTypes.PrivateApp]; + const hasPublicApps = !!componentTypes[ComponentTypes.PublicApp]; if (runnableComponents.length === 0) { logger.error( diff --git a/commands/theme/preview.ts b/commands/theme/preview.ts index c82f6dd46..5bbfdf027 100644 --- a/commands/theme/preview.ts +++ b/commands/theme/preview.ts @@ -20,10 +20,8 @@ const { ApiErrorContext, logError } = require('../../lib/errorHandlers/index'); const { handleExit, handleKeypress } = require('../../lib/process'); const { getThemeJSONPath } = require('@hubspot/local-dev-lib/cms/themes'); const { getProjectConfig } = require('../../lib/projects'); -const { - findProjectComponents, - COMPONENT_TYPES, -} = require('../../lib/projects/structure'); +const { findProjectComponents } = require('../../lib/projects/structure'); +const { ComponentTypes } = require('../../types/Projects'); const { preview } = require('@hubspot/theme-preview-dev-server'); const { hasFeature } = require('../../lib/hasFeature'); const i18nKey = 'commands.theme.subcommands.preview'; @@ -85,7 +83,7 @@ const determineSrcAndDest = async options => { if (!themeJsonPath) { const projectComponents = await findProjectComponents(projectDir); const themeComponents = projectComponents.filter( - c => c.type === COMPONENT_TYPES.hublTheme + c => c.type === ComponentTypes.HublTheme ); if (themeComponents.length === 0) { logger.error(i18n(`${i18nKey}.errors.noThemeComponents`)); diff --git a/lib/DevServerManager.ts b/lib/DevServerManager.ts index 525cf5751..90644e676 100644 --- a/lib/DevServerManager.ts +++ b/lib/DevServerManager.ts @@ -1,48 +1,77 @@ -// @ts-nocheck -const { logger } = require('@hubspot/local-dev-lib/logger'); -const { COMPONENT_TYPES } = require('./projects/structure'); -const { i18n } = require('./lang'); -const { promptUser } = require('./prompts/promptUtils'); -const { DevModeInterface } = require('@hubspot/ui-extensions-dev-server'); -const { +import { logger } from '@hubspot/local-dev-lib/logger'; +import { Environment } from '@hubspot/local-dev-lib/types/Config'; +import { i18n } from './lang'; +import { promptUser } from './prompts/promptUtils'; +import { DevModeInterface as UIEDevModeInterface } from '@hubspot/ui-extensions-dev-server'; +import { startPortManagerServer, stopPortManagerServer, requestPorts, -} = require('@hubspot/local-dev-lib/portManager'); -const { +} from '@hubspot/local-dev-lib/portManager'; +import { getHubSpotApiOrigin, getHubSpotWebsiteOrigin, -} = require('@hubspot/local-dev-lib/urls'); -const { getAccountConfig } = require('@hubspot/local-dev-lib/config'); +} from '@hubspot/local-dev-lib/urls'; +import { getAccountConfig } from '@hubspot/local-dev-lib/config'; +import { ProjectConfig, ComponentTypes, Component } from '../types/Projects'; const i18nKey = 'lib.DevServerManager'; const SERVER_KEYS = { privateApp: 'privateApp', publicApp: 'publicApp', +} as const; + +type ServerKey = keyof typeof SERVER_KEYS; + +type DevServerInterface = { + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type + setup?: Function; + start?: (options: object) => Promise; + fileChange?: (filePath: string, event: string) => Promise; + cleanup?: () => Promise; +}; + +type DevServer = { + componentType: ComponentTypes; + serverInterface: DevServerInterface; +}; + +type ComponentsByType = { + [key in ComponentTypes]?: { [key: string]: Component }; }; class DevServerManager { + private initialized: boolean; + private started: boolean; + private componentsByType: ComponentsByType; + private devServers: { [key in ServerKey]: DevServer }; + constructor() { this.initialized = false; this.started = false; this.componentsByType = {}; - this.server = null; - this.path = null; this.devServers = { [SERVER_KEYS.privateApp]: { - componentType: COMPONENT_TYPES.privateApp, - serverInterface: DevModeInterface, + componentType: ComponentTypes.PrivateApp, + serverInterface: UIEDevModeInterface, }, [SERVER_KEYS.publicApp]: { - componentType: COMPONENT_TYPES.publicApp, - serverInterface: DevModeInterface, + componentType: ComponentTypes.PublicApp, + serverInterface: UIEDevModeInterface, }, }; } - async iterateDevServers(callback) { - const serverKeys = Object.keys(this.devServers); + async iterateDevServers( + callback: ( + serverInterface: DevServerInterface, + compatibleComponents: { + [key: string]: Component; + } + ) => Promise + ): Promise { + const serverKeys: ServerKey[] = Object.keys(this.devServers) as ServerKey[]; for (let i = 0; i < serverKeys.length; i++) { const serverKey = serverKeys[i]; @@ -59,21 +88,37 @@ class DevServerManager { } } - arrangeComponentsByType(components) { - return components.reduce((acc, component) => { + arrangeComponentsByType(components: Component[]): ComponentsByType { + return components.reduce((acc, component) => { if (!acc[component.type]) { acc[component.type] = {}; } - acc[component.type][component.config.name] = component; + if ('name' in component.config && component.config.name) { + acc[component.type]![component.config.name] = component; + } return acc; }, {}); } - async setup({ components, onUploadRequired, accountId, setActiveApp }) { + async setup({ + components, + onUploadRequired, + accountId, + setActiveApp, + }: { + components: Component[]; + onUploadRequired: () => void; + accountId: number; + setActiveApp: (appUid: string | undefined) => Promise; + }): Promise { this.componentsByType = this.arrangeComponentsByType(components); - const { env } = getAccountConfig(accountId); + let env: Environment; + const accountConfig = getAccountConfig(accountId); + if (accountConfig) { + env = accountConfig.env; + } await startPortManagerServer(); await this.iterateDevServers( async (serverInterface, compatibleComponents) => { @@ -96,7 +141,13 @@ class DevServerManager { this.initialized = true; } - async start({ accountId, projectConfig }) { + async start({ + accountId, + projectConfig, + }: { + accountId: number; + projectConfig: ProjectConfig; + }): Promise { if (this.initialized) { await this.iterateDevServers(async serverInterface => { if (serverInterface.start) { @@ -114,7 +165,13 @@ class DevServerManager { this.started = true; } - fileChange({ filePath, event }) { + async fileChange({ + filePath, + event, + }: { + filePath: string; + event: string; + }): Promise { if (this.started) { this.iterateDevServers(async serverInterface => { if (serverInterface.fileChange) { @@ -124,7 +181,7 @@ class DevServerManager { } } - async cleanup() { + async cleanup(): Promise { if (this.started) { await this.iterateDevServers(async serverInterface => { if (serverInterface.cleanup) { @@ -137,4 +194,7 @@ class DevServerManager { } } -module.exports = new DevServerManager(); +const Manager = new DevServerManager(); + +export default Manager; +module.exports = Manager; diff --git a/lib/LocalDevManager.ts b/lib/LocalDevManager.ts index 5503f6c10..18af42151 100644 --- a/lib/LocalDevManager.ts +++ b/lib/LocalDevManager.ts @@ -22,11 +22,8 @@ const DevServerManager = require('./DevServerManager'); const { EXIT_CODES } = require('./enums/exitCodes'); const { getProjectDetailUrl } = require('./projects/urls'); const { getAccountHomeUrl } = require('./localDev'); -const { - CONFIG_FILES, - COMPONENT_TYPES, - getAppCardConfigs, -} = require('./projects/structure'); +const { CONFIG_FILES, getAppCardConfigs } = require('./projects/structure'); +const { ComponentTypes } = require('../types/Projects'); const { UI_COLORS, uiCommandReference, @@ -92,7 +89,7 @@ class LocalDevManager { return component.config.uid === appUid; }); - if (this.activeApp.type === COMPONENT_TYPES.publicApp) { + if (this.activeApp.type === ComponentTypes.PublicApp) { try { await this.setActivePublicAppData(); await this.checkActivePublicAppInstalls(); @@ -212,7 +209,7 @@ class LocalDevManager { ) ); - if (this.activeApp.type === COMPONENT_TYPES.publicApp) { + if (this.activeApp.type === ComponentTypes.PublicApp) { logger.log( uiLink( i18n(`${i18nKey}.viewTestAccountLink`), @@ -319,7 +316,7 @@ class LocalDevManager { let warning = reason; if (!reason) { warning = - this.activeApp.type === COMPONENT_TYPES.publicApp && + this.activeApp.type === ComponentTypes.PublicApp && this.publicAppActiveInstalls > 0 ? i18n(`${i18nKey}.uploadWarning.defaultPublicAppWarning`, { installCount: this.publicAppActiveInstalls, @@ -383,7 +380,7 @@ class LocalDevManager { const missingComponents = []; this.runnableComponents.forEach(({ type, config, path }) => { - if (Object.values(COMPONENT_TYPES).includes(type)) { + if (Object.values(ComponentTypes).includes(type)) { const cardConfigs = getAppCardConfigs(config, path); if (!deployedComponentNames.includes(config.name)) { @@ -423,7 +420,7 @@ class LocalDevManager { }); const configPaths = this.runnableComponents - .filter(({ type }) => Object.values(COMPONENT_TYPES).includes(type)) + .filter(({ type }) => Object.values(ComponentTypes).includes(type)) .map(component => { const appConfigPath = path.join( component.path, diff --git a/lib/projects/structure.ts b/lib/projects/structure.ts index a36b10480..d6ad0a903 100644 --- a/lib/projects/structure.ts +++ b/lib/projects/structure.ts @@ -1,89 +1,23 @@ import * as fs from 'fs'; import * as path from 'path'; -import { ValueOf } from '@hubspot/local-dev-lib/types/Utils'; import { walk } from '@hubspot/local-dev-lib/fs'; import { logger } from '@hubspot/local-dev-lib/logger'; import { logError } from '../errorHandlers/index'; - -export type Component = { - type: ComponentTypes; - config: object; - runnable: boolean; - path: string; -}; - -type PrivateAppComponentConfig = { - name: string; - description: string; - uid: string; - scopes: Array; - public: boolean; - extensions?: { - crm: { - cards: Array<{ file: string }>; - }; - }; -}; - -type PublicAppComponentConfig = { - name: string; - uid: string; - description: string; - allowedUrls: Array; - auth: { - redirectUrls: Array; - requiredScopes: Array; - optionalScopes: Array; - conditionallyRequiredScopes: Array; - }; - support: { - supportEmail: string; - documentationUrl: string; - supportUrl: string; - supportPhone: string; - }; - extensions?: { - crm: { - cards: Array<{ file: string }>; - }; - }; - webhooks?: { - file: string; - }; -}; - -type AppCardComponentConfig = { - type: 'crm-card'; - data: { - title: string; - uid: string; - location: string; - module: { - file: string; - }; - objectTypes: Array<{ name: string }>; - }; -}; - -type GenericComponentConfig = - | PublicAppComponentConfig - | PrivateAppComponentConfig - | AppCardComponentConfig; - -export const COMPONENT_TYPES = { - privateApp: 'private-app', - publicApp: 'public-app', - hublTheme: 'hubl-theme', -} as const; - -type ComponentTypes = ValueOf; +import { + ComponentTypes, + Component, + GenericComponentConfig, + PublicAppComponentConfig, + PrivateAppComponentConfig, + AppCardComponentConfig, +} from '../../types/Projects'; export const CONFIG_FILES: { - [k in ValueOf]: string; + [k in ComponentTypes]: string; } = { - [COMPONENT_TYPES.privateApp]: 'app.json', - [COMPONENT_TYPES.publicApp]: 'public-app.json', - [COMPONENT_TYPES.hublTheme]: 'theme.json', + [ComponentTypes.PrivateApp]: 'app.json', + [ComponentTypes.PublicApp]: 'public-app.json', + [ComponentTypes.HublTheme]: 'theme.json', }; function getComponentTypeFromConfigFile( @@ -189,7 +123,7 @@ export async function findProjectComponents( if (parsedConfig) { const isLegacy = getIsLegacyApp(parsedConfig, dir); - const isHublTheme = base === CONFIG_FILES[COMPONENT_TYPES.hublTheme]; + const isHublTheme = base === CONFIG_FILES[ComponentTypes.HublTheme]; const componentType = getComponentTypeFromConfigFile(base); if (componentType) { diff --git a/types/Projects.ts b/types/Projects.ts index e6010faeb..0d7b9ac25 100644 --- a/types/Projects.ts +++ b/types/Projects.ts @@ -49,3 +49,74 @@ export type ProjectTemplateRepoConfig = { projects?: ProjectTemplate[]; components?: ComponentTemplate[]; }; + +export type PrivateAppComponentConfig = { + name: string; + description: string; + uid: string; + scopes: Array; + public: boolean; + extensions?: { + crm: { + cards: Array<{ file: string }>; + }; + }; +}; + +export type PublicAppComponentConfig = { + name: string; + uid: string; + description: string; + allowedUrls: Array; + auth: { + redirectUrls: Array; + requiredScopes: Array; + optionalScopes: Array; + conditionallyRequiredScopes: Array; + }; + support: { + supportEmail: string; + documentationUrl: string; + supportUrl: string; + supportPhone: string; + }; + extensions?: { + crm: { + cards: Array<{ file: string }>; + }; + }; + webhooks?: { + file: string; + }; +}; + +export type AppCardComponentConfig = { + type: 'crm-card'; + data: { + title: string; + uid: string; + location: string; + module: { + file: string; + }; + objectTypes: Array<{ name: string }>; + }; +}; + +export type GenericComponentConfig = + | PublicAppComponentConfig + | PrivateAppComponentConfig + | AppCardComponentConfig; + +export enum ComponentTypes { + PrivateApp = 'private-app', + PublicApp = 'public-app', + HublTheme = 'hubl-theme', +} + +export type Component = { + type: ComponentTypes; + config: GenericComponentConfig; + runnable: boolean; + path: string; +}; From 48e9d585fd83e7c451fcd7dd4b7562257858e377 Mon Sep 17 00:00:00 2001 From: Joe Yeager Date: Tue, 14 Jan 2025 10:03:51 -0800 Subject: [PATCH 07/11] fix: Revert regression to the debug logging for axios errors (#1338) --- lib/errorHandlers/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/lib/errorHandlers/index.ts b/lib/errorHandlers/index.ts index 0a521d1c9..45f74b8e3 100644 --- a/lib/errorHandlers/index.ts +++ b/lib/errorHandlers/index.ts @@ -6,6 +6,7 @@ import { import { shouldSuppressError } from './suppressError'; import { i18n } from '../lang'; import util from 'util'; +import { isAxiosError } from 'axios'; const i18nKey = 'lib.errorHandlers.index'; @@ -50,10 +51,12 @@ export function debugError(error: unknown, context?: ApiErrorContext): void { logger.debug(i18n(`${i18nKey}.errorOccurred`, { error: String(error) })); } - if (error instanceof Error) { + if (error instanceof Error && error.cause) { logger.debug( i18n(`${i18nKey}.errorCause`, { - cause: util.inspect(error.cause, false, null, true), + cause: isAxiosError(error.cause) + ? `${error.cause}` + : util.inspect(error.cause, false, null, true), }) ); } From 5d3aec41551fbc76c3bd2cd1be57678bdfaff5a7 Mon Sep 17 00:00:00 2001 From: Joe Yeager Date: Wed, 15 Jan 2025 10:23:37 -0800 Subject: [PATCH 08/11] chore: Add a debugging command (#1340) --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 06cc0c486..d27048d5a 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "circular-deps": "yarn madge --circular .", "release": "yarn ts-node ./scripts/release.ts release", "hs": "yarn build && node ./dist/bin/hs", + "hs-debug": "yarn build && NODE_DEBUG=http* node --inspect-brk ./dist/bin/hs", "update-ldl": "yarn add --exact @hubspot/local-dev-lib@latest" }, "lint-staged": { From e8f353fb91845835b716447eaa2949c71ae7e629 Mon Sep 17 00:00:00 2001 From: ruslanhubspot <165080640+ruslanhubspot@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:34:39 -0500 Subject: [PATCH 09/11] Update hs upload --clean flag description (#1343) --- lang/en.lyaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/en.lyaml b/lang/en.lyaml index 0b4f5c7e4..10a8691b0 100644 --- a/lang/en.lyaml +++ b/lang/en.lyaml @@ -972,7 +972,7 @@ en: convertFields: describe: "If true, converts any javascript fields files contained in module folder or project root." clean: - describe: "Will cause upload to delete files in your HubSpot account that are not found locally." + describe: "Will delete the destination directory and its contents before uploading. This will also clear the global content associated with any global partial templates and modules." force: describe: "Skips confirmation prompts when doing a clean upload." previewUrl: "To preview this theme, visit: {{ previewUrl }}" From c045dfbcdd8d2f67dd27dbb3e27b152d3e35e9b5 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Thu, 16 Jan 2025 10:59:22 -0500 Subject: [PATCH 10/11] Convert lib/serverlessLogs and lib/buildAccount to TS (#1337) --- commands/logs.ts | 8 +- commands/sandbox/create.ts | 12 +- lang/en.lyaml | 2 +- lib/__tests__/serverlessLogs.test.ts | 48 ++--- lib/buildAccount.ts | 256 ++++++++++++++----------- lib/localDev.ts | 31 +-- lib/prompts/personalAccessKeyPrompt.ts | 2 +- lib/sandboxes.ts | 2 +- lib/serverlessLogs.ts | 98 +++++----- lib/ui/serverlessFunctionLogs.ts | 59 +++--- package.json | 2 +- yarn.lock | 8 +- 12 files changed, 265 insertions(+), 263 deletions(-) diff --git a/commands/logs.ts b/commands/logs.ts index 3570a1416..c6e86375e 100644 --- a/commands/logs.ts +++ b/commands/logs.ts @@ -61,13 +61,7 @@ const endpointLog = async (accountId, functionPath, options) => { } }; - await tailLogs({ - accountId, - compact, - tailCall, - fetchLatest, - name: functionPath, - }); + await tailLogs(accountId, functionPath, fetchLatest, tailCall, compact); } else if (latest) { try { const { data } = await getLatestFunctionLog(accountId, functionPath); diff --git a/commands/sandbox/create.ts b/commands/sandbox/create.ts index 33dbb1314..d05ef2b77 100644 --- a/commands/sandbox/create.ts +++ b/commands/sandbox/create.ts @@ -28,7 +28,7 @@ const { HUBSPOT_ACCOUNT_TYPES, HUBSPOT_ACCOUNT_TYPE_STRINGS, } = require('@hubspot/local-dev-lib/constants/config'); -const { buildNewAccount } = require('../../lib/buildAccount'); +const { buildSandbox } = require('../../lib/buildAccount'); const { hubspotAccountNamePrompt, } = require('../../lib/prompts/accountNamePrompt'); @@ -130,13 +130,13 @@ exports.handler = async options => { } try { - const { result } = await buildNewAccount({ - name: sandboxName, - accountType: sandboxType, + const result = await buildSandbox( + sandboxName, accountConfig, + sandboxType, env, - force, - }); + force + ); const sandboxAccountConfig = getAccountConfig(result.sandbox.sandboxHubId); // For v1 sandboxes, keep sync here. Once we migrate to v2, this will be handled by BE automatically diff --git a/lang/en.lyaml b/lang/en.lyaml index 10a8691b0..db54c51ef 100644 --- a/lang/en.lyaml +++ b/lang/en.lyaml @@ -1499,7 +1499,7 @@ en: missingScopeError: "Couldn't execute the {{ request }} because the access key for {{ accountName }} is missing required scopes. To update scopes, run {{ authCommand }}. Then deactivate the existing key and generate a new one that includes the missing scopes." serverless: verifyAccessKeyAndUserAccess: - fetchScopeDataError: "Error verifying access of scopeGroup {{ scopeGroup }}: {{ error }}" + fetchScopeDataError: "Error verifying access of scopeGroup {{ scopeGroup }}:" portalMissingScope: "Your account does not have access to this action. Talk to an account admin to request it." userMissingScope: "You don't have access to this action. Ask an account admin to change your permissions in Users & Teams settings." genericMissingScope: "Your access key does not allow this action. Please generate a new access key by running `hs auth personalaccesskey`." diff --git a/lib/__tests__/serverlessLogs.test.ts b/lib/__tests__/serverlessLogs.test.ts index 1cb92dd66..195787f82 100644 --- a/lib/__tests__/serverlessLogs.test.ts +++ b/lib/__tests__/serverlessLogs.test.ts @@ -12,16 +12,10 @@ const ACCOUNT_ID = 123; describe('lib/serverlessLogs', () => { describe('tailLogs()', () => { let stdinMock; - let spinnies; beforeEach(() => { jest.spyOn(process, 'exit').mockImplementation(() => {}); stdinMock = mockStdIn.stdin(); - spinnies = { - succeed: jest.fn(), - fail: jest.fn(), - stopAll: jest.fn(), - }; }); afterEach(() => { @@ -33,14 +27,16 @@ describe('lib/serverlessLogs', () => { const compact = false; const fetchLatest = jest.fn(() => { return Promise.resolve({ - id: '1234', - executionTime: 510, - log: 'Log message', - error: null, - status: 'SUCCESS', - createdAt: 1620232011451, - memory: '70/128 MB', - duration: '53.40 ms', + data: { + id: '1234', + executionTime: 510, + log: 'Log message', + error: null, + status: 'SUCCESS', + createdAt: 1620232011451, + memory: '70/128 MB', + duration: '53.40 ms', + }, }); }); const tailCall = jest.fn(() => { @@ -54,13 +50,7 @@ describe('lib/serverlessLogs', () => { }); }); - await tailLogs({ - accountId: ACCOUNT_ID, - compact, - spinnies, - fetchLatest, - tailCall, - }); + await tailLogs(ACCOUNT_ID, 'name', fetchLatest, tailCall, compact); jest.runOnlyPendingTimers(); expect(fetchLatest).toHaveBeenCalled(); @@ -116,13 +106,7 @@ describe('lib/serverlessLogs', () => { Promise.resolve({ data: latestLogResponse }) ); - await tailLogs({ - accountId: ACCOUNT_ID, - compact, - spinnies, - fetchLatest, - tailCall, - }); + await tailLogs(ACCOUNT_ID, 'name', fetchLatest, tailCall, compact); jest.runOnlyPendingTimers(); expect(outputLogs).toHaveBeenCalledWith( latestLogResponse, @@ -148,13 +132,7 @@ describe('lib/serverlessLogs', () => { ) ); - await tailLogs({ - accountId: ACCOUNT_ID, - compact, - spinnies, - fetchLatest, - tailCall, - }); + await tailLogs(ACCOUNT_ID, 'name', fetchLatest, tailCall, compact); jest.runOnlyPendingTimers(); expect(tailCall).toHaveBeenCalledTimes(2); }); diff --git a/lib/buildAccount.ts b/lib/buildAccount.ts index e6e236678..91f421bea 100644 --- a/lib/buildAccount.ts +++ b/lib/buildAccount.ts @@ -1,47 +1,39 @@ -// @ts-nocheck -const { +import { getAccessToken, updateConfigWithAccessToken, -} = require('@hubspot/local-dev-lib/personalAccessKey'); -const { - personalAccessKeyPrompt, -} = require('./prompts/personalAccessKeyPrompt'); -const { +} from '@hubspot/local-dev-lib/personalAccessKey'; +import { accountNameExistsInConfig, updateAccountConfig, writeConfig, getAccountId, -} = require('@hubspot/local-dev-lib/config'); -const { - getAccountIdentifier, -} = require('@hubspot/local-dev-lib/config/getAccountIdentifier'); -const { logger } = require('@hubspot/local-dev-lib/logger'); -const { i18n } = require('./lang'); -const { cliAccountNamePrompt } = require('./prompts/accountNamePrompt'); -const SpinniesManager = require('./ui/SpinniesManager'); -const { debugError, logError } = require('./errorHandlers/index'); -const { - createDeveloperTestAccount, -} = require('@hubspot/local-dev-lib/api/developerTestAccounts'); -const { - HUBSPOT_ACCOUNT_TYPES, -} = require('@hubspot/local-dev-lib/constants/config'); -const { createSandbox } = require('@hubspot/local-dev-lib/api/sandboxHubs'); -const { - SANDBOX_API_TYPE_MAP, - handleSandboxCreateError, -} = require('./sandboxes'); -const { - handleDeveloperTestAccountCreateError, -} = require('./developerTestAccounts'); - -async function saveAccountToConfig({ - env, - personalAccessKey, - accountName, - accountId, - force = false, -}) { +} from '@hubspot/local-dev-lib/config'; +import { getAccountIdentifier } from '@hubspot/local-dev-lib/config/getAccountIdentifier'; +import { logger } from '@hubspot/local-dev-lib/logger'; +import { createDeveloperTestAccount } from '@hubspot/local-dev-lib/api/developerTestAccounts'; +import { HUBSPOT_ACCOUNT_TYPES } from '@hubspot/local-dev-lib/constants/config'; +import { createSandbox } from '@hubspot/local-dev-lib/api/sandboxHubs'; +import { Environment } from '@hubspot/local-dev-lib/types/Config'; + +import { personalAccessKeyPrompt } from './prompts/personalAccessKeyPrompt'; +import { i18n } from './lang'; +import { cliAccountNamePrompt } from './prompts/accountNamePrompt'; +import SpinniesManager from './ui/SpinniesManager'; +import { debugError, logError } from './errorHandlers/index'; + +import { SANDBOX_API_TYPE_MAP, handleSandboxCreateError } from './sandboxes'; +import { handleDeveloperTestAccountCreateError } from './developerTestAccounts'; +import { CLIAccount } from '@hubspot/local-dev-lib/types/Accounts'; +import { DeveloperTestAccount } from '@hubspot/local-dev-lib/types/developerTestAccounts'; +import { SandboxResponse } from '@hubspot/local-dev-lib/types/Sandbox'; + +export async function saveAccountToConfig( + accountId: number | undefined, + accountName: string, + env: Environment, + personalAccessKey?: string, + force = false +): Promise { if (!personalAccessKey) { const configData = await personalAccessKeyPrompt({ env, @@ -57,8 +49,8 @@ async function saveAccountToConfig({ env ); - let validName = updatedConfig.name; - if (!updatedConfig.name) { + let validName = updatedConfig?.name || ''; + if (!updatedConfig?.name) { const nameForConfig = accountName.toLowerCase().split(' ').join('-'); validName = nameForConfig; const invalidAccountName = accountNameExistsInConfig(nameForConfig); @@ -83,8 +75,8 @@ async function saveAccountToConfig({ updateAccountConfig({ ...updatedConfig, - environment: updatedConfig.env, - tokenInfo: updatedConfig.auth.tokenInfo, + env: updatedConfig?.env, + tokenInfo: updatedConfig?.auth?.tokenInfo, name: validName, }); writeConfig(); @@ -93,105 +85,145 @@ async function saveAccountToConfig({ return validName; } -async function buildNewAccount({ - name, - accountType, - accountConfig, - env, - portalLimit, // Used only for developer test accounts - force = false, -}) { +export async function buildDeveloperTestAccount( + name: string, + accountConfig: CLIAccount, + env: Environment, + portalLimit: number +): Promise { + const i18nKey = 'lib.developerTestAccount.create.loading'; + + const id = getAccountIdentifier(accountConfig); + const accountId = getAccountId(id); + + if (!accountId) { + throw new Error(i18n(`${i18nKey}.fail`)); + } + SpinniesManager.init({ succeedColor: 'white', }); + + logger.log(''); + SpinniesManager.add('buildDeveloperTestAccount', { + text: i18n(`${i18nKey}.add`, { + accountName: name, + }), + }); + + let developerTestAccount: DeveloperTestAccount; + + try { + const { data } = await createDeveloperTestAccount(accountId, name); + + developerTestAccount = data; + + SpinniesManager.succeed('buildDeveloperTestAccount', { + text: i18n(`${i18nKey}.succeed`, { + accountName: name, + accountId: developerTestAccount.id, + }), + }); + } catch (e) { + debugError(e); + + SpinniesManager.fail('buildDeveloperTestAccount', { + text: i18n(`${i18nKey}.fail`, { + accountName: name, + }), + }); + + handleDeveloperTestAccountCreateError(e, accountId, env, portalLimit); + } + + try { + await saveAccountToConfig(accountId, name, env); + } catch (err) { + logError(err); + throw err; + } + + return developerTestAccount; +} + +type SandboxType = + | typeof HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX + | typeof HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX; + +type SandboxAccount = SandboxResponse & { + name: string; +}; + +export async function buildSandbox( + name: string, + accountConfig: CLIAccount, + sandboxType: SandboxType, + env: Environment, + force = false +): Promise { + let i18nKey: string; + if (sandboxType === HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX) { + i18nKey = 'lib.sandbox.create.loading.standard'; + } else { + i18nKey = 'lib.sandbox.create.loading.developer'; + } + const id = getAccountIdentifier(accountConfig); const accountId = getAccountId(id); - const isSandbox = - accountType === HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX || - accountType === HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX; - const isDeveloperTestAccount = - accountType === HUBSPOT_ACCOUNT_TYPES.DEVELOPER_TEST; - - let result; - let spinniesI18nKey; - if (isSandbox) { - if (accountType === HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX) { - spinniesI18nKey = 'lib.sandbox.create.loading.standard'; - } - if (accountType === HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX) { - spinniesI18nKey = 'lib.sandbox.create.loading.developer'; - } - } else if (isDeveloperTestAccount) { - spinniesI18nKey = 'lib.developerTestAccount.create.loading'; + + if (!accountId) { + throw new Error(i18n(`${i18nKey}.fail`)); } + SpinniesManager.init({ + succeedColor: 'white', + }); + logger.log(''); - SpinniesManager.add('buildNewAccount', { - text: i18n(`${spinniesI18nKey}.add`, { + SpinniesManager.add('buildSandbox', { + text: i18n(`${i18nKey}.add`, { accountName: name, }), }); - let resultAccountId; + let sandbox: SandboxAccount; + try { - if (isSandbox) { - const sandboxApiType = SANDBOX_API_TYPE_MAP[accountType]; // API expects sandbox type as 1 or 2. - - const { data } = await createSandbox(accountId, name, sandboxApiType); - result = { name, ...data }; - resultAccountId = result.sandbox.sandboxHubId; - } else if (isDeveloperTestAccount) { - const { data } = await createDeveloperTestAccount(accountId, name); - result = data; - resultAccountId = result.id; - } + const sandboxApiType = SANDBOX_API_TYPE_MAP[sandboxType]; + + const { data } = await createSandbox(accountId, name, sandboxApiType); + sandbox = { name, ...data }; - SpinniesManager.succeed('buildNewAccount', { - text: i18n(`${spinniesI18nKey}.succeed`, { + SpinniesManager.succeed('buildSandbox', { + text: i18n(`${i18nKey}.succeed`, { accountName: name, - accountId: resultAccountId, + accountId: sandbox.sandbox.sandboxHubId, }), }); - } catch (err) { - debugError(err); + } catch (e) { + debugError(e); - SpinniesManager.fail('buildNewAccount', { - text: i18n(`${spinniesI18nKey}.fail`, { + SpinniesManager.fail('buildSandbox', { + text: i18n(`${i18nKey}.fail`, { accountName: name, }), }); - if (isSandbox) { - handleSandboxCreateError(err, env, name, accountId); - } - if (isDeveloperTestAccount) { - handleDeveloperTestAccountCreateError(err, env, accountId, portalLimit); - } + handleSandboxCreateError(e, env, name, accountId); } - let configAccountName; - try { - // Response contains PAK, save to config here - configAccountName = await saveAccountToConfig({ + await saveAccountToConfig( + accountId, + name, env, - personalAccessKey: result.personalAccessKey, - accountName: name, - accountId: resultAccountId, - force, - }); + sandbox.personalAccessKey, + force + ); } catch (err) { logError(err); throw err; } - return { - configAccountName, - result, - }; + return sandbox; } - -module.exports = { - buildNewAccount, - saveAccountToConfig, -}; diff --git a/lib/localDev.ts b/lib/localDev.ts index 20d5b02b9..886e185ca 100644 --- a/lib/localDev.ts +++ b/lib/localDev.ts @@ -49,7 +49,11 @@ const { logError, ApiErrorContext } = require('./errorHandlers/index'); const { PERSONAL_ACCESS_KEY_AUTH_METHOD, } = require('@hubspot/local-dev-lib/constants/auth'); -const { buildNewAccount, saveAccountToConfig } = require('./buildAccount'); +const { + buildSandbox, + buildDeveloperTestAccount, + saveAccountToConfig, +} = require('./buildAccount'); const { hubspotAccountNamePrompt } = require('./prompts/accountNamePrompt'); const i18nKey = 'lib.localDev'; @@ -208,12 +212,12 @@ const createSandboxForLocalDev = async (accountId, accountConfig, env) => { accountId ); - const { result } = await buildNewAccount({ + const result = await buildSandbox( name, - accountType: HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX, accountConfig, - env, - }); + HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX, + env + ); const targetAccountId = result.sandbox.sandboxHubId; @@ -286,13 +290,12 @@ const createDeveloperTestAccountForLocalDev = async ( accountId ); - const { result } = await buildNewAccount({ + const result = await buildDeveloperTestAccount( name, - accountType: HUBSPOT_ACCOUNT_TYPES.DEVELOPER_TEST, accountConfig, env, - portalLimit: maxTestPortals, - }); + maxTestPortals + ); return result.id; } catch (err) { @@ -319,11 +322,11 @@ const useExistingDevTestAccount = async (env, account) => { logger.log(''); process.exit(EXIT_CODES.SUCCESS); } - const devTestAcctConfigName = await saveAccountToConfig({ - env, - accountName: account.accountName, - accountId: account.id, - }); + const devTestAcctConfigName = await saveAccountToConfig( + account.id, + account.accountName, + env + ); logger.success( i18n(`lib.developerTestAccount.create.success.configFileUpdated`, { accountName: devTestAcctConfigName, diff --git a/lib/prompts/personalAccessKeyPrompt.ts b/lib/prompts/personalAccessKeyPrompt.ts index dd15dc6ef..51c43436a 100644 --- a/lib/prompts/personalAccessKeyPrompt.ts +++ b/lib/prompts/personalAccessKeyPrompt.ts @@ -49,7 +49,7 @@ export async function personalAccessKeyPrompt({ account, }: { env: string; - account?: string; + account?: number; }): Promise { const websiteOrigin = getHubSpotWebsiteOrigin(env); let url = `${websiteOrigin}/l/personal-access-key`; diff --git a/lib/sandboxes.ts b/lib/sandboxes.ts index feb561dd8..b4a25698a 100644 --- a/lib/sandboxes.ts +++ b/lib/sandboxes.ts @@ -186,7 +186,7 @@ export function handleSandboxCreateError( env: Environment, name: string, accountId: number -) { +): never { if (isMissingScopeError(err)) { logger.error( i18n(`${i18nKey}.create.failure.scopes.message`, { diff --git a/lib/serverlessLogs.ts b/lib/serverlessLogs.ts index c716d7ece..476d4f4de 100644 --- a/lib/serverlessLogs.ts +++ b/lib/serverlessLogs.ts @@ -1,37 +1,41 @@ -// @ts-nocheck -const https = require('https'); -const SpinniesManager = require('./ui/SpinniesManager'); -const { handleExit, handleKeypress } = require('./process'); -const chalk = require('chalk'); -const { logger } = require('@hubspot/local-dev-lib/logger'); -const { outputLogs } = require('./ui/serverlessFunctionLogs'); -const { logError, ApiErrorContext } = require('./errorHandlers/index'); - -const { EXIT_CODES } = require('./enums/exitCodes'); -const { +import https from 'https'; +import chalk from 'chalk'; +import { logger } from '@hubspot/local-dev-lib/logger'; +import { isHubSpotHttpError, isMissingScopeError, -} = require('@hubspot/local-dev-lib/errors/index'); -const { +} from '@hubspot/local-dev-lib/errors/index'; +import { SCOPE_GROUPS, PERSONAL_ACCESS_KEY_AUTH_METHOD, -} = require('@hubspot/local-dev-lib/constants/auth'); -const { getAccountConfig } = require('@hubspot/local-dev-lib/config'); -const { fetchScopeData } = require('@hubspot/local-dev-lib/api/localDevAuth'); -const { i18n } = require('./lang'); +} from '@hubspot/local-dev-lib/constants/auth'; +import { getAccountConfig } from '@hubspot/local-dev-lib/config'; +import { fetchScopeData } from '@hubspot/local-dev-lib/api/localDevAuth'; + +import { outputLogs } from './ui/serverlessFunctionLogs'; +import { logError, ApiErrorContext } from './errorHandlers/index'; +import SpinniesManager from './ui/SpinniesManager'; +import { handleExit, handleKeypress } from './process'; +import { EXIT_CODES } from './enums/exitCodes'; +import { i18n } from './lang'; +import { HubSpotPromise } from '@hubspot/local-dev-lib/types/Http'; +import { + FunctionLog, + GetFunctionLogsResponse, +} from '@hubspot/local-dev-lib/types/Functions'; const TAIL_DELAY = 5000; -const base64EncodeString = valueToEncode => { +function base64EncodeString(valueToEncode: string): string { if (typeof valueToEncode !== 'string') { return valueToEncode; } const stringBuffer = Buffer.from(valueToEncode); return encodeURIComponent(stringBuffer.toString('base64')); -}; +} -const handleUserInput = () => { +function handleUserInput(): void { const onTerminate = async () => { SpinniesManager.remove('tailLogs'); SpinniesManager.remove('stopMessage'); @@ -44,10 +48,18 @@ const handleUserInput = () => { onTerminate(); } }); -}; +} -async function verifyAccessKeyAndUserAccess(accountId, scopeGroup) { +async function verifyAccessKeyAndUserAccess( + accountId: number, + scopeGroup: string +): Promise { const accountConfig = getAccountConfig(accountId); + + if (!accountConfig) { + return; + } + // TODO[JOE]: Update this i18n key const i18nKey = 'lib.serverless'; const { authType } = accountConfig; @@ -57,14 +69,15 @@ async function verifyAccessKeyAndUserAccess(accountId, scopeGroup) { let scopesData; try { - scopesData = await fetchScopeData(accountId, scopeGroup); + const resp = await fetchScopeData(accountId, scopeGroup); + scopesData = resp.data; } catch (e) { logger.debug( i18n(`${i18nKey}.verifyAccessKeyAndUserAccess.fetchScopeDataError`, { scopeGroup, - error: e, }) ); + logger.debug(e); return; } const { portalScopesInGroup, userScopesInGroup } = scopesData; @@ -87,14 +100,14 @@ async function verifyAccessKeyAndUserAccess(accountId, scopeGroup) { } } -const tailLogs = async ({ - accountId, - compact, - fetchLatest, - tailCall, - name, -}) => { - let initialAfter; +export async function tailLogs( + accountId: number, + name: string, + fetchLatest: () => HubSpotPromise, + tailCall: (after?: string) => HubSpotPromise, + compact = false +): Promise { + let initialAfter = ''; try { const { data: latestLog } = await fetchLatest(); @@ -113,9 +126,9 @@ const tailLogs = async ({ } } - const tail = async after => { - let latestLog; - let nextAfter; + async function tail(after?: string): Promise { + let latestLog: GetFunctionLogsResponse; + let nextAfter: string; try { const { data } = await tailCall(after); latestLog = data; @@ -141,7 +154,7 @@ const tailLogs = async ({ setTimeout(async () => { await tail(nextAfter); }, TAIL_DELAY); - }; + } SpinniesManager.init(); @@ -156,14 +169,14 @@ const tailLogs = async ({ handleUserInput(); await tail(initialAfter); -}; +} -const outputBuildLog = async buildLogUrl => { +export async function outputBuildLog(buildLogUrl: string): Promise { if (!buildLogUrl) { logger.debug( 'Unable to display build output. No build log URL was provided.' ); - return; + return ''; } return new Promise(resolve => { @@ -191,9 +204,4 @@ const outputBuildLog = async buildLogUrl => { resolve(''); } }); -}; - -module.exports = { - outputBuildLog, - tailLogs, -}; +} diff --git a/lib/ui/serverlessFunctionLogs.ts b/lib/ui/serverlessFunctionLogs.ts index 66108eab3..f75a56a16 100644 --- a/lib/ui/serverlessFunctionLogs.ts +++ b/lib/ui/serverlessFunctionLogs.ts @@ -2,11 +2,15 @@ import moment from 'moment'; import chalk from 'chalk'; import { logger, Styles } from '@hubspot/local-dev-lib/logger'; import { i18n } from '../lang'; +import { + FunctionLog, + GetFunctionLogsResponse, +} from '@hubspot/local-dev-lib/types/Functions'; const i18nKey = 'lib.ui.serverlessFunctionLogs'; const SEPARATOR = ' - '; -const LOG_STATUS_COLORS = { +const LOG_STATUS_COLORS: { [key: string]: (status: string) => string } = { SUCCESS: Styles.success, ERROR: Styles.error, UNHANDLED_ERROR: Styles.error, @@ -15,22 +19,6 @@ const LOG_STATUS_COLORS = { type LogStatus = keyof typeof LOG_STATUS_COLORS; -type Log = { - status: LogStatus; - createdAt: string; - executionTime: number; - log?: string; - error?: { - type: string; - message: string; - stackTrace?: string[]; - }; -}; - -type LogsResponse = { - results?: Log[]; -}; - type Options = { compact?: boolean; insertions?: { @@ -38,22 +26,22 @@ type Options = { }; }; -function errorHandler(log: Log, options: Options): string { +function errorHandler(log: FunctionLog, options: Options): string { return `${formatLogHeader(log, options)}${formatError(log, options)}`; } const logHandler: { - [key in LogStatus]: (log: Log, options: Options) => string; + [key in LogStatus]: (log: FunctionLog, options: Options) => string; } = { ERROR: errorHandler, UNHANDLED_ERROR: errorHandler, HANDLED_ERROR: errorHandler, - SUCCESS: (log: Log, options: Options): string => { + SUCCESS: (log: FunctionLog, options: Options): string => { return `${formatLogHeader(log, options)}${formatSuccess(log, options)}`; }, }; -function formatSuccess(log: Log, options: Options): string { +function formatSuccess(log: FunctionLog, options: Options): string { if (!log.log || options.compact) { return ''; } @@ -61,7 +49,7 @@ function formatSuccess(log: Log, options: Options): string { return `\n${log.log}`; } -function formatError(log: Log, options: Options): string { +function formatError(log: FunctionLog, options: Options): string { if (!log.error || options.compact) { return ''; } @@ -69,7 +57,7 @@ function formatError(log: Log, options: Options): string { return `${log.error.type}: ${log.error.message}\n${formatStackTrace(log)}`; } -function formatLogHeader(log: Log, options: Options): string { +function formatLogHeader(log: FunctionLog, options: Options): string { const color = LOG_STATUS_COLORS[log.status]; const headerInsertion = options && options.insertions && options.insertions.header; @@ -79,7 +67,7 @@ function formatLogHeader(log: Log, options: Options): string { }${SEPARATOR}${formatExecutionTime(log)}`; } -function formatStackTrace(log: Log): string { +function formatStackTrace(log: FunctionLog): string { const stackTrace = log.error?.stackTrace || []; return stackTrace .map(trace => { @@ -88,15 +76,15 @@ function formatStackTrace(log: Log): string { .join(''); } -function formatTimestamp(log: Log): string { +function formatTimestamp(log: FunctionLog): string { return `${chalk.whiteBright(moment(log.createdAt).toISOString())}`; } -function formatExecutionTime(log: Log): string { +function formatExecutionTime(log: FunctionLog): string { return `${chalk.whiteBright('Execution Time:')} ${log.executionTime}ms`; } -function processLog(log: Log, options: Options): string | void { +function processLog(log: FunctionLog, options: Options): string | void { try { return logHandler[log.status](log, options); } catch (e) { @@ -109,8 +97,8 @@ function processLog(log: Log, options: Options): string | void { } function isLogsResponse( - logsResp: LogsResponse | Log -): logsResp is LogsResponse { + logsResp: GetFunctionLogsResponse | FunctionLog +): logsResp is GetFunctionLogsResponse { return ( logsResp && 'results' in logsResp && @@ -120,7 +108,7 @@ function isLogsResponse( } function processLogs( - logsResp: LogsResponse | Log, + logsResp: GetFunctionLogsResponse | FunctionLog, options: Options ): string | void { const isLogsResp = isLogsResponse(logsResp); @@ -134,13 +122,12 @@ function processLogs( }) .join('\n'); } - return processLog(logsResp as Log, options); + return processLog(logsResp, options); } -function outputLogs(logsResp: LogsResponse | Log, options: Options): void { +export function outputLogs( + logsResp: GetFunctionLogsResponse | FunctionLog, + options: Options +): void { logger.log(processLogs(logsResp, options)); } - -module.exports = { - outputLogs, -}; diff --git a/package.json b/package.json index d27048d5a..ab7d82683 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "repository": "https://github.com/HubSpot/hubspot-cli", "dependencies": { - "@hubspot/local-dev-lib": "3.1.1", + "@hubspot/local-dev-lib": "3.1.2", "@hubspot/serverless-dev-runtime": "7.0.1", "@hubspot/theme-preview-dev-server": "0.0.10", "@hubspot/ui-extensions-dev-server": "0.8.40", diff --git a/yarn.lock b/yarn.lock index 065849c51..564d14de1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1446,10 +1446,10 @@ semver "^6.3.0" unixify "^1.0.0" -"@hubspot/local-dev-lib@3.1.1": - version "3.1.1" - resolved "https://registry.yarnpkg.com/@hubspot/local-dev-lib/-/local-dev-lib-3.1.1.tgz#b67646d7a7b399cebc5d74f62c389c13554e950c" - integrity sha512-/SIKBuC3ORkKMXityS6tCz2sNU7OY96slJtLM0CxRlKDCiwEGb08Lf0Ruf1PMBJnAu8iHbw/6SprefFEHEQOpw== +"@hubspot/local-dev-lib@3.1.2": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@hubspot/local-dev-lib/-/local-dev-lib-3.1.2.tgz#9af9f8ca5cf9aba912cb9c5f9acf51e760604465" + integrity sha512-vn9pyKs/2NW86GWhTlp9VyDcpKUUUEWQgHwQpq57Luz9IWwCFAmFaTzSggmttTnqJRAmqwMIN8DHUKeMEJqquA== dependencies: address "^2.0.1" axios "^1.3.5" From 40afea24034986bb32eaf23c63450b2974525ffe Mon Sep 17 00:00:00 2001 From: Jessica Sines Date: Thu, 16 Jan 2025 11:40:41 -0500 Subject: [PATCH 11/11] Adjust `hs upload --clean` warning prompt language (#1344) --- lang/en.lyaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lang/en.lyaml b/lang/en.lyaml index db54c51ef..8ff856c77 100644 --- a/lang/en.lyaml +++ b/lang/en.lyaml @@ -987,7 +987,7 @@ en: uploading: "Uploading files from \"{{ src }}\" to \"{{ dest }}\" in the Design Manager of account {{ accountId }}" notUploaded: "There was an error processing \"{{ src }}\". The file has not been uploaded." cleaning: "Removing \"{{ filePath }}\" from account {{ accountId }} and uploading local..." - confirmCleanUpload: "You are about to remove any remote files in \"{{ filePath }}\" on HubSpot account {{ accountId }} that don't exist locally. Are you sure you want to do this?" + confirmCleanUpload: "You are about to delete the directory \"{{ filePath }}\" and its contents on HubSpot account {{ accountId }} before uploading. This will also clear the global content associated with any global partial templates and modules. Are you sure you want to do this?" watch: describe: "Watch a directory on your computer for changes and upload the changed files to the HubSpot CMS." errors: