From 680d1553125ac26aa7e00c7c72942f85a50f6a74 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 10 Jan 2025 11:14:15 -0500 Subject: [PATCH 01/14] Convert lib/sandboxes to TS --- commands/sandbox/create.ts | 10 +- lang/en.lyaml | 2 + lib/buildAccount.ts | 9 +- lib/sandboxes.ts | 213 ++++++++++++++++--------------------- 4 files changed, 105 insertions(+), 129 deletions(-) diff --git a/commands/sandbox/create.ts b/commands/sandbox/create.ts index b516e307d..d277adacd 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 @@ -156,7 +156,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 3ce2a43d3..a921cd2e7 100644 --- a/lang/en.lyaml +++ b/lang/en.lyaml @@ -1418,6 +1418,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. @@ -1477,6 +1478,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..40d4032d0 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, accountConfig, name, accountId); } if (isDeveloperTestAccount) { handleDeveloperTestAccountCreateError(err, env, accountId, portalLimit); diff --git a/lib/sandboxes.ts b/lib/sandboxes.ts index 2f0599870..c05d97bfb 100644 --- a/lib/sandboxes.ts +++ b/lib/sandboxes.ts @@ -1,59 +1,59 @@ -// @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 { +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, 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 { +} 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 { getValidEnv } from '@hubspot/local-dev-lib/environment'; +import { AccountType, CLIAccount } from '@hubspot/local-dev-lib/types/Accounts'; -const syncTypes = { +import { i18n } from './lang'; +import { uiAccountDescription } from './ui'; +import { logError } from './errorHandlers/index'; +import { Environment } from '@hubspot/local-dev-lib/types/Config'; + +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 +68,63 @@ function getHasSandboxesByType(parentAccountConfig, type) { return false; } -function getSandboxLimit(error) { +class SandboxLimitError { + context?: { + limit?: string[]; + }; +} + +function getSandboxLimit(error: unknown): number { // 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 + + if (error instanceof SandboxLimitError) { + const limit = + error.context && error.context.limit && error.context.limit[0]; + return limit ? parseInt(limit, 10) : 1; // Default to 1 + } + return 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(`${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(`${i18nKey}.create.failure.usageLimitFetch`); } if (sandboxType === HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX) { if (usage['DEVELOPER'].available === 0) { @@ -153,7 +137,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 +150,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 +174,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 +187,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 +200,25 @@ const validateSandboxUsageLimits = async (accountConfig, sandboxType, env) => { } } } -}; +} -function handleSandboxCreateError({ - err, - env, - accountId, - name, - accountConfig, -}) { +export function handleSandboxCreateError( + err: unknown, + env: Environment, + accountConfig: CLIAccount, + 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 +232,7 @@ function handleSandboxCreateError({ ) { logger.log(''); logger.error( - i18n('lib.sandboxes.create.failure.invalidUser', { + i18n(`${i18nKey}.create.failure.invalidUser`, { accountName: name, parentAccountName: uiAccountDescription(accountId), }) @@ -263,7 +247,7 @@ function handleSandboxCreateError({ ) { logger.log(''); logger.error( - i18n('lib.sandboxes.create.failure.403Gating', { + i18n(`${i18nKey}.create.failure.403Gating`, { accountName: name, parentAccountName: uiAccountDescription(accountId), accountId, @@ -277,8 +261,8 @@ function handleSandboxCreateError({ subCategory: 'SandboxErrors.NUM_DEVELOPMENT_SANDBOXES_LIMIT_EXCEEDED_ERROR', }) && - err.error && - err.error.message + 'error' in err && + err.error instanceof Error ) { logger.log(''); const devSandboxLimit = getSandboxLimit(err.error); @@ -290,7 +274,7 @@ function handleSandboxCreateError({ if (hasDevelopmentSandboxes) { logger.error( i18n( - `lib.sandboxes.create.failure.alreadyInConfig.developer.${ + `${i18nKey}.create.failure.alreadyInConfig.developer.${ plural ? 'other' : 'one' }`, { @@ -303,7 +287,7 @@ function handleSandboxCreateError({ const baseUrl = getHubSpotWebsiteOrigin(getValidEnv(getEnv(accountId))); logger.error( i18n( - `lib.sandboxes.create.failure.limit.developer.${ + `${i18nKey}.create.failure.limit.developer.${ plural ? 'other' : 'one' }`, { @@ -321,8 +305,8 @@ function handleSandboxCreateError({ category: 'VALIDATION_ERROR', subCategory: 'SandboxErrors.NUM_STANDARD_SANDBOXES_LIMIT_EXCEEDED_ERROR', }) && - err.error && - err.error.message + 'error' in err && + err.error instanceof Error ) { logger.log(''); const standardSandboxLimit = getSandboxLimit(err.error); @@ -334,7 +318,7 @@ function handleSandboxCreateError({ if (hasStandardSandboxes) { logger.error( i18n( - `lib.sandboxes.create.failure.alreadyInConfig.standard.${ + `${i18nKey}.create.failure.alreadyInConfig.standard.${ plural ? 'other' : 'one' }`, { @@ -347,7 +331,7 @@ function handleSandboxCreateError({ const baseUrl = getHubSpotWebsiteOrigin(getValidEnv(getEnv(accountId))); logger.error( i18n( - `lib.sandboxes.create.failure.limit.standard.${ + `${i18nKey}.create.failure.limit.standard.${ plural ? 'other' : 'one' }`, { @@ -364,16 +348,3 @@ function handleSandboxCreateError({ } throw err; } - -module.exports = { - sandboxTypeMap, - sandboxApiTypeMap, - syncTypes, - getSandboxTypeAsString, - getHasSandboxesByType, - getSandboxLimit, - validateSandboxUsageLimits, - getAvailableSyncTypes, - getSyncTypesWithContactRecordsPrompt, - handleSandboxCreateError, -}; From 1281aa76f9cc9e45b898adef59d4139c4e3c9e97 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 10 Jan 2025 11:44:13 -0500 Subject: [PATCH 02/14] Convert sandbox sync to TS --- commands/sandbox/create.ts | 7 +--- lib/localDev.ts | 10 ++--- lib/sandboxSync.ts | 76 ++++++++++++++++++-------------------- lib/sandboxes.ts | 7 ++-- types/Sandboxes.ts | 3 ++ 5 files changed, 49 insertions(+), 54 deletions(-) create mode 100644 types/Sandboxes.ts diff --git a/commands/sandbox/create.ts b/commands/sandbox/create.ts index d277adacd..33dbb1314 100644 --- a/commands/sandbox/create.ts +++ b/commands/sandbox/create.ts @@ -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( 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 c05d97bfb..229362fd2 100644 --- a/lib/sandboxes.ts +++ b/lib/sandboxes.ts @@ -15,11 +15,12 @@ import { } from '@hubspot/local-dev-lib/errors/index'; import { getValidEnv } from '@hubspot/local-dev-lib/environment'; import { AccountType, CLIAccount } from '@hubspot/local-dev-lib/types/Accounts'; +import { Environment } from '@hubspot/local-dev-lib/types/Config'; import { i18n } from './lang'; import { uiAccountDescription } from './ui'; import { logError } from './errorHandlers/index'; -import { Environment } from '@hubspot/local-dev-lib/types/Config'; +import { SandboxSyncTask } from '../types/Sandboxes'; const i18nKey = 'lib.sandbox'; @@ -39,7 +40,7 @@ export const SANDBOX_API_TYPE_MAP = { [HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX]: 2, } as const; -export function getSandboxTypeAsString(accountType: AccountType): string { +export function getSandboxTypeAsString(accountType?: AccountType): string { if (accountType === HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX) { return 'development'; // Only place we're using this specific name } @@ -89,7 +90,7 @@ function getSandboxLimit(error: unknown): number { export async function getAvailableSyncTypes( parentAccountConfig: CLIAccount, config: CLIAccount -): Promise> { +): Promise> { const parentId = getAccountIdentifier(parentAccountConfig); const parentPortalId = getAccountId(parentId); const id = getAccountIdentifier(config); 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 3cbb2c06ba6dcabe00cfd027a7e100e99096744e Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 10 Jan 2025 14:18:21 -0500 Subject: [PATCH 03/14] Remove unreachable sandbox error code --- commands/sandbox/create.ts | 1 + lib/buildAccount.ts | 2 +- lib/sandboxes.ts | 114 +------------------------------------ 3 files changed, 3 insertions(+), 114 deletions(-) diff --git a/commands/sandbox/create.ts b/commands/sandbox/create.ts index 33dbb1314..18e304a68 100644 --- a/commands/sandbox/create.ts +++ b/commands/sandbox/create.ts @@ -79,6 +79,7 @@ exports.handler = async options => { // Check usage limits and exit if parent portal has no available sandboxes for the selected type try { + console.log('validate usage limit'); await validateSandboxUsageLimits(accountConfig, sandboxType, env); } catch (err) { if (isMissingScopeError(err)) { diff --git a/lib/buildAccount.ts b/lib/buildAccount.ts index 40d4032d0..e6e236678 100644 --- a/lib/buildAccount.ts +++ b/lib/buildAccount.ts @@ -162,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/sandboxes.ts b/lib/sandboxes.ts index 229362fd2..458e1adbc 100644 --- a/lib/sandboxes.ts +++ b/lib/sandboxes.ts @@ -1,11 +1,7 @@ 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, - getEnv, - getConfigAccounts, -} from '@hubspot/local-dev-lib/config'; +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'; @@ -13,7 +9,6 @@ import { isMissingScopeError, isSpecifiedError, } from '@hubspot/local-dev-lib/errors/index'; -import { getValidEnv } from '@hubspot/local-dev-lib/environment'; import { AccountType, CLIAccount } from '@hubspot/local-dev-lib/types/Accounts'; import { Environment } from '@hubspot/local-dev-lib/types/Config'; @@ -69,23 +64,6 @@ function getHasSandboxesByType( return false; } -class SandboxLimitError { - context?: { - limit?: string[]; - }; -} - -function getSandboxLimit(error: unknown): number { - // Error context should contain a limit property with a list of one number. That number is the current limit - - if (error instanceof SandboxLimitError) { - const limit = - error.context && error.context.limit && error.context.limit[0]; - return limit ? parseInt(limit, 10) : 1; // Default to 1 - } - return 1; -} - // Fetches available sync types for a given sandbox portal export async function getAvailableSyncTypes( parentAccountConfig: CLIAccount, @@ -206,7 +184,6 @@ export async function validateSandboxUsageLimits( export function handleSandboxCreateError( err: unknown, env: Environment, - accountConfig: CLIAccount, name: string, accountId: number ) { @@ -255,95 +232,6 @@ export function handleSandboxCreateError( }) ); logger.log(''); - } else if ( - isSpecifiedError(err, { - statusCode: 400, - category: 'VALIDATION_ERROR', - subCategory: - 'SandboxErrors.NUM_DEVELOPMENT_SANDBOXES_LIMIT_EXCEEDED_ERROR', - }) && - 'error' in err && - err.error instanceof Error - ) { - 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( - `${i18nKey}.create.failure.alreadyInConfig.developer.${ - plural ? 'other' : 'one' - }`, - { - accountName: uiAccountDescription(accountId), - limit: devSandboxLimit, - } - ) - ); - } else { - const baseUrl = getHubSpotWebsiteOrigin(getValidEnv(getEnv(accountId))); - logger.error( - i18n( - `${i18nKey}.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', - }) && - 'error' in err && - err.error instanceof Error - ) { - 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( - `${i18nKey}.create.failure.alreadyInConfig.standard.${ - plural ? 'other' : 'one' - }`, - { - accountName: uiAccountDescription(accountId), - limit: standardSandboxLimit, - } - ) - ); - } else { - const baseUrl = getHubSpotWebsiteOrigin(getValidEnv(getEnv(accountId))); - logger.error( - i18n( - `${i18nKey}.create.failure.limit.standard.${ - plural ? 'other' : 'one' - }`, - { - accountName: uiAccountDescription(accountId), - limit: standardSandboxLimit, - link: `${baseUrl}/sandboxes-developer/${accountId}/standard`, - } - ) - ); - } - logger.log(''); } else { logError(err); } From 35a2ca07ef039e130cf670c7d125f255f885c1b8 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 10 Jan 2025 15:44:30 -0500 Subject: [PATCH 04/14] Serverless logs WIP --- lang/en.lyaml | 2 +- lib/serverlessLogs.ts | 90 +++++++++++++++++--------------- lib/ui/serverlessFunctionLogs.ts | 9 ++-- 3 files changed, 52 insertions(+), 49 deletions(-) diff --git a/lang/en.lyaml b/lang/en.lyaml index 0b4f5c7e4..8634c9de2 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/serverlessLogs.ts b/lib/serverlessLogs.ts index c716d7ece..57ef754be 100644 --- a/lib/serverlessLogs.ts +++ b/lib/serverlessLogs.ts @@ -1,37 +1,37 @@ -// @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'; 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 +44,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 +65,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 +96,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<{ id: string }>, + tailCall: (after: string) => HubSpotPromise, + compact = false +) { + let initialAfter: string; try { const { data: latestLog } = await fetchLatest(); @@ -113,7 +122,7 @@ const tailLogs = async ({ } } - const tail = async after => { + async function tail(after: string) { let latestLog; let nextAfter; try { @@ -141,7 +150,7 @@ const tailLogs = async ({ setTimeout(async () => { await tail(nextAfter); }, TAIL_DELAY); - }; + } SpinniesManager.init(); @@ -156,14 +165,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 +200,4 @@ const outputBuildLog = async buildLogUrl => { resolve(''); } }); -}; - -module.exports = { - outputBuildLog, - tailLogs, -}; +} diff --git a/lib/ui/serverlessFunctionLogs.ts b/lib/ui/serverlessFunctionLogs.ts index 66108eab3..39dd60383 100644 --- a/lib/ui/serverlessFunctionLogs.ts +++ b/lib/ui/serverlessFunctionLogs.ts @@ -137,10 +137,9 @@ function processLogs( return processLog(logsResp as Log, options); } -function outputLogs(logsResp: LogsResponse | Log, options: Options): void { +export function outputLogs( + logsResp: LogsResponse | Log, + options: Options +): void { logger.log(processLogs(logsResp, options)); } - -module.exports = { - outputLogs, -}; From b04e77c7ce2048ba714ea01a1acb40b101191856 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 10 Jan 2025 16:46:52 -0500 Subject: [PATCH 05/14] Convert lib/serverlessLogs to TS --- lib/serverlessLogs.ts | 22 +++++++++----- lib/ui/serverlessFunctionLogs.ts | 52 ++++++++++++-------------------- package.json | 2 +- yarn.lock | 8 ++--- 4 files changed, 39 insertions(+), 45 deletions(-) diff --git a/lib/serverlessLogs.ts b/lib/serverlessLogs.ts index 57ef754be..6b0e05efa 100644 --- a/lib/serverlessLogs.ts +++ b/lib/serverlessLogs.ts @@ -19,6 +19,10 @@ 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; @@ -99,11 +103,11 @@ async function verifyAccessKeyAndUserAccess( export async function tailLogs( accountId: number, name: string, - fetchLatest: () => HubSpotPromise<{ id: string }>, - tailCall: (after: string) => HubSpotPromise, + fetchLatest: () => HubSpotPromise, + tailCall: (after: string) => HubSpotPromise, compact = false -) { - let initialAfter: string; +): Promise { + let initialAfter = ''; try { const { data: latestLog } = await fetchLatest(); @@ -122,9 +126,9 @@ export async function tailLogs( } } - async function tail(after: string) { - let latestLog; - let nextAfter; + async function tail(after: string): Promise { + let latestLog: GetFunctionLogsResponse; + let nextAfter: string; try { const { data } = await tailCall(after); latestLog = data; @@ -164,7 +168,9 @@ export async function tailLogs( handleUserInput(); - await tail(initialAfter); + if (initialAfter) { + await tail(initialAfter); + } } export async function outputBuildLog(buildLogUrl: string): Promise { diff --git a/lib/ui/serverlessFunctionLogs.ts b/lib/ui/serverlessFunctionLogs.ts index 39dd60383..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,11 +122,11 @@ function processLogs( }) .join('\n'); } - return processLog(logsResp as Log, options); + return processLog(logsResp, options); } export function outputLogs( - logsResp: LogsResponse | Log, + logsResp: GetFunctionLogsResponse | FunctionLog, options: Options ): void { logger.log(processLogs(logsResp, options)); diff --git a/package.json b/package.json index cbe773169..c26ebfbe4 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 8add9d76b74b318ce7e304de6ad7fb2699bd5477 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Mon, 13 Jan 2025 13:02:54 -0500 Subject: [PATCH 06/14] wip --- lib/buildAccount.ts | 121 +++++++++++++------------ lib/localDev.ts | 10 +- lib/prompts/personalAccessKeyPrompt.ts | 2 +- 3 files changed, 69 insertions(+), 64 deletions(-) diff --git a/lib/buildAccount.ts b/lib/buildAccount.ts index e6e236678..580249d93 100644 --- a/lib/buildAccount.ts +++ b/lib/buildAccount.ts @@ -1,47 +1,37 @@ -// @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'; + +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 +47,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 +73,8 @@ async function saveAccountToConfig({ updateAccountConfig({ ...updatedConfig, - environment: updatedConfig.env, - tokenInfo: updatedConfig.auth.tokenInfo, + env: updatedConfig?.env, + tokenInfo: updatedConfig?.auth?.tokenInfo, name: validName, }); writeConfig(); @@ -93,14 +83,28 @@ async function saveAccountToConfig({ return validName; } -async function buildNewAccount({ +type ValidBuildAccountType = + | typeof HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX + | typeof HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX + | typeof HUBSPOT_ACCOUNT_TYPES.DEVELOPER_TEST; + +type BuildNewAccountOptions = { + name: string; + accountType: ValidBuildAccountType; + accountConfig: CLIAccount; + env: Environment; + portalLimit?: number; + force?: boolean; +}; + +export async function buildNewAccount({ name, accountType, accountConfig, env, portalLimit, // Used only for developer test accounts force = false, -}) { +}: BuildNewAccountOptions) { SpinniesManager.init({ succeedColor: 'white', }); @@ -112,8 +116,12 @@ async function buildNewAccount({ const isDeveloperTestAccount = accountType === HUBSPOT_ACCOUNT_TYPES.DEVELOPER_TEST; + if ((!isSandbox && !isDeveloperTestAccount) || !accountId) { + return; + } + let result; - let spinniesI18nKey; + let spinniesI18nKey: string; if (isSandbox) { if (accountType === HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX) { spinniesI18nKey = 'lib.sandbox.create.loading.standard'; @@ -121,7 +129,7 @@ async function buildNewAccount({ if (accountType === HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX) { spinniesI18nKey = 'lib.sandbox.create.loading.developer'; } - } else if (isDeveloperTestAccount) { + } else { spinniesI18nKey = 'lib.developerTestAccount.create.loading'; } @@ -133,6 +141,7 @@ async function buildNewAccount({ }); let resultAccountId; + let resultPersonalAccessKey; try { if (isSandbox) { const sandboxApiType = SANDBOX_API_TYPE_MAP[accountType]; // API expects sandbox type as 1 or 2. @@ -140,6 +149,7 @@ async function buildNewAccount({ const { data } = await createSandbox(accountId, name, sandboxApiType); result = { name, ...data }; resultAccountId = result.sandbox.sandboxHubId; + resultPersonalAccessKey = result.personalAccessKey; } else if (isDeveloperTestAccount) { const { data } = await createDeveloperTestAccount(accountId, name); result = data; @@ -169,17 +179,17 @@ async function buildNewAccount({ } } - let configAccountName; + let configAccountName: string; try { // Response contains PAK, save to config here - configAccountName = await saveAccountToConfig({ + configAccountName = await saveAccountToConfig( + resultAccountId, + name, env, - personalAccessKey: result.personalAccessKey, - accountName: name, - accountId: resultAccountId, - force, - }); + resultPersonalAccessKey, + force + ); } catch (err) { logError(err); throw err; @@ -190,8 +200,3 @@ async function buildNewAccount({ result, }; } - -module.exports = { - buildNewAccount, - saveAccountToConfig, -}; diff --git a/lib/localDev.ts b/lib/localDev.ts index 20d5b02b9..c4c2d348e 100644 --- a/lib/localDev.ts +++ b/lib/localDev.ts @@ -319,11 +319,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`; From b4001cb2a24d66cc54afa67873b57eaad5550ce4 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Mon, 13 Jan 2025 14:15:41 -0500 Subject: [PATCH 07/14] Convert lib/buildAccount to TS --- commands/sandbox/create.ts | 12 +-- lib/buildAccount.ts | 185 +++++++++++++++++++++---------------- lib/localDev.ts | 21 +++-- lib/sandboxes.ts | 2 +- 4 files changed, 125 insertions(+), 95 deletions(-) diff --git a/commands/sandbox/create.ts b/commands/sandbox/create.ts index 18e304a68..c44eb45fd 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'); @@ -131,13 +131,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/lib/buildAccount.ts b/lib/buildAccount.ts index 580249d93..91f421bea 100644 --- a/lib/buildAccount.ts +++ b/lib/buildAccount.ts @@ -24,6 +24,8 @@ 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, @@ -83,111 +85,139 @@ export async function saveAccountToConfig( return validName; } -type ValidBuildAccountType = - | typeof HUBSPOT_ACCOUNT_TYPES.STANDARD_SANDBOX - | typeof HUBSPOT_ACCOUNT_TYPES.DEVELOPMENT_SANDBOX - | typeof HUBSPOT_ACCOUNT_TYPES.DEVELOPER_TEST; +export async function buildDeveloperTestAccount( + name: string, + accountConfig: CLIAccount, + env: Environment, + portalLimit: number +): Promise { + const i18nKey = 'lib.developerTestAccount.create.loading'; -type BuildNewAccountOptions = { - name: string; - accountType: ValidBuildAccountType; - accountConfig: CLIAccount; - env: Environment; - portalLimit?: number; - force?: boolean; -}; + const id = getAccountIdentifier(accountConfig); + const accountId = getAccountId(id); + + if (!accountId) { + throw new Error(i18n(`${i18nKey}.fail`)); + } -export async function buildNewAccount({ - name, - accountType, - accountConfig, - env, - portalLimit, // Used only for developer test accounts - force = false, -}: BuildNewAccountOptions) { SpinniesManager.init({ succeedColor: 'white', }); - 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; - - if ((!isSandbox && !isDeveloperTestAccount) || !accountId) { - return; + + 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); } - let result; - let spinniesI18nKey: string; - 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'; - } + 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 { - spinniesI18nKey = 'lib.developerTestAccount.create.loading'; + i18nKey = 'lib.sandbox.create.loading.developer'; + } + + const id = getAccountIdentifier(accountConfig); + const accountId = getAccountId(id); + + 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 resultPersonalAccessKey; + 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; - resultPersonalAccessKey = result.personalAccessKey; - } else if (isDeveloperTestAccount) { - const { data } = await createDeveloperTestAccount(accountId, name); - result = data; - resultAccountId = result.id; - } + const sandboxApiType = SANDBOX_API_TYPE_MAP[sandboxType]; - SpinniesManager.succeed('buildNewAccount', { - text: i18n(`${spinniesI18nKey}.succeed`, { + const { data } = await createSandbox(accountId, name, sandboxApiType); + sandbox = { name, ...data }; + + 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: string; - try { - // Response contains PAK, save to config here - configAccountName = await saveAccountToConfig( - resultAccountId, + await saveAccountToConfig( + accountId, name, env, - resultPersonalAccessKey, + sandbox.personalAccessKey, force ); } catch (err) { @@ -195,8 +225,5 @@ export async function buildNewAccount({ throw err; } - return { - configAccountName, - result, - }; + return sandbox; } diff --git a/lib/localDev.ts b/lib/localDev.ts index c4c2d348e..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) { diff --git a/lib/sandboxes.ts b/lib/sandboxes.ts index 458e1adbc..d7712201a 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`, { From c8db8cee01c67b642c5691cf156082d1a21c0492 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Mon, 13 Jan 2025 14:51:25 -0500 Subject: [PATCH 08/14] Fix unit tests --- commands/logs.ts | 8 +---- lib/__tests__/serverlessLogs.test.ts | 48 ++++++++-------------------- lib/serverlessLogs.ts | 8 ++--- 3 files changed, 17 insertions(+), 47 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/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/serverlessLogs.ts b/lib/serverlessLogs.ts index 6b0e05efa..476d4f4de 100644 --- a/lib/serverlessLogs.ts +++ b/lib/serverlessLogs.ts @@ -104,7 +104,7 @@ export async function tailLogs( accountId: number, name: string, fetchLatest: () => HubSpotPromise, - tailCall: (after: string) => HubSpotPromise, + tailCall: (after?: string) => HubSpotPromise, compact = false ): Promise { let initialAfter = ''; @@ -126,7 +126,7 @@ export async function tailLogs( } } - async function tail(after: string): Promise { + async function tail(after?: string): Promise { let latestLog: GetFunctionLogsResponse; let nextAfter: string; try { @@ -168,9 +168,7 @@ export async function tailLogs( handleUserInput(); - if (initialAfter) { - await tail(initialAfter); - } + await tail(initialAfter); } export async function outputBuildLog(buildLogUrl: string): Promise { From 6352c292959c4ffdfa1487118125508c91e1fe10 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Mon, 13 Jan 2025 15:03:03 -0500 Subject: [PATCH 09/14] rm console log --- commands/sandbox/create.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/commands/sandbox/create.ts b/commands/sandbox/create.ts index c44eb45fd..d05ef2b77 100644 --- a/commands/sandbox/create.ts +++ b/commands/sandbox/create.ts @@ -79,7 +79,6 @@ exports.handler = async options => { // Check usage limits and exit if parent portal has no available sandboxes for the selected type try { - console.log('validate usage limit'); await validateSandboxUsageLimits(accountConfig, sandboxType, env); } catch (err) { if (isMissingScopeError(err)) { From e08ee6d9c1194a5d70b6ae4029881ec8d2ab767a Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Tue, 14 Jan 2025 13:14:28 -0500 Subject: [PATCH 10/14] convert lib/localDev to TS --- commands/project/upload.ts | 8 +- commands/project/watch.ts | 8 +- lib/localDev.ts | 264 +++++++++++-------- lib/projects/buildAndDeploy.ts | 8 +- lib/projects/upload.ts | 19 +- lib/prompts/projectDevTargetAccountPrompt.ts | 4 +- types/Projects.ts | 7 + 7 files changed, 177 insertions(+), 141 deletions(-) diff --git a/commands/project/upload.ts b/commands/project/upload.ts index 343e93d52..0e6432d09 100644 --- a/commands/project/upload.ts +++ b/commands/project/upload.ts @@ -48,7 +48,7 @@ exports.handler = async options => { }); try { - const result = await handleProjectUpload( + const { result, uploadError } = await handleProjectUpload( derivedAccountId, projectConfig, projectDir, @@ -56,9 +56,9 @@ exports.handler = async options => { message ); - if (result.uploadError) { + if (uploadError) { if ( - isSpecifiedError(result.uploadError, { + isSpecifiedError(uploadError, { subCategory: PROJECT_ERROR_TYPES.PROJECT_LOCKED, }) ) { @@ -67,7 +67,7 @@ exports.handler = async options => { logger.log(); } else { logError( - result.uploadError, + uploadError, new ApiErrorContext({ accountId: derivedAccountId, request: 'project upload', diff --git a/commands/project/watch.ts b/commands/project/watch.ts index ceac52615..db969c6fd 100644 --- a/commands/project/watch.ts +++ b/commands/project/watch.ts @@ -113,16 +113,16 @@ exports.handler = async options => { // Upload all files if no build exists for this project yet if (initialUpload || hasNoBuilds) { - const result = await handleProjectUpload( + const { uploadError } = await handleProjectUpload( derivedAccountId, projectConfig, projectDir, startWatching ); - if (result.uploadError) { + if (uploadError) { if ( - isSpecifiedError(result.uploadError, { + isSpecifiedError(uploadError, { subCategory: PROJECT_ERROR_TYPES.PROJECT_LOCKED, }) ) { @@ -131,7 +131,7 @@ exports.handler = async options => { logger.log(); } else { logError( - result.uploadError, + uploadError, new ApiErrorContext({ accountId: derivedAccountId, request: 'project upload', diff --git a/lib/localDev.ts b/lib/localDev.ts index 886e185ca..e8e0130b4 100644 --- a/lib/localDev.ts +++ b/lib/localDev.ts @@ -1,66 +1,72 @@ -// @ts-nocheck -const { logger } = require('@hubspot/local-dev-lib/logger'); -const { +import { logger } from '@hubspot/local-dev-lib/logger'; +import { HUBSPOT_ACCOUNT_TYPES, HUBSPOT_ACCOUNT_TYPE_STRINGS, -} = require('@hubspot/local-dev-lib/constants/config'); -const { +} from '@hubspot/local-dev-lib/constants/config'; +import { isMissingScopeError, isSpecifiedError, -} = require('@hubspot/local-dev-lib/errors/index'); -const { getHubSpotWebsiteOrigin } = require('@hubspot/local-dev-lib/urls'); -const { getAccountConfig, getEnv } = require('@hubspot/local-dev-lib/config'); -const { createProject } = require('@hubspot/local-dev-lib/api/projects'); -const { - ENVIRONMENTS, -} = require('@hubspot/local-dev-lib/constants/environments'); -const { +} from '@hubspot/local-dev-lib/errors/index'; +import { getHubSpotWebsiteOrigin } from '@hubspot/local-dev-lib/urls'; +import { getAccountConfig, getEnv } from '@hubspot/local-dev-lib/config'; +import { createProject } from '@hubspot/local-dev-lib/api/projects'; +import { ENVIRONMENTS } from '@hubspot/local-dev-lib/constants/environments'; +import { PERSONAL_ACCESS_KEY_AUTH_METHOD } from '@hubspot/local-dev-lib/constants/auth'; +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 { DeveloperTestAccount } from '@hubspot/local-dev-lib/types/developerTestAccounts'; +import { Project } from '@hubspot/local-dev-lib/types/Project'; + +import { confirmDefaultAccountPrompt, selectSandboxTargetAccountPrompt, selectDeveloperTestTargetAccountPrompt, confirmUseExistingDeveloperTestAccountPrompt, -} = require('./prompts/projectDevTargetAccountPrompt'); -const { confirmPrompt } = require('./prompts/promptUtils'); -const { - validateSandboxUsageLimits, - getAvailableSyncTypes, -} = require('./sandboxes'); -const { syncSandbox } = require('./sandboxSync'); -const { - validateDevTestAccountUsageLimits, -} = require('./developerTestAccounts'); -const { uiCommandReference, uiLine, uiAccountDescription } = require('./ui'); -const SpinniesManager = require('./ui/SpinniesManager'); -const { i18n } = require('./lang'); -const { EXIT_CODES } = require('./enums/exitCodes'); -const { trackCommandMetadataUsage } = require('./usageTracking'); -const { - isAppDeveloperAccount, - isDeveloperTestAccount, -} = require('./accountTypes'); -const { handleProjectUpload } = require('./projects/upload'); -const { pollProjectBuildAndDeploy } = require('./projects/buildAndDeploy'); -const { +} from './prompts/projectDevTargetAccountPrompt'; +import { confirmPrompt } from './prompts/promptUtils'; +import { validateSandboxUsageLimits, getAvailableSyncTypes } from './sandboxes'; +import { syncSandbox } from './sandboxSync'; +import { validateDevTestAccountUsageLimits } from './developerTestAccounts'; +import { uiCommandReference, uiLine, uiAccountDescription } from './ui'; +import SpinniesManager from './ui/SpinniesManager'; +import { i18n } from './lang'; +import { EXIT_CODES } from './enums/exitCodes'; +import { trackCommandMetadataUsage } from './usageTracking'; +import { isAppDeveloperAccount, isDeveloperTestAccount } from './accountTypes'; +import { handleProjectUpload } from './projects/upload'; +import { pollProjectBuildAndDeploy } from './projects/buildAndDeploy'; +import { PROJECT_ERROR_TYPES, PROJECT_BUILD_TEXT, PROJECT_DEPLOY_TEXT, -} = require('./constants'); -const { logError, ApiErrorContext } = require('./errorHandlers/index'); -const { - PERSONAL_ACCESS_KEY_AUTH_METHOD, -} = require('@hubspot/local-dev-lib/constants/auth'); -const { +} from './constants'; +import { logError, ApiErrorContext } from './errorHandlers/index'; +import { buildSandbox, buildDeveloperTestAccount, saveAccountToConfig, -} = require('./buildAccount'); -const { hubspotAccountNamePrompt } = require('./prompts/accountNamePrompt'); +} from './buildAccount'; +import { hubspotAccountNamePrompt } from './prompts/accountNamePrompt'; +import { + ProjectConfig, + ProjectPollResult, + ProjectSubtask, +} from '../types/Projects'; +import { FileResult } from 'tmp'; +import { Build } from '@hubspot/local-dev-lib/types/Build'; const i18nKey = 'lib.localDev'; // If the user passed in the --account flag, confirm they want to use that account as // their target account, otherwise exit -const confirmDefaultAccountIsTarget = async accountConfig => { +export async function confirmDefaultAccountIsTarget( + accountConfig: CLIAccount +): Promise { + if (!accountConfig.name || !accountConfig.accountType) { + throw new Error('TODO'); + } + logger.log(); const useDefaultAccount = await confirmDefaultAccountPrompt( accountConfig.name, @@ -79,10 +85,13 @@ const confirmDefaultAccountIsTarget = async accountConfig => { ); process.exit(EXIT_CODES.SUCCESS); } -}; +} // Confirm the default account is supported for the type of apps being developed -const checkIfDefaultAccountIsSupported = (accountConfig, hasPublicApps) => { +export function checkIfDefaultAccountIsSupported( + accountConfig: CLIAccount, + hasPublicApps: boolean +): void { if ( hasPublicApps && !( @@ -106,14 +115,19 @@ const checkIfDefaultAccountIsSupported = (accountConfig, hasPublicApps) => { ); process.exit(EXIT_CODES.SUCCESS); } -}; +} -const checkIfParentAccountIsAuthed = accountConfig => { - if (!getAccountConfig(accountConfig.parentAccountId)) { +export function checkIfParentAccountIsAuthed(accountConfig: CLIAccount): void { + if ( + !accountConfig.parentAccountId || + !getAccountConfig(accountConfig.parentAccountId) + ) { logger.error( i18n(`${i18nKey}.checkIfParentAccountIsAuthed.notAuthedError`, { - accountId: accountConfig.parentAccountId, - accountIdentifier: uiAccountDescription(accountConfig.portalId), + accountId: accountConfig.parentAccountId || '', + accountIdentifier: uiAccountDescription( + getAccountIdentifier(accountConfig) + ), authCommand: uiCommandReference( `hs auth --account=${accountConfig.parentAccountId}` ), @@ -121,10 +135,13 @@ const checkIfParentAccountIsAuthed = accountConfig => { ); process.exit(EXIT_CODES.SUCCESS); } -}; +} // Confirm the default account is a developer account if developing public apps -const checkIfAccountFlagIsSupported = (accountConfig, hasPublicApps) => { +export function checkIfAccountFlagIsSupported( + accountConfig: CLIAccount, + hasPublicApps: boolean +): void { if (hasPublicApps) { if (!isDeveloperTestAccount(accountConfig)) { logger.error( @@ -144,14 +161,14 @@ const checkIfAccountFlagIsSupported = (accountConfig, hasPublicApps) => { ); process.exit(EXIT_CODES.SUCCESS); } -}; +} // If the user isn't using the recommended account type, prompt them to use or create one -const suggestRecommendedNestedAccount = async ( - accounts, - accountConfig, - hasPublicApps -) => { +export async function suggestRecommendedNestedAccount( + accounts: CLIAccount[], + accountConfig: CLIAccount, + hasPublicApps: boolean +) { logger.log(); uiLine(); if (hasPublicApps) { @@ -170,11 +187,15 @@ const suggestRecommendedNestedAccount = async ( ? selectDeveloperTestTargetAccountPrompt : selectSandboxTargetAccountPrompt; - return targetAccountPrompt(accounts, accountConfig, hasPublicApps); -}; + return targetAccountPrompt(accounts, accountConfig); +} // Create a new sandbox and return its accountId -const createSandboxForLocalDev = async (accountId, accountConfig, env) => { +export async function createSandboxForLocalDev( + accountId: number, + accountConfig: CLIAccount, + env: Environment +): Promise { try { await validateSandboxUsageLimits( accountConfig, @@ -222,6 +243,12 @@ const createSandboxForLocalDev = async (accountId, accountConfig, env) => { const targetAccountId = result.sandbox.sandboxHubId; const sandboxAccountConfig = getAccountConfig(result.sandbox.sandboxHubId); + + if (!sandboxAccountConfig) { + logger.error('TODO'); + process.exit(EXIT_CODES.ERROR); + } + const syncTasks = await getAvailableSyncTypes( accountConfig, sandboxAccountConfig @@ -239,14 +266,14 @@ const createSandboxForLocalDev = async (accountId, accountConfig, env) => { logError(err); process.exit(EXIT_CODES.ERROR); } -}; +} // Create a developer test account and return its accountId -const createDeveloperTestAccountForLocalDev = async ( - accountId, - accountConfig, - env -) => { +export async function createDeveloperTestAccountForLocalDev( + accountId: number, + accountConfig: CLIAccount, + env: Environment +): Promise { let currentPortalCount = 0; let maxTestPortals = 10; try { @@ -302,10 +329,13 @@ const createDeveloperTestAccountForLocalDev = async ( logError(err); process.exit(EXIT_CODES.ERROR); } -}; +} // Prompt user to confirm usage of an existing developer test account that is not currently in the config -const useExistingDevTestAccount = async (env, account) => { +export async function useExistingDevTestAccount( + env: Environment, + account: DeveloperTestAccount +): Promise { const useExistingDevTestAcct = await confirmUseExistingDeveloperTestAccountPrompt(account); if (!useExistingDevTestAcct) { @@ -333,15 +363,15 @@ const useExistingDevTestAccount = async (env, account) => { authType: PERSONAL_ACCESS_KEY_AUTH_METHOD.name, }) ); -}; +} // Prompt the user to create a new project if one doesn't exist on their target account -const createNewProjectForLocalDev = async ( - projectConfig, - targetAccountId, - shouldCreateWithoutConfirmation, - hasPublicApps -) => { +export async function createNewProjectForLocalDev( + projectConfig: ProjectConfig, + targetAccountId: number, + shouldCreateWithoutConfirmation: boolean, + hasPublicApps: boolean +): Promise { // Create the project without prompting if this is a newly created sandbox let shouldCreateProject = shouldCreateWithoutConfirmation; @@ -404,26 +434,46 @@ const createNewProjectForLocalDev = async ( ); process.exit(EXIT_CODES.SUCCESS); } -}; +} + +function projectUploadCallback( + accountId: number, + projectConfig: ProjectConfig, + tempFile: FileResult, + buildId?: number +): Promise { + if (!buildId) { + throw new Error('TODO'); + } -// Create an initial build if the project was newly created in the account -// Return the newly deployed build -const createInitialBuildForNewProject = async ( - projectConfig, - projectDir, - targetAccountId -) => { - const initialUploadResult = await handleProjectUpload( - targetAccountId, + return pollProjectBuildAndDeploy( + accountId, projectConfig, - projectDir, - (...args) => pollProjectBuildAndDeploy(...args, true), - i18n(`${i18nKey}.createInitialBuildForNewProject.initialUploadMessage`) + tempFile, + buildId, + true ); +} + +// Create an initial build if the project was newly created in the account +// Return the newly deployed build +export async function createInitialBuildForNewProject( + projectConfig: ProjectConfig, + projectDir: string, + targetAccountId: number +): Promise { + const { result: initialUploadResult, uploadError } = + await handleProjectUpload( + targetAccountId, + projectConfig, + projectDir, + projectUploadCallback, + i18n(`${i18nKey}.createInitialBuildForNewProject.initialUploadMessage`) + ); - if (initialUploadResult.uploadError) { + if (uploadError) { if ( - isSpecifiedError(initialUploadResult.uploadError, { + isSpecifiedError(uploadError, { subCategory: PROJECT_ERROR_TYPES.PROJECT_LOCKED, }) ) { @@ -434,7 +484,7 @@ const createInitialBuildForNewProject = async ( logger.log(); } else { logError( - initialUploadResult.uploadError, + uploadError, new ApiErrorContext({ accountId: targetAccountId, projectName: projectConfig.name, @@ -444,13 +494,13 @@ const createInitialBuildForNewProject = async ( process.exit(EXIT_CODES.ERROR); } - if (!initialUploadResult.succeeded) { - let subTasks = []; + if (!initialUploadResult?.succeeded) { + let subTasks: ProjectSubtask[] = []; - if (initialUploadResult.buildResult.status === 'FAILURE') { + if (initialUploadResult?.buildResult.status === 'FAILURE') { subTasks = initialUploadResult.buildResult[PROJECT_BUILD_TEXT.SUBTASK_KEY]; - } else if (initialUploadResult.deployResult.status === 'FAILURE') { + } else if (initialUploadResult?.deployResult?.status === 'FAILURE') { subTasks = initialUploadResult.deployResult[PROJECT_DEPLOY_TEXT.SUBTASK_KEY]; } @@ -467,25 +517,11 @@ const createInitialBuildForNewProject = async ( } return initialUploadResult.buildResult; -}; +} -const getAccountHomeUrl = accountId => { +export function getAccountHomeUrl(accountId: number): string { const baseUrl = getHubSpotWebsiteOrigin( getEnv(accountId) === 'qa' ? ENVIRONMENTS.QA : ENVIRONMENTS.PROD ); return `${baseUrl}/home?portalId=${accountId}`; -}; - -module.exports = { - confirmDefaultAccountIsTarget, - checkIfDefaultAccountIsSupported, - checkIfAccountFlagIsSupported, - suggestRecommendedNestedAccount, - createSandboxForLocalDev, - createDeveloperTestAccountForLocalDev, - useExistingDevTestAccount, - createNewProjectForLocalDev, - createInitialBuildForNewProject, - getAccountHomeUrl, - checkIfParentAccountIsAuthed, -}; +} diff --git a/lib/projects/buildAndDeploy.ts b/lib/projects/buildAndDeploy.ts index 1a1960f7f..83f2c5d63 100644 --- a/lib/projects/buildAndDeploy.ts +++ b/lib/projects/buildAndDeploy.ts @@ -36,6 +36,7 @@ import { ProjectTask, ProjectSubtask, ProjectPollStatusFunctionText, + ProjectPollResult, } from '../../types/Projects'; const i18nKey = 'lib.projectBuildAndDeploy'; @@ -453,13 +454,6 @@ export const pollDeployStatus = makePollTaskStatusFunc({ }, }); -type ProjectPollResult = { - succeeded: boolean; - buildId: number; - buildResult: Build; - deployResult: Deploy | null; -}; - export async function displayWarnLogs( accountId: number, projectName: string, diff --git a/lib/projects/upload.ts b/lib/projects/upload.ts index 98068259d..b3ff5ff1e 100644 --- a/lib/projects/upload.ts +++ b/lib/projects/upload.ts @@ -80,19 +80,20 @@ type ProjectUploadCallbackFunction = ( projectConfig: ProjectConfig, tempFile: FileResult, buildId?: number -) => Promise; +) => Promise; -type ProjectUploadDefaultResult = { +type ProjectUploadResult = { + result?: T; uploadError?: unknown; }; -export async function handleProjectUpload( +export async function handleProjectUpload( accountId: number, projectConfig: ProjectConfig, projectDir: string, callbackFunc: ProjectUploadCallbackFunction, uploadMessage: string -) { +): Promise> { const srcDir = path.resolve(projectDir, projectConfig.srcDir); const filenames = fs.readdirSync(srcDir); @@ -116,10 +117,8 @@ export async function handleProjectUpload( const output = fs.createWriteStream(tempFile.name); const archive = archiver('zip'); - const result = new Promise(resolve => + const result = new Promise>(resolve => output.on('close', async function () { - let uploadResult: ProjectUploadDefaultResult | T | undefined; - logger.debug( i18n(`${i18nKey}.handleProjectUpload.compressed`, { byteCount: archive.pointer(), @@ -136,16 +135,16 @@ export async function handleProjectUpload( if (error) { console.log(error); - uploadResult = { uploadError: error }; + resolve({ uploadError: error }); } else if (callbackFunc) { - uploadResult = await callbackFunc( + const uploadResult = await callbackFunc( accountId, projectConfig, tempFile, buildId ); + resolve({ result: uploadResult }); } - resolve(uploadResult || {}); }) ); diff --git a/lib/prompts/projectDevTargetAccountPrompt.ts b/lib/prompts/projectDevTargetAccountPrompt.ts index 8be678aa2..b89774159 100644 --- a/lib/prompts/projectDevTargetAccountPrompt.ts +++ b/lib/prompts/projectDevTargetAccountPrompt.ts @@ -11,7 +11,7 @@ import { import { getAccountIdentifier } from '@hubspot/local-dev-lib/config/getAccountIdentifier'; import { logger } from '@hubspot/local-dev-lib/logger'; import { fetchDeveloperTestAccounts } from '@hubspot/local-dev-lib/api/developerTestAccounts'; -import { CLIAccount, AccountType } from '@hubspot/local-dev-lib/types/Accounts'; +import { CLIAccount } from '@hubspot/local-dev-lib/types/Accounts'; import { Usage } from '@hubspot/local-dev-lib/types/Sandbox'; import { DeveloperTestAccount, @@ -214,7 +214,7 @@ async function selectTargetAccountPrompt( export async function confirmDefaultAccountPrompt( accountName: string, - accountType: AccountType + accountType: string ): Promise { const { useDefaultAccount } = await promptUser<{ useDefaultAccount: boolean; diff --git a/types/Projects.ts b/types/Projects.ts index e6010faeb..b68fa614c 100644 --- a/types/Projects.ts +++ b/types/Projects.ts @@ -49,3 +49,10 @@ export type ProjectTemplateRepoConfig = { projects?: ProjectTemplate[]; components?: ComponentTemplate[]; }; + +export type ProjectPollResult = { + succeeded: boolean; + buildId: number; + buildResult: Build; + deployResult: Deploy | null; +}; From 88b54cb560ca6588e3da83d614ab8b9460978eee Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Tue, 14 Jan 2025 17:16:17 -0500 Subject: [PATCH 11/14] Convert LocalDevManager to TS --- lib/LocalDevManager.ts | 278 +++++++++++++++++--------- lib/projects/structure.ts | 27 +++ lib/prompts/installPublicAppPrompt.ts | 2 +- types/Projects.ts | 4 +- 4 files changed, 211 insertions(+), 100 deletions(-) diff --git a/lib/LocalDevManager.ts b/lib/LocalDevManager.ts index 18af42151..59c86b400 100644 --- a/lib/LocalDevManager.ts +++ b/lib/LocalDevManager.ts @@ -1,40 +1,47 @@ -// @ts-nocheck -const path = require('path'); -const chokidar = require('chokidar'); -const chalk = require('chalk'); -const { i18n } = require('./lang'); -const { handleKeypress } = require('./process'); -const { logger } = require('@hubspot/local-dev-lib/logger'); -const { - fetchAppInstallationData, -} = require('@hubspot/local-dev-lib/api/localDevAuth'); -const { +import path from 'path'; +import chokidar, { FSWatcher } from 'chokidar'; +import chalk from 'chalk'; +import { logger } from '@hubspot/local-dev-lib/logger'; +import { fetchAppInstallationData } from '@hubspot/local-dev-lib/api/localDevAuth'; +import { fetchPublicAppsForPortal, fetchPublicAppProductionInstallCounts, -} = require('@hubspot/local-dev-lib/api/appsDev'); -const { +} from '@hubspot/local-dev-lib/api/appsDev'; +import { getAccountId, getConfigDefaultAccount, -} = require('@hubspot/local-dev-lib/config'); -const { PROJECT_CONFIG_FILE } = require('./constants'); -const SpinniesManager = require('./ui/SpinniesManager'); -const DevServerManager = require('./DevServerManager'); -const { EXIT_CODES } = require('./enums/exitCodes'); -const { getProjectDetailUrl } = require('./projects/urls'); -const { getAccountHomeUrl } = require('./localDev'); -const { CONFIG_FILES, getAppCardConfigs } = require('./projects/structure'); -const { ComponentTypes } = require('../types/Projects'); -const { +} from '@hubspot/local-dev-lib/config'; +import { Build } from '@hubspot/local-dev-lib/types/Build'; +import { PublicApp } from '@hubspot/local-dev-lib/types/Apps'; +import { Environment } from '@hubspot/local-dev-lib/types/Config'; + +import { PROJECT_CONFIG_FILE } from './constants'; +import SpinniesManager from './ui/SpinniesManager'; +import DevServerManager from './DevServerManager'; +import { EXIT_CODES } from './enums/exitCodes'; +import { getProjectDetailUrl } from './projects/urls'; +import { getAccountHomeUrl } from './localDev'; +import { + componentIsApp, + componentIsPublicApp, + CONFIG_FILES, + getAppCardConfigs, + getComponentUid, +} from './projects/structure'; +import { Component, ComponentTypes, ProjectConfig } from '../types/Projects'; +import { UI_COLORS, uiCommandReference, uiAccountDescription, uiBetaTag, uiLink, uiLine, -} = require('./ui'); -const { logError } = require('./errorHandlers/index'); -const { installPublicAppPrompt } = require('./prompts/installPublicAppPrompt'); -const { confirmPrompt } = require('./prompts/promptUtils'); +} from './ui'; +import { logError } from './errorHandlers/index'; +import { installPublicAppPrompt } from './prompts/installPublicAppPrompt'; +import { confirmPrompt } from './prompts/promptUtils'; +import { i18n } from './lang'; +import { handleKeypress } from './process'; const WATCH_EVENTS = { add: 'add', @@ -45,11 +52,42 @@ const WATCH_EVENTS = { const i18nKey = 'lib.LocalDevManager'; +type LocalDevManagerConstructorOptions = { + targetAccountId: number; + parentAccountId: number; + projectConfig: ProjectConfig; + projectDir: string; + projectId: number; + debug?: boolean; + deployedBuild: Build; + isGithubLinked: boolean; + runnableComponents: Component[]; + env: Environment; +}; + class LocalDevManager { - constructor(options) { + targetAccountId: number; + targetProjectAccountId: number; + projectConfig: ProjectConfig; + projectDir: string; + projectId: number; + debug: boolean; + deployedBuild: Build; + isGithubLinked: boolean; + watcher: FSWatcher | null; + uploadWarnings: { [key: string]: boolean }; + runnableComponents: Component[]; + activeApp: Component | null; + activePublicAppData: PublicApp | null; + env: Environment; + publicAppActiveInstalls: number | null; + projectSourceDir: string; + mostRecentUploadWarning: string | null; + + constructor(options: LocalDevManagerConstructorOptions) { this.targetAccountId = options.targetAccountId; // The account that the project exists in. This is not always the targetAccountId - this.targetProjectAccountId = options.parentAccountId || options.accountId; + this.targetProjectAccountId = options.parentAccountId; this.projectConfig = options.projectConfig; this.projectDir = options.projectDir; @@ -64,6 +102,7 @@ class LocalDevManager { this.activePublicAppData = null; this.env = options.env; this.publicAppActiveInstalls = null; + this.mostRecentUploadWarning = null; this.projectSourceDir = path.join( this.projectDir, @@ -76,7 +115,7 @@ class LocalDevManager { } } - async setActiveApp(appUid) { + async setActiveApp(appUid?: string): Promise { if (!appUid) { logger.error( i18n(`${i18nKey}.missingUid`, { @@ -85,11 +124,12 @@ class LocalDevManager { ); process.exit(EXIT_CODES.ERROR); } - this.activeApp = this.runnableComponents.find(component => { - return component.config.uid === appUid; - }); + this.activeApp = + this.runnableComponents.find(component => { + return getComponentUid(component) === appUid; + }) || null; - if (this.activeApp.type === ComponentTypes.PublicApp) { + if (componentIsPublicApp(this.activeApp)) { try { await this.setActivePublicAppData(); await this.checkActivePublicAppInstalls(); @@ -100,7 +140,7 @@ class LocalDevManager { } } - async setActivePublicAppData() { + async setActivePublicAppData(): Promise { if (!this.activeApp) { return; } @@ -110,9 +150,13 @@ class LocalDevManager { } = await fetchPublicAppsForPortal(this.targetProjectAccountId); const activePublicAppData = portalPublicApps.find( - ({ sourceId }) => sourceId === this.activeApp.config.uid + ({ sourceId }) => sourceId === getComponentUid(this.activeApp) ); + if (!activePublicAppData) { + return; + } + // TODO: Update to account for new API with { data } const { data: { uniquePortalInstallCount }, @@ -125,7 +169,7 @@ class LocalDevManager { this.publicAppActiveInstalls = uniquePortalInstallCount; } - async checkActivePublicAppInstalls() { + async checkActivePublicAppInstalls(): Promise { if ( !this.activePublicAppData || !this.publicAppActiveInstalls || @@ -156,7 +200,7 @@ class LocalDevManager { } } - async start() { + async start(): Promise { SpinniesManager.stopAll(); SpinniesManager.init(); @@ -205,11 +249,11 @@ class LocalDevManager { getProjectDetailUrl( this.projectConfig.name, this.targetProjectAccountId - ) + ) || '' ) ); - if (this.activeApp.type === ComponentTypes.PublicApp) { + if (this.activeApp?.type === ComponentTypes.PublicApp) { logger.log( uiLink( i18n(`${i18nKey}.viewTestAccountLink`), @@ -237,7 +281,7 @@ class LocalDevManager { this.compareLocalProjectToDeployed(); } - async stop(showProgress = true) { + async stop(showProgress = true): Promise { if (showProgress) { SpinniesManager.add('cleanupMessage', { text: i18n(`${i18nKey}.exitingStart`), @@ -264,22 +308,20 @@ class LocalDevManager { process.exit(EXIT_CODES.SUCCESS); } - async getActiveAppInstallationData() { - const { data } = await fetchAppInstallationData( + async checkPublicAppInstallation(): Promise { + if (!componentIsPublicApp(this.activeApp) || !this.activePublicAppData) { + return; + } + + const { + data: { isInstalledWithScopeGroups, previouslyAuthorizedScopeGroups }, + } = await fetchAppInstallationData( this.targetAccountId, this.projectId, this.activeApp.config.uid, this.activeApp.config.auth.requiredScopes, this.activeApp.config.auth.optionalScopes ); - - return data; - } - - async checkPublicAppInstallation() { - const { isInstalledWithScopeGroups, previouslyAuthorizedScopeGroups } = - await this.getActiveAppInstallationData(); - const isReinstall = previouslyAuthorizedScopeGroups.length > 0; if (!isInstalledWithScopeGroups) { @@ -294,7 +336,7 @@ class LocalDevManager { } } - updateKeypressListeners() { + updateKeypressListeners(): void { handleKeypress(async key => { if ((key.ctrl && key.name === 'c') || key.name === 'q') { this.stop(); @@ -302,8 +344,8 @@ class LocalDevManager { }); } - getUploadCommand() { - const currentDefaultAccount = getConfigDefaultAccount(); + getUploadCommand(): string { + const currentDefaultAccount = getConfigDefaultAccount() || undefined; return this.targetProjectAccountId !== getAccountId(currentDefaultAccount) ? uiCommandReference( @@ -312,11 +354,15 @@ class LocalDevManager { : uiCommandReference('hs project upload'); } - logUploadWarning(reason) { - let warning = reason; - if (!reason) { + logUploadWarning(reason?: string): void { + let warning: string; + + if (reason) { + warning = reason; + } else { warning = - this.activeApp.type === ComponentTypes.PublicApp && + componentIsPublicApp(this.activeApp) && + this.publicAppActiveInstalls && this.publicAppActiveInstalls > 0 ? i18n(`${i18nKey}.uploadWarning.defaultPublicAppWarning`, { installCount: this.publicAppActiveInstalls, @@ -356,10 +402,29 @@ class LocalDevManager { } } - monitorConsoleOutput() { + monitorConsoleOutput(): void { const originalStdoutWrite = process.stdout.write.bind(process.stdout); - process.stdout.write = function (chunk, encoding, callback) { + type StdoutCallback = (err?: Error) => void; + + // Need to provide both overloads for process.stdout.write to satisfy TS + function customStdoutWrite( + this: LocalDevManager, + buffer: Uint8Array | string, + cb?: StdoutCallback + ): boolean; + function customStdoutWrite( + this: LocalDevManager, + str: Uint8Array | string, + encoding?: BufferEncoding, + cb?: StdoutCallback + ): boolean; + function customStdoutWrite( + this: LocalDevManager, + chunk: Uint8Array | string, + encoding?: BufferEncoding | StdoutCallback, + callback?: StdoutCallback + ) { // Reset the most recently logged warning if ( this.mostRecentUploadWarning && @@ -368,42 +433,51 @@ class LocalDevManager { delete this.uploadWarnings[this.mostRecentUploadWarning]; } + if (typeof encoding === 'function') { + return originalStdoutWrite(chunk, callback); + } return originalStdoutWrite(chunk, encoding, callback); - }.bind(this); + } + + customStdoutWrite.bind(this); + + process.stdout.write = customStdoutWrite; } - compareLocalProjectToDeployed() { + compareLocalProjectToDeployed(): void { const deployedComponentNames = this.deployedBuild.subbuildStatuses.map( subbuildStatus => subbuildStatus.buildName ); - const missingComponents = []; - - this.runnableComponents.forEach(({ type, config, path }) => { - if (Object.values(ComponentTypes).includes(type)) { - const cardConfigs = getAppCardConfigs(config, path); + const missingComponents: string[] = []; - if (!deployedComponentNames.includes(config.name)) { - missingComponents.push( - `${i18n(`${i18nKey}.uploadWarning.appLabel`)} ${config.name}` - ); - } + this.runnableComponents + .filter(componentIsApp) + .forEach(({ type, config, path }) => { + if (Object.values(ComponentTypes).includes(type)) { + const cardConfigs = getAppCardConfigs(config, path); - cardConfigs.forEach(cardConfig => { - if ( - cardConfig.data && - cardConfig.data.title && - !deployedComponentNames.includes(cardConfig.data.title) - ) { + if (!deployedComponentNames.includes(config.name)) { missingComponents.push( - `${i18n(`${i18nKey}.uploadWarning.uiExtensionLabel`)} ${ - cardConfig.data.title - }` + `${i18n(`${i18nKey}.uploadWarning.appLabel`)} ${config.name}` ); } - }); - } - }); + + cardConfigs.forEach(cardConfig => { + if ( + cardConfig.data && + cardConfig.data.title && + !deployedComponentNames.includes(cardConfig.data.title) + ) { + missingComponents.push( + `${i18n(`${i18nKey}.uploadWarning.uiExtensionLabel`)} ${ + cardConfig.data.title + }` + ); + } + }); + } + }); if (missingComponents.length) { this.logUploadWarning( @@ -414,7 +488,7 @@ class LocalDevManager { } } - startWatching() { + startWatching(): void { this.watcher = chokidar.watch(this.projectDir, { ignoreInitial: true, }); @@ -446,11 +520,15 @@ class LocalDevManager { }); } - async stopWatching() { - await this.watcher.close(); + async stopWatching(): Promise { + await this.watcher?.close(); } - handleWatchEvent(filePath, event, configPaths) { + handleWatchEvent( + filePath: string, + event: string, + configPaths: string[] + ): void { if (configPaths.includes(filePath)) { this.logUploadWarning(); } else { @@ -458,7 +536,7 @@ class LocalDevManager { } } - async devServerSetup() { + async devServerSetup(): Promise { try { await DevServerManager.setup({ components: this.runnableComponents, @@ -472,13 +550,15 @@ class LocalDevManager { logger.error(e); } logger.error( - i18n(`${i18nKey}.devServer.setupError`, { message: e.message }) + i18n(`${i18nKey}.devServer.setupError`, { + message: e instanceof Error ? e.message : '', + }) ); return false; } } - async devServerStart() { + async devServerStart(): Promise { try { await DevServerManager.start({ accountId: this.targetAccountId, @@ -489,13 +569,15 @@ class LocalDevManager { logger.error(e); } logger.error( - i18n(`${i18nKey}.devServer.startError`, { message: e.message }) + i18n(`${i18nKey}.devServer.startError`, { + message: e instanceof Error ? e.message : '', + }) ); process.exit(EXIT_CODES.ERROR); } } - devServerFileChange(filePath, event) { + devServerFileChange(filePath: string, event: string): void { try { DevServerManager.fileChange({ filePath, event }); } catch (e) { @@ -504,13 +586,13 @@ class LocalDevManager { } logger.error( i18n(`${i18nKey}.devServer.fileChangeError`, { - message: e.message, + message: e instanceof Error ? e.message : '', }) ); } } - async devServerCleanup() { + async devServerCleanup(): Promise { try { await DevServerManager.cleanup(); return true; @@ -519,11 +601,13 @@ class LocalDevManager { logger.error(e); } logger.error( - i18n(`${i18nKey}.devServer.cleanupError`, { message: e.message }) + i18n(`${i18nKey}.devServer.cleanupError`, { + message: e instanceof Error ? e.message : '', + }) ); return false; } } } -module.exports = LocalDevManager; +export default LocalDevManager; diff --git a/lib/projects/structure.ts b/lib/projects/structure.ts index d6ad0a903..1f4b2f55c 100644 --- a/lib/projects/structure.ts +++ b/lib/projects/structure.ts @@ -149,3 +149,30 @@ export function getProjectComponentTypes(components: Array): { components.forEach(({ type }) => (projectContents[type] = true)); return projectContents; } + +export function getComponentUid(component?: Component | null): string | null { + if (!component) { + return null; + } else if ('uid' in component.config) { + return component.config.uid; + } else { + return component.config.data.uid; + } +} + +export function componentIsApp( + component?: Component | null +): component is Component< + PublicAppComponentConfig | PrivateAppComponentConfig +> { + return ( + component?.type === ComponentTypes.PublicApp || + component?.type === ComponentTypes.PrivateApp + ); +} + +export function componentIsPublicApp( + component?: Component | null +): component is Component { + return component?.type === ComponentTypes.PublicApp; +} diff --git a/lib/prompts/installPublicAppPrompt.ts b/lib/prompts/installPublicAppPrompt.ts index 56f6759af..33e5a527b 100644 --- a/lib/prompts/installPublicAppPrompt.ts +++ b/lib/prompts/installPublicAppPrompt.ts @@ -10,7 +10,7 @@ const i18nKey = 'lib.prompts.installPublicAppPrompt'; export async function installPublicAppPrompt( env: string, targetAccountId: number, - clientId: number, + clientId: string, scopes: string[], redirectUrls: string[], isReinstall = false diff --git a/types/Projects.ts b/types/Projects.ts index 690c77eea..3a3606efb 100644 --- a/types/Projects.ts +++ b/types/Projects.ts @@ -121,9 +121,9 @@ export enum ComponentTypes { HublTheme = 'hubl-theme', } -export type Component = { +export type Component = { type: ComponentTypes; - config: GenericComponentConfig; + config: T; runnable: boolean; path: string; }; From eff583aa6566f8a87ef5cb1a87dd96a4fa26dc76 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Tue, 14 Jan 2025 17:29:21 -0500 Subject: [PATCH 12/14] Add new error messages --- lang/en.lyaml | 3 +++ lib/localDev.ts | 16 +++++++++++++--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/lang/en.lyaml b/lang/en.lyaml index 8634c9de2..d42ebc22b 100644 --- a/lang/en.lyaml +++ b/lang/en.lyaml @@ -1075,6 +1075,7 @@ en: fileChangeError: "Failed to notify local dev server of file change: {{ message }}" localDev: confirmDefaultAccountIsTarget: + configError: "An error occurred while reading the default account from your config. Run {{ authCommand }} to re-auth this account" declineDefaultAccountExplanation: "To develop on a different account, run {{ useCommand }} to change your default account, then re-run {{ devCommand }}." checkIfDefaultAccountIsSupported: publicApp: "This project contains a public app. Local development of public apps is only supported on developer accounts and developer test accounts. Change your default account using {{ useCommand }}, or link a new account with {{ authCommand }}." @@ -1095,6 +1096,7 @@ en: createInitialBuildForNewProject: initialUploadMessage: "HubSpot Local Dev Server Startup" projectLockedError: "Your project is locked. This may mean that another user is running the {{#bold}}`hs project watch`{{/bold}} command for this project. If this is you, unlock the project in Projects UI." + genericError: "An error occurred while creating the initial build for this project. Run {{ uploadCommand }} to try again." checkIfParentAccountIsAuthed: notAuthedError: "To develop this project locally, run {{ authCommand }} to authenticate the App Developer Account {{ accountId }} associated with {{ accountIdentifier }}." projects: @@ -1421,6 +1423,7 @@ en: 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." + generic: "An error occurred while creating a new sandbox. Please try again." limit: developer: one: "{{#bold}}{{ accountName }}{{/bold}} reached the limit of {{ limit }} development sandbox. diff --git a/lib/localDev.ts b/lib/localDev.ts index e8e0130b4..76fe50088 100644 --- a/lib/localDev.ts +++ b/lib/localDev.ts @@ -64,7 +64,12 @@ export async function confirmDefaultAccountIsTarget( accountConfig: CLIAccount ): Promise { if (!accountConfig.name || !accountConfig.accountType) { - throw new Error('TODO'); + logger.error( + i18n(`${i18nKey}.confirmDefaultAccountIsTarget.configError`, { + authCommand: uiCommandReference('hs auth'), + }) + ); + process.exit(EXIT_CODES.ERROR); } logger.log(); @@ -245,7 +250,7 @@ export async function createSandboxForLocalDev( const sandboxAccountConfig = getAccountConfig(result.sandbox.sandboxHubId); if (!sandboxAccountConfig) { - logger.error('TODO'); + logger.error(i18n('lib.sandbox.create.failure.generic')); process.exit(EXIT_CODES.ERROR); } @@ -443,7 +448,12 @@ function projectUploadCallback( buildId?: number ): Promise { if (!buildId) { - throw new Error('TODO'); + logger.error( + i18n(`${i18nKey}.createInitialBuildForNewProject.initialUploadMessage`, { + uploadCommand: uiCommandReference('hs project upload'), + }) + ); + process.exit(EXIT_CODES.ERROR); } return pollProjectBuildAndDeploy( From 9886bf2eb75431541a21d7716bd89a4377c11806 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Thu, 16 Jan 2025 12:02:33 -0500 Subject: [PATCH 13/14] module.export localdevmanager --- lib/LocalDevManager.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/LocalDevManager.ts b/lib/LocalDevManager.ts index 59c86b400..70eef133f 100644 --- a/lib/LocalDevManager.ts +++ b/lib/LocalDevManager.ts @@ -611,3 +611,4 @@ class LocalDevManager { } export default LocalDevManager; +module.exports = LocalDevManager; From 55bcbe772033c4c72109037bdf4a6c2d6d9cf730 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Fri, 17 Jan 2025 15:54:28 -0500 Subject: [PATCH 14/14] remove todo --- lib/LocalDevManager.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/LocalDevManager.ts b/lib/LocalDevManager.ts index 70eef133f..6de661f95 100644 --- a/lib/LocalDevManager.ts +++ b/lib/LocalDevManager.ts @@ -157,7 +157,6 @@ class LocalDevManager { return; } - // TODO: Update to account for new API with { data } const { data: { uniquePortalInstallCount }, } = await fetchPublicAppProductionInstallCounts(