From e6bea304f1d7f3e3aaccfb5c6daedc54eaac39ca Mon Sep 17 00:00:00 2001 From: Branden Rodgers Date: Fri, 3 Nov 2023 14:50:23 -0400 Subject: [PATCH 1/9] spike to clean lang file --- config/CLIConfiguration.ts | 55 +++-- config/configFile.ts | 3 +- config/configUtils.ts | 4 +- config/environment.ts | 8 +- errors/apiErrors.ts | 3 +- errors/fileSystemErrors.ts | 2 +- errors/standardErrors.ts | 4 +- http/index.ts | 8 +- lang/en.lyaml | 419 +++++++++++++++++++------------------ lib/archive.ts | 28 ++- lib/cms/functions.ts | 24 ++- lib/cms/handleFieldsJS.ts | 22 +- lib/cms/modules.ts | 6 +- lib/cms/processFieldsJs.ts | 4 +- lib/cms/templates.ts | 10 +- lib/cms/uploadFolder.ts | 12 +- lib/cms/watch.ts | 37 ++-- lib/fileMapper.ts | 32 +-- lib/github.ts | 29 ++- lib/gitignore.ts | 4 +- lib/hubdb.ts | 12 +- lib/logging/git.ts | 2 +- lib/logging/logs.ts | 34 +-- lib/oauth.ts | 6 +- lib/personalAccessKey.ts | 6 +- lib/sandboxes.ts | 30 ++- lib/trackUsage.ts | 3 +- models/OAuth2Manager.ts | 4 +- utils/cms/modules.ts | 4 +- utils/logger.ts | 2 +- utils/notify.ts | 4 +- 31 files changed, 478 insertions(+), 343 deletions(-) diff --git a/config/CLIConfiguration.ts b/config/CLIConfiguration.ts index 01abb9d1..e0ec4d59 100644 --- a/config/CLIConfiguration.ts +++ b/config/CLIConfiguration.ts @@ -129,7 +129,7 @@ class CLIConfiguration { ): boolean { const validateLogger = makeTypedLogger( logCallbacks, - 'config.cliConfiguration.validate' + `${i18nKey}.validate` ); if (!this.config) { @@ -315,7 +315,9 @@ class CLIConfiguration { } = updatedAccountFields; if (!accountId) { - throwErrorWithMessage(`${i18nKey}.updateAccount`); + throwErrorWithMessage( + `${i18nKey}.updateAccount.errors.accountIdRequired` + ); } if (!this.config) { debug(`${i18nKey}.updateAccount.noConfigToUpdate`); @@ -402,13 +404,15 @@ class CLIConfiguration { */ updateDefaultAccount(defaultAccount: string | number): CLIConfig_NEW | null { if (!this.config) { - throwErrorWithMessage(`${i18nKey}.noConfigLoaded`); + throwErrorWithMessage(`${i18nKey}.errors.noConfigLoaded`); } if ( !defaultAccount || (typeof defaultAccount !== 'number' && typeof defaultAccount !== 'string') ) { - throwErrorWithMessage(`${i18nKey}.updateDefaultAccount`); + throwErrorWithMessage( + `${i18nKey}.updateDefaultAccount.errors.invalidInput` + ); } this.config.defaultAccount = defaultAccount; @@ -420,7 +424,7 @@ class CLIConfiguration { */ renameAccount(currentName: string, newName: string): void { if (!this.config) { - throwErrorWithMessage(`${i18nKey}.noConfigLoaded`); + throwErrorWithMessage(`${i18nKey}.errors.noConfigLoaded`); } const accountId = this.getAccountId(currentName); let accountConfigToRename: CLIAccount_NEW | null = null; @@ -430,7 +434,9 @@ class CLIConfiguration { } if (!accountConfigToRename) { - throwErrorWithMessage(`${i18nKey}.renameAccount`, { currentName }); + throwErrorWithMessage(`${i18nKey}.renameAccount.errors.invalidName`, { + currentName, + }); } if (accountId) { @@ -447,19 +453,22 @@ class CLIConfiguration { */ removeAccountFromConfig(nameOrId: string | number): boolean { if (!this.config) { - throwErrorWithMessage(`${i18nKey}.noConfigLoaded`); + throwErrorWithMessage(`${i18nKey}.errors.noConfigLoaded`); } const accountId = this.getAccountId(nameOrId); if (!accountId) { - throwErrorWithMessage(`${i18nKey}.removeAccountFromConfig`, { nameOrId }); + throwErrorWithMessage( + `${i18nKey}.removeAccountFromConfig.errors.invalidId`, + { nameOrId } + ); } let removedAccountIsDefault = false; const accountConfig = this.getAccount(accountId); if (accountConfig) { - debug(`${i18nKey}.removeAccountFromConfig`, { accountId }); + debug(`${i18nKey}.removeAccountFromConfig.deleting`, { accountId }); const index = this.getConfigAccountIndex(accountId); this.config.accounts.splice(index, 1); @@ -478,11 +487,11 @@ class CLIConfiguration { */ updateDefaultMode(defaultMode: string): CLIConfig_NEW | null { if (!this.config) { - throwErrorWithMessage(`${i18nKey}.noConfigLoaded`); + throwErrorWithMessage(`${i18nKey}.errors.noConfigLoaded`); } const ALL_MODES = Object.values(MODE); if (!defaultMode || !ALL_MODES.find(m => m === defaultMode)) { - throwErrorWithMessage(`${i18nKey}.updateDefaultMode`, { + throwErrorWithMessage(`${i18nKey}.updateDefaultMode.errors.invalidMode`, { defaultMode, validModes: commaSeparatedValues(ALL_MODES), }); @@ -497,14 +506,17 @@ class CLIConfiguration { */ updateHttpTimeout(timeout: string): CLIConfig_NEW | null { if (!this.config) { - throwErrorWithMessage(`${i18nKey}.noConfigLoaded`); + throwErrorWithMessage(`${i18nKey}.errors.noConfigLoaded`); } const parsedTimeout = parseInt(timeout); if (isNaN(parsedTimeout) || parsedTimeout < MIN_HTTP_TIMEOUT) { - throwErrorWithMessage(`${i18nKey}.updateHttpTimeout`, { - timeout, - minTimeout: MIN_HTTP_TIMEOUT, - }); + throwErrorWithMessage( + `${i18nKey}.updateHttpTimeout.errors.invalidTimeout`, + { + timeout, + minTimeout: MIN_HTTP_TIMEOUT, + } + ); } this.config.httpTimeout = parsedTimeout; @@ -516,12 +528,15 @@ class CLIConfiguration { */ updateAllowUsageTracking(isEnabled: boolean): CLIConfig_NEW | null { if (!this.config) { - throwErrorWithMessage(`${i18nKey}.noConfigLoaded`); + throwErrorWithMessage(`${i18nKey}.errors.noConfigLoaded`); } if (typeof isEnabled !== 'boolean') { - throwErrorWithMessage(`${i18nKey}.updateAllowUsageTracking`, { - isEnabled: `${isEnabled}`, - }); + throwErrorWithMessage( + `${i18nKey}.updateAllowUsageTracking.errors.invalidInput`, + { + isEnabled: `${isEnabled}`, + } + ); } this.config.allowUsageTracking = isEnabled; diff --git a/config/configFile.ts b/config/configFile.ts index 44a9f0df..9bebacb1 100644 --- a/config/configFile.ts +++ b/config/configFile.ts @@ -66,7 +66,7 @@ export function parseConfig(configSource: string): CLIConfig_NEW { try { parsed = yaml.load(configSource) as CLIConfig_NEW; } catch (err) { - throwErrorWithMessage(`${i18nKey}.parsing`, {}, err as BaseError); + throwErrorWithMessage(`${i18nKey}.errors.parsing`, {}, err as BaseError); } return parsed; @@ -88,7 +88,6 @@ export function loadConfigFromFile(): CLIConfig_NEW | null { return parseConfig(source); } - // TODO: Maybe use log callbacks here debug(`${i18nKey}.errorLoading`, { configPath }); return null; diff --git a/config/configUtils.ts b/config/configUtils.ts index 3afd2a79..40958805 100644 --- a/config/configUtils.ts +++ b/config/configUtils.ts @@ -13,6 +13,8 @@ import { PersonalAccessKeyAccount_NEW, } from '../types/Accounts'; +const i18nKey = 'config.configUtils'; + export function getOrderedAccount( unorderedAccount: CLIAccount_NEW ): CLIAccount_NEW { @@ -142,7 +144,7 @@ export function generateConfig( configAccount = generateOauthAccountConfig(options as OAuthOptions); break; default: - debug('config.configUtils.unknownType', { type }); + debug(`${i18nKey}.unknownType`, { type }); return null; } diff --git a/config/environment.ts b/config/environment.ts index 661b40a1..01a259bf 100644 --- a/config/environment.ts +++ b/config/environment.ts @@ -10,6 +10,8 @@ import { import { generateConfig } from './configUtils'; import { getValidEnv } from '../lib/environment'; +const i18nKey = 'config.environment'; + type EnvironmentConfigVariables = { apiKey?: string; clientId?: string; @@ -47,12 +49,12 @@ export function loadConfigFromEnvironment(): CLIConfig_NEW | null { env, } = getConfigVariablesFromEnv(); if (!accountId) { - debug('environment.loadConfig.missingAccountId'); + debug(`${i18nKey}.loadConfig.missingAccountId`); return null; } if (!env) { - debug('environment.loadConfig.missingEnv'); + debug(`${i18nKey}.loadConfig.missingEnv`); return null; } @@ -79,6 +81,6 @@ export function loadConfigFromEnvironment(): CLIConfig_NEW | null { }); } - debug('environment.loadConfig.unknownAuthType'); + debug(`${i18nKey}.loadConfig.unknownAuthType`); return null; } diff --git a/errors/apiErrors.ts b/errors/apiErrors.ts index 3608464d..419240b3 100644 --- a/errors/apiErrors.ts +++ b/errors/apiErrors.ts @@ -8,6 +8,8 @@ import { i18n } from '../utils/lang'; import { throwError } from './standardErrors'; import { HubSpotAuthError } from '../models/HubSpotAuthError'; +const i18nKey = 'errors.apiErrors'; + export function isApiStatusCodeError(err: GenericError): boolean { return ( err.name === 'StatusCodeError' || @@ -121,7 +123,6 @@ export function throwApiStatusCodeError( error: StatusCodeError, context: StatusCodeErrorContext = {} ): never { - const i18nKey = 'errors.errorTypes.api'; const { status } = error; const { method } = error.options || {}; const { projectName } = context; diff --git a/errors/fileSystemErrors.ts b/errors/fileSystemErrors.ts index 7d2485c4..1a9d6f0d 100644 --- a/errors/fileSystemErrors.ts +++ b/errors/fileSystemErrors.ts @@ -2,7 +2,7 @@ import { i18n } from '../utils/lang'; import { isSystemError } from './standardErrors'; import { BaseError, FileSystemErrorContext } from '../types/Error'; -const i18nKey = 'errors.errorTypes.fileSystem'; +const i18nKey = 'errors.fileSystemErrors'; export function throwFileSystemError( error: BaseError, diff --git a/errors/standardErrors.ts b/errors/standardErrors.ts index be1d78cc..11714d67 100644 --- a/errors/standardErrors.ts +++ b/errors/standardErrors.ts @@ -18,7 +18,7 @@ function genericThrowErrorWithMessage( interpolation?: { [key: string]: string | number }, cause?: BaseError ): never { - const message = i18n(`errors.${identifier}`, interpolation); + const message = i18n(identifier, interpolation); if (cause) { throw new ErrorType(message, { cause }); } @@ -73,7 +73,7 @@ export function throwError(error: BaseError): never { } else { // Error or Error subclass const name = error.name || 'Error'; - const message = [i18n('errors.errorTypes.generic', { name })]; + const message = [i18n('errors.generic', { name })]; [error.message, error.reason].forEach(msg => { if (msg) { message.push(msg); diff --git a/http/index.ts b/http/index.ts index 5267556d..d9f3de97 100644 --- a/http/index.ts +++ b/http/index.ts @@ -13,6 +13,8 @@ import { throwErrorWithMessage } from '../errors/standardErrors'; import { makeTypedLogger } from '../utils/logger'; import axios, { AxiosRequestConfig, AxiosResponse } from 'axios'; +const i18nKey = 'http.index'; + async function withOauth( accountId: number, accountConfig: FlatAccountFields, @@ -22,7 +24,7 @@ async function withOauth( const oauth = getOauthManager(accountId, accountConfig); if (!oauth) { - throwErrorWithMessage('http.index.withOauth', { accountId }); + throwErrorWithMessage(`${i18nKey}.errors.withOauth`, { accountId }); } const accessToken = await oauth.accessToken(); @@ -72,7 +74,7 @@ async function withAuth( const accountConfig = getAccountConfig(accountId); if (!accountConfig) { - throwErrorWithMessage('http.index.withAuth', { accountId }); + throwErrorWithMessage(`${i18nKey}.errors.withAuth`, { accountId }); } const { env, authType, apiKey } = accountConfig; @@ -173,7 +175,7 @@ function createGetRequestStream(contentType: string) { const axiosConfig = addQueryParams(rest, query); const logger = makeTypedLogger( logCallbacks, - 'http.index.createGetRequestStream' + `${i18nKey}.createGetRequestStream` ); // eslint-disable-next-line no-async-promise-executor diff --git a/lang/en.lyaml b/lang/en.lyaml index ca340df5..7ef15269 100644 --- a/lang/en.lyaml +++ b/lang/en.lyaml @@ -1,9 +1,16 @@ en: - debug: + lib: + trackUsage: + invalidEvent: "Usage tracking event {{ eventName }} is not a valid event type." + sendingEventAuthenticated: "Sending usage event to authenticated endpoint" + sendingEventUnauthenticated: "Sending usage event to unauthenticated endpoint" archive: extractZip: init: "Extracting project source..." success: "Completed project source extraction." + errors: + write: "An error occured writing temp project source." + extract: "An error occured extracting project source." copySourceToDest: init: "Copying project source..." sourceEmpty: "Project source is empty" @@ -11,91 +18,60 @@ en: error: "An error occured copying project source to {{ dest }}." cleanupTempDir: error: "Failed to clean up temp dir: {{ tmpDir }}" + gitignore: + errors: + configIgnore: "Unable to determine if config file is properly ignored by git." github: - fetchJsonFromRepository: "Fetching {{ url }}..." + fetchJsonFromRepository: + fetching: "Fetching {{ url }}..." + errors: + fetchFail: "An error occured fetching JSON file." + fetchReleaseData: + errors: + fetchFail: "Failed fetching release data for {{ tag }} project." downloadGithubRepoZip: fetching: "Fetching {{ releaseType }} with name {{ repoName }}..." fetchingName: "Fetching {{ name }}..." completed: "Completed project fetch." + errors: + fetchFail: "An error occurred fetching the project source." cloneGithubRepo: success: "Your new {{ type }} has been created in {{ dest }}" downloadGithubRepoContents: downloading: "Downloading content piece: {{ contentPiecePath }} from {{ downloadUrl }} to {{ downloadPath }}" - git: - securityIssue: "Security Issue Detected" - configFileTracked: "The HubSpot config file can be tracked by git." - fileName: 'File: "{{ configPath }}"' - remediate: "To remediate:" - moveConfig: "- Move the config file to your home directory: '{{ homeDir }}'" - addGitignore: "- Add gitignore pattern '{{ configPath }}' to a .gitignore file in root of your repository." - noRemote: "- Ensure that the config file has not already been pushed to a remote repository." - checkFailed: "Unable to determine if config file is properly ignored by git." - modules: - createModule: - creatingModule: "Creating module at {{ path }}" - creatingPath: "Creating {{ path }}" - oauth: - writeTokenInfo: "Updating Oauth2 token info for portalId: {{ portalId }}" - addOauthToAccountConfig: - init: "Updating configuration" - success: "Configuration updated" - filemapper: - skippedExisting: "Skipped existing {{ filepath }}" - wroteFolder: "Wrote folder {{ filepath }}" - completedFetch: 'Completed fetch of file "{{ src }}"{{ version }} to "{{ dest }}" from the Design Manager' - folderFetch: 'Fetched "{{ src }}" from account {{ accountId }} from the Design Manager successfully' - completedFolderFetch: 'Completed fetch of folder "{{ src }}"{{ version }} to "{{ dest }}" from the Design Manager' - watch: - notifyOfThemePreview: "To preview this theme, visit: {{ previewUrl }}" - skipUnsupportedExtension: "Skipping {{ file }} due to unsupported extension" - skipIgnoreRule: "Skipping {{ file }} due to an ignore rule" - uploadAttempt: 'Attempting to upload file "{{ file }}" to "{{ dest }}"' - uploadSuccess: "Uploaded file {{ file }} to {{ dest }}" - uploadFailed: "Uploading file {{ file }} to {{ dest }} failed" - uploadRetry: 'Retrying to upload file "{{ file }}" to "{{ dest }}"' - deleteAttempt: "Attempting to delete file {{ remoteFilePath }}" - deleteAttemptWithType: "Attempting to delete {{ type }} {{ remoteFilePath }}" - deleteSuccess: "Deleted file {{ remoteFilePath }}" - deleteSuccessWithType: "Deleted {{ type }} {{ remoteFilePath }}" - deleteFailed: "Deleting file {{ remoteFilePath }} failed" - folderUploadSuccess: "Completed uploading files in {{ src }} to {{ dest }} in {{ accountId }}" - ready: "Watcher is ready and watching {{ src }}. Any changes detected will be automatically uploaded and overwrite the current version in the developer file system." - config: - cliConfiguration: - load: - configFromEnv: "Loaded config from environment variables for {{ accountId }}" - configFromFile: "Loaded config from configuration file." - empty: "The config file was empty. Initializing an empty config." - validate: - noConfig: "Valiation failed: No config was found." - noConfigAccounts: "Valiation failed: config.accounts[] is not defined." - emptyAccountConfig: "Valiation failed: config.accounts[] has an empty entry." - noAccountId: "Valiation failed: config.accounts[] has an entry missing accountId." - duplicateAccountIds: "Valiation failed: config.accounts[] has multiple entries with {{ accountId }}." - duplicateAccountNames: "Valiation failed: config.accounts[] has multiple entries with {{ accountName }}." - nameContainsSpaces: "Valiation failed: config.name {{ accountName }} cannot contain spaces." - updateAccount: - noConfigToUpdate: "No config to update." - updating: "Updating account config for {{ accountId }}" - addingConfigEntry: "Adding account config entry for {{ accountId }}" - removeAccountFromConfig: "Deleting config for {{ accountId }}" - configFile: - errorReading: "Config file could not be read: {{ configPath }}" - writeSuccess: "Successfully wrote updated config data to {{ configPath }}" - errorLoading: "A configuration file could not be found at {{ configPath }}." - configUtils: - unknownType: "Unknown auth type {{ type }}" - environment: - loadConfig: - missingAccountId: "Unable to load config from environment variables: Missing accountId" - missingEnv: "Unable to load config from environment variables: Missing env" - unknownAuthType: "Unable to load config from environment variables: Unknown auth type" + errors: + fetchFail: "Failed to fetch contents: {{ errorMessage }}" + hubdb: + errors: + invalidJsonPath: "The HubDB table file must be a '.json' file" + invalidJsonFile: "The '{{{ src }}' path is not a path to a file" + personalAccessKey: + errors: + accountNotFound: "Account with id {{ accountId }} does not exist." + invalidPersonalAccessKey: "Error while retrieving new access token: {{ errorMessage }}" + sandboxes: + errors: + createSandbox: "There was an error creating your sandbox." + deleteSandbox: "There was an error deleting your sandbox." + getSandboxUsageLimits: "There was an error fetching sandbox usage limits." + initiateSync: "There was an error initiating the sandbox sync." + fetchTaskStatus: "There was an error fetching the task status while syncing sandboxes." + fetchTypes: "There was an error fetching sandbox types." cms: + modules: + createModule: + creatingModule: "Creating module at {{ path }}" + creatingPath: "Creating {{ path }}" + errors: + writeModuleMeta: "The {{ path }} path already exists" functions: updateExistingConfig: unableToReadFile: "The file {{ configFilePath }} could not be read" invalidJSON: "The file {{ configFilePath }} is not valid JSON" couldNotUpdateFile: "The file {{ configFilePath }} could not be updated" + errors: + configIsNotObjectError: "The existing {{ configFilePath }} is not an object" + endpointAreadyExistsError: "The endpoint {{ endpointPath }} already exists in {{ configFilePath }}" createFunction: destPathAlreadyExists: "The {{ path }} path already exists" createdDest: "Created {{ path }}" @@ -103,145 +79,186 @@ en: createdFunctionFile: "Created {{ path }}" createdConfigFile: "Created {{ path }}" success: "A function for the endpoint '/_hcms/api/{{ endpointPath }}' has been created. Upload {{ folderName }} to try it out" + errors: + nestedConfigError: "Cannot create a functions directory inside '{{ ancestorConfigPath }}'" + jsFileConflictError: "The JavaScript file at '{{ functionFilePath }}'' already exists" handleFieldsJs: convertFieldsJs: creating: "Creating child process with pid {{ pid }}" terminating: "Child process with pid {{ pid }} has been terminated" + errors: + errorConverting: "There was an error converting '{{ filePath }}'" + saveOutput: + errors: + saveFailed: "There was an error saving the json output of {{ path }}" + createTmpDirSync: + errors: + writeFailed: "An error occured writing temporary project source." + cleanupTmpDirSync: + errors: + deleteFailed: "There was an error deleting the temporary project source" uploadFolder: - attempt: 'Attempting to upload file "{{ file }}" to "{{ destPath }}"' - success: 'Uploaded file "{{ file}}" to "{{ destPath }}"' - failed: 'Uploading file "{{ file }}" to "{{ destPath }}" failed so scheduled retry' - retry: 'Retrying to upload file "{{ file }}" to "{{ destPath }}"' - retryFailed: 'Uploading file "{{ file }}" to "{{ destPath }}" failed' - models: - OAuth2Manager: - fetchingAccessToken: "Fetching access token for accountId {{ accountId }} for clientId {{ clientId }}" - updatingTokenInfo: "Persisting updated tokenInfo for accountId {{ accountId }} for clientId {{ clientId }}" - refreshingAccessToken: "Waiting for access token for accountId {{ accountId }} for clientId {{ clientId }} to be fetched" - http: - index: - createGetRequestStream: - onWrite: "Wrote file {{ filepath }}" - api: - filemapper: - trackUsage: - invalidEvent: "Usage tracking event {{ eventName }} is not a valid event type." - sendingEventAuthenticated: "Sending usage event to authenticated endpoint" - sendingEventUnauthenticated: "Sending usage event to unauthenticated endpoint" - templates: - debug: - creatingPath: "Making {{ path }} if needed" - logging: - creatingFile: "Creating file at {{ path }}" - processFieldsJs: - converting: 'Converting "{{ src }}" to "{{ dest }}".' - converted: 'Finished converting "{{ src }}" to "{{ dest }}".' - errors: - hubdb: - invalidJsonPath: "The HubDB table file must be a '.json' file" - invalidJsonFile: "The '{{{ src }}' path is not a path to a file" - archive: - extractZip: - write: "An error occured writing temp project source." - extract: "An error occured extracting project source." - errorTypes: - fileSystem: - readAction: "reading from" - writeAction: "writing to" - otherAction: "accessing" - unknownFilepath: "a file or folder" - baseMessage: "An error occurred while {{ fileAction }} {{ filepath }}." - systemErrorMessage: "This is the result of a system error: {{ errorMessage }}" - api: - messageDetail: "{{ request }} in account {{ accountId }}" - unableToUpload: 'Unable to upload "{{ payload }}.' - codes: - 400: "The {{ messageDetail }} was bad." - 401: "The {{ messageDetail }} was unauthorized." - 403MissingScope: "Couldn't run the project command because there are scopes missing in your production account. To update scopes, deactivate your current personal access key for {{ accountId }}, and generate a new one. Then run `hs auth` to update the CLI with the new key." - 403Gating: "The current target account {{ accountId }} does not have access to HubSpot projects. To opt in to the CRM Development Beta and use projects, visit https://app.hubspot.com/l/whats-new/betas?productUpdateId=13860216." - 403: "The {{ messageDetail }} was forbidden." - 404Request: 'The {{ action }} failed because "{{ request }}" was not found in account {{ accountId }}.' - 404: "The {{ messageDetail }} was not found." - 429: "The {{ messageDetail }} surpassed the rate limit. Retry in one minute." - 503: "The {{ messageDetail }} could not be handled at this time. Please try again or visit https://help.hubspot.com/ to submit a ticket or contact HubSpot Support if the issue persists." - 500generic: "The {{ messageDetail }} failed due to a server error. Please try again or visit https://help.hubspot.com/ to submit a ticket or contact HubSpot Support if the issue persists." - 400generic: "The {{ messageDetail }} failed due to a client error." - generic: "The {{ messageDetail }} failed." - cmsFields: - syntax: 'There was an error converting JS file "{{ path }}"' - notFunction: 'There was an error loading JS file "{{ path }}". Expected type "Function" but received type "{{ returned }}". Make sure that your default export is a function.' - notReturnArray: 'There was an error loading JS file "{{ path }}". Expected type "Array" but received type "{{ returned }}" . Make sure that your function returns an array' - invalidPath: 'The path "{{ path }}" is not a path to a file or folder' - generic: "A {{ name }} has occurred" - filemapper: - invalidNode: "Invalid FileMapperNode: {{ json }}" - invalidFileType: "Invalid file type requested: {{ srcPath }}" - assetTimeout: "HubSpot assets are unavailable at the moment. Please wait a few minutes and try again." - failedToFetchFile: 'Failed fetch of file "{{ src }}" to "{{ dest }}" from the Design Manager' - failedToFetchFolder: 'Failed fetch of folder "{{ src }}" to "{{ dest }}" from the Design Manager' - invalidFetchFolderRequest: 'Invalid request for folder: "{{ src }}"' - incompleteFetch: 'Not all files in folder "{{ src }}" were successfully fetched. Re-run the last command to try again' - github: - downloadGithubRepoContents: "Failed to fetch contents: {{ errorMessage }}" - fetchJsonFromRepository: "An error occured fetching JSON file." - fetchReleaseData: "Failed fetching release data for {{ tag }} project." - downloadGithubRepoZip: "An error occurred fetching the project source." - modules: - throwInvalidPathInput: "Expected Path Input" - writeModuleMeta: "The {{ path }} path already exists" - personalAccessKey: - accountNotFound: "Account with id {{ accountId }} does not exist." - invalidPersonalAccessKey: "Error while retrieving new access token: {{ errorMessage }}" - templates: - fileAnnotations: "Error reading file annotations {{ file }}" - pathExists: "The {{ path }} path already exists" - utils: + uploadFolder: + success: 'Uploaded file "{{ file}}" to "{{ destPath }}"' + attempt: 'Attempting to upload file "{{ file }}" to "{{ destPath }}"' + failed: 'Uploading file "{{ file }}" to "{{ destPath }}" failed so scheduled retry' + retry: 'Retrying to upload file "{{ file }}" to "{{ destPath }}"' + retryFailed: 'Uploading file "{{ file }}" to "{{ destPath }}" failed' + templates: + createTemplate: + creatingFile: "Creating file at {{ path }}" + creatingPath: "Making {{ path }} if needed" + errors: + pathExists: "The {{ path }} path already exists" + processFieldsJs: + converting: 'Converting "{{ src }}" to "{{ dest }}".' + converted: 'Finished converting "{{ src }}" to "{{ dest }}".' + errors: + invalidMjsFile: ".mjs files are only supported when using Node 13.2.0+" + watch: + notifyOfThemePreview: "To preview this theme, visit: {{ previewUrl }}" + skipUnsupportedExtension: "Skipping {{ file }} due to unsupported extension" + skipIgnoreRule: "Skipping {{ file }} due to an ignore rule" + uploadAttempt: 'Attempting to upload file "{{ file }}" to "{{ dest }}"' + uploadSuccess: "Uploaded file {{ file }} to {{ dest }}" + uploadFailed: "Uploading file {{ file }} to {{ dest }} failed" + uploadRetry: 'Retrying to upload file "{{ file }}" to "{{ dest }}"' + deleteAttempt: "Attempting to delete file {{ remoteFilePath }}" + deleteAttemptWithType: "Attempting to delete {{ type }} {{ remoteFilePath }}" + deleteSuccess: "Deleted file {{ remoteFilePath }}" + deleteSuccessWithType: "Deleted {{ type }} {{ remoteFilePath }}" + deleteFailed: "Deleting file {{ remoteFilePath }} failed" + folderUploadSuccess: "Completed uploading files in {{ src }} to {{ dest }} in {{ accountId }}" + ready: "Watcher is ready and watching {{ src }}. Any changes detected will be automatically uploaded and overwrite the current version in the developer file system." + logging: git: - configIgnore: "Unable to determine if config file is properly ignored by git." - notify: - filePath: "Unable to notify file '{{ filePath }}'" - config: - cliConfiguration: + securityIssue: "Security Issue Detected" + configFileTracked: "The HubSpot config file can be tracked by git." + fileName: 'File: "{{ configPath }}"' + remediate: "To remediate:" + moveConfig: "- Move the config file to your home directory: '{{ homeDir }}'" + addGitignore: "- Add gitignore pattern '{{ configPath }}' to a .gitignore file in root of your repository." + noRemote: "- Ensure that the config file has not already been pushed to a remote repository." + checkFailed: "Unable to determine if config file is properly ignored by git." + logs: + unableToProcessLog: "Unable to process log {{ log }}" + oauth: + writeTokenInfo: "Updating Oauth2 token info for portalId: {{ portalId }}" + addOauthToAccountConfig: + init: "Updating configuration" + success: "Configuration updated" + fileMapper: + skippedExisting: "Skipped existing {{ filepath }}" + wroteFolder: "Wrote folder {{ filepath }}" + completedFetch: 'Completed fetch of file "{{ src }}"{{ version }} to "{{ dest }}" from the Design Manager' + folderFetch: 'Fetched "{{ src }}" from account {{ accountId }} from the Design Manager successfully' + completedFolderFetch: 'Completed fetch of folder "{{ src }}"{{ version }} to "{{ dest }}" from the Design Manager' + errors: + invalidRequest: "Invalid request for file: {{ src }}" + invalidNode: "Invalid FileMapperNode: {{ json }}" + invalidFileType: "Invalid file type requested: {{ srcPath }}" + assetTimeout: "HubSpot assets are unavailable at the moment. Please wait a few minutes and try again." + failedToFetchFile: 'Failed fetch of file "{{ src }}" to "{{ dest }}" from the Design Manager' + failedToFetchFolder: 'Failed fetch of folder "{{ src }}" to "{{ dest }}" from the Design Manager' + invalidFetchFolderRequest: 'Invalid request for folder: "{{ src }}"' + incompleteFetch: 'Not all files in folder "{{ src }}" were successfully fetched. Re-run the last command to try again' + config: + cliConfiguration: + errors: noConfigLoaded: "No config loaded." - updateAccount: "An accountId is required to update the config" - updateDefaultAccount: "A 'defaultAccount' with value of number or string is required to update the config." - renameAccount: "Cannot find account with identifier {{ currentName }}" - removeAccountFromConfig: "Unable to find account for {{ nameOrId }}." - updateDefaultMode: "The mode {{ defaultMode }} is invalid. Valid values are {{ validModes }}." - updateHttpTimeout: "The value {{ timeout }} is invalid. The value must be a number greater than {{ minTimeout }}." - updateAllowUsageTracking: "Unable to update allowUsageTracking. The value {{ isEnabled }} is invalid. The value must be a boolean." - configFile: + load: + configFromEnv: "Loaded config from environment variables for {{ accountId }}" + configFromFile: "Loaded config from configuration file." + empty: "The config file was empty. Initializing an empty config." + validate: + noConfig: "Valiation failed: No config was found." + noConfigAccounts: "Valiation failed: config.accounts[] is not defined." + emptyAccountConfig: "Valiation failed: config.accounts[] has an empty entry." + noAccountId: "Valiation failed: config.accounts[] has an entry missing accountId." + duplicateAccountIds: "Valiation failed: config.accounts[] has multiple entries with {{ accountId }}." + duplicateAccountNames: "Valiation failed: config.accounts[] has multiple entries with {{ accountName }}." + nameContainsSpaces: "Valiation failed: config.name {{ accountName }} cannot contain spaces." + updateAccount: + noConfigToUpdate: "No config to update." + updating: "Updating account config for {{ accountId }}" + addingConfigEntry: "Adding account config entry for {{ accountId }}" + errors: + accountIdRequired: "An accountId is required to update the config" + updateDefaultAccount: + errors: + invalidInput: "A 'defaultAccount' with value of number or string is required to update the config." + renameAccount: + errors: + invalidName: "Cannot find account with identifier {{ currentName }}" + removeAccountFromConfig: + deleting: "Deleting config for {{ accountId }}" + errors: + invalidId: "Unable to find account for {{ nameOrId }}." + updateDefaultMode: + errors: + invalidMode: "The mode {{ defaultMode }} is invalid. Valid values are {{ validModes }}." + updateHttpTimeout: + errors: + invalidTimeout: "The value {{ timeout }} is invalid. The value must be a number greater than {{ minTimeout }}." + updateAllowUsageTracking: + errors: + invalidInput: "Unable to update allowUsageTracking. The value {{ isEnabled }} is invalid. The value must be a boolean." + configFile: + errorReading: "Config file could not be read: {{ configPath }}" + writeSuccess: "Successfully wrote updated config data to {{ configPath }}" + errorLoading: "A configuration file could not be found at {{ configPath }}." + errors: parsing: "Config file could not be parsed" - models: - OAuth2Manager: + configUtils: + unknownType: "Unknown auth type {{ type }}" + environment: + loadConfig: + missingAccountId: "Unable to load config from environment variables: Missing accountId" + missingEnv: "Unable to load config from environment variables: Missing env" + unknownAuthType: "Unable to load config from environment variables: Unknown auth type" + models: + OAuth2Manager: + fetchingAccessToken: "Fetching access token for accountId {{ accountId }} for clientId {{ clientId }}" + updatingTokenInfo: "Persisting updated tokenInfo for accountId {{ accountId }} for clientId {{ clientId }}" + refreshingAccessToken: "Waiting for access token for accountId {{ accountId }} for clientId {{ clientId }} to be fetched" + errors: missingRefreshToken: "The account {{ accountId }} has not been authenticated with Oauth2" auth: "Error while retrieving new token: {{ token }}" - http: - index: + utils: + notify: + errors: + filePath: "Unable to notify file '{{ filePath }}'" + cms: + modules: + throwInvalidPathInput: "Expected Path Input" + http: + index: + createGetRequestStream: + onWrite: "Wrote file {{ filepath }}" + errors: withOauth: "Oauth manager for account {{ accountId }} not found." withAuth: "Account with id {{ accountId }} not found." - cms: - handleFieldsJs: - convertFieldsJs: "There was an error converting '{{ filePath }}'" - saveOutput: "There was an error saving the json output of {{ path }}" - createTmpDirSync: "An error occured writing temporary project source." - cleanupTmpDirSync: "There was an error deleting the temporary project source" - functions: - updateExistingConfig: - configIsNotObjectError: "The existing {{ configFilePath }} is not an object" - endpointAreadyExistsError: "The endpoint {{ endpointPath }} already exists in {{ configFilePath }}" - createFunction: - nestedConfigError: "Cannot create a functions directory inside '{{ ancestorConfigPath }}'" - jsFileConflictError: "The JavaScript file at '{{ functionFilePath }}'' already exists" - sandboxes: - createSandbox: "There was an error creating your sandbox." - deleteSandbox: "There was an error deleting your sandbox." - getSandboxUsageLimits: "There was an error fetching sandbox usage limits." - initiateSync: "There was an error initiating the sandbox sync." - fetchTaskStatus: "There was an error fetching the task status while syncing sandboxes." - fetchTypes: "There was an error fetching sandbox types." - processFieldsJs: - fieldsJsNotReturnArray: 'There was an error loading JS file "{{ path }}". Expected type "Array". Make sure that your function returns an array' - fieldsJsNotFunction: 'There was an error loading JS file "{{ path }}". Expected type "Function". Make sure that your default export is a function.' - invalidMjsFile: ".mjs files are only supported when using Node 13.2.0+" + errors: + fileSystemErrors: + readAction: "reading from" + writeAction: "writing to" + otherAction: "accessing" + unknownFilepath: "a file or folder" + baseMessage: "An error occurred while {{ fileAction }} {{ filepath }}." + apiErrors: + messageDetail: "{{ request }} in account {{ accountId }}" + unableToUpload: 'Unable to upload "{{ payload }}.' + codes: + 400: "The {{ messageDetail }} was bad." + 401: "The {{ messageDetail }} was unauthorized." + 403MissingScope: "Couldn't run the project command because there are scopes missing in your production account. To update scopes, deactivate your current personal access key for {{ accountId }}, and generate a new one. Then run `hs auth` to update the CLI with the new key." + 403Gating: "The current target account {{ accountId }} does not have access to HubSpot projects. To opt in to the CRM Development Beta and use projects, visit https://app.hubspot.com/l/whats-new/betas?productUpdateId=13860216." + 403: "The {{ messageDetail }} was forbidden." + 404Request: 'The {{ action }} failed because "{{ request }}" was not found in account {{ accountId }}.' + 404: "The {{ messageDetail }} was not found." + 429: "The {{ messageDetail }} surpassed the rate limit. Retry in one minute." + 503: "The {{ messageDetail }} could not be handled at this time. Please try again or visit https://help.hubspot.com/ to submit a ticket or contact HubSpot Support if the issue persists." + 500Generic: "The {{ messageDetail }} failed due to a server error. Please try again or visit https://help.hubspot.com/ to submit a ticket or contact HubSpot Support if the issue persists." + 400Generic: "The {{ messageDetail }} failed due to a client error." + generic: "The {{ messageDetail }} failed." + generic: "A {{ name }} has occurred" diff --git a/lib/archive.ts b/lib/archive.ts index 9dcb51a4..900550ee 100644 --- a/lib/archive.ts +++ b/lib/archive.ts @@ -11,6 +11,8 @@ import { BaseError } from '../types/Error'; const extract = promisify(__extract); +const i18nKey = 'lib.archive'; + type ZipData = { extractDir: string; tmpDir: string; @@ -20,7 +22,7 @@ async function extractZip(name: string, zip: Buffer): Promise { const result: ZipData = { extractDir: '', tmpDir: '' }; const TMP_FOLDER_PREFIX = `hubspot-temp-${name}-`; - debug('archive.extractZip.init'); + debug(`${i18nKey}.extractZip.init`); // Write zip to disk let tmpZipPath = ''; @@ -38,7 +40,11 @@ async function extractZip(name: string, zip: Buffer): Promise { write: true, }); } else { - throwErrorWithMessage('archive.extractZip.write', {}, err as BaseError); + throwErrorWithMessage( + `${i18nKey}.extractZip.errors.write`, + {}, + err as BaseError + ); } return result; } @@ -48,9 +54,13 @@ async function extractZip(name: string, zip: Buffer): Promise { await extract(tmpZipPath, { dir: tmpExtractPath }); result.extractDir = tmpExtractPath; } catch (err) { - throwErrorWithMessage('archive.extractZip.extract', {}, err as BaseError); + throwErrorWithMessage( + `${i18nKey}.extractZip.errors.extract`, + {}, + err as BaseError + ); } - debug('archive.extractZip.success'); + debug(`${i18nKey}.extractZip.success`); return result; } @@ -65,14 +75,14 @@ async function copySourceToDest( { sourceDir, includesRootDir = true }: CopySourceToDestOptions = {} ): Promise { try { - debug('archive.copySourceToDest.init'); + debug(`${i18nKey}.copySourceToDest.init`); const srcDirPath = [src]; if (includesRootDir) { const files = await fs.readdir(src); const rootDir = files[0]; if (!rootDir) { - debug('archive.copySourceToDest.sourceEmpty'); + debug(`${i18nKey}.copySourceToDest.sourceEmpty`); // Create the dest path if it doesn't already exist fs.ensureDir(dest); // No root found so nothing to copy @@ -88,10 +98,10 @@ async function copySourceToDest( const projectSrcDir = join(...srcDirPath); await fs.copy(projectSrcDir, dest); - debug('archive.copySourceToDest.success'); + debug(`${i18nKey}.copySourceToDest.success`); return true; } catch (err) { - debug('archive.copySourceToDest.error', { dest }); + debug(`${i18nKey}.copySourceToDest.error`, { dest }); throwFileSystemError(err as BaseError, { filepath: dest, write: true, @@ -105,7 +115,7 @@ function cleanupTempDir(tmpDir: string): void { try { fs.remove(tmpDir); } catch (e) { - debug('archive.cleanupTempDir.error', { tmpDir }); + debug(`${i18nKey}.cleanupTempDir.error`, { tmpDir }); } } diff --git a/lib/cms/functions.ts b/lib/cms/functions.ts index b584c1b0..378e3295 100644 --- a/lib/cms/functions.ts +++ b/lib/cms/functions.ts @@ -10,7 +10,7 @@ import { throwFileSystemError } from '../../errors/fileSystemErrors'; import { BaseError } from '../../types/Error'; import { LogCallbacksArg } from '../../types/LogCallbacks'; -const i18nKey = 'cms.functions'; +const i18nKey = 'lib.cms.functions'; type Config = { runtime: string; @@ -94,14 +94,14 @@ function updateExistingConfig( if (!isObject(config)) { throwErrorWithMessage( - `${i18nKey}.updateExistingConfig.configIsNotObjectError`, + `${i18nKey}.updateExistingConfig.errors.configIsNotObjectError`, { configFilePath } ); } if (config.endpoints) { if (config.endpoints[endpointPath]) { throwErrorWithMessage( - `${i18nKey}.updateExistingConfig.endpointAreadyExistsError`, + `${i18nKey}.updateExistingConfig.errors.endpointAreadyExistsError`, { configFilePath, endpointPath, @@ -171,9 +171,12 @@ export async function createFunction( }); if (ancestorFunctionsConfig) { - throwErrorWithMessage(`${i18nKey}.createFunction.nestedConfigError`, { - ancestorConfigPath: path.dirname(ancestorFunctionsConfig), - }); + throwErrorWithMessage( + `${i18nKey}.createFunction.errors.nestedConfigError`, + { + ancestorConfigPath: path.dirname(ancestorFunctionsConfig), + } + ); } const folderName = functionsFolder.endsWith('.functions') @@ -196,9 +199,12 @@ export async function createFunction( const configFilePath = path.join(destPath, 'serverless.json'); if (!allowExistingFile && fs.existsSync(functionFilePath)) { - throwErrorWithMessage(`${i18nKey}.createFunction.jsFileConflictError`, { - functionFilePath, - }); + throwErrorWithMessage( + `${i18nKey}.createFunction.errors.jsFileConflictError`, + { + functionFilePath, + } + ); } await downloadGithubRepoContents( diff --git a/lib/cms/handleFieldsJS.ts b/lib/cms/handleFieldsJS.ts index 8ef50677..e902b9a6 100644 --- a/lib/cms/handleFieldsJS.ts +++ b/lib/cms/handleFieldsJS.ts @@ -8,7 +8,7 @@ import { debug } from '../../utils/logger'; import { throwErrorWithMessage } from '../../errors/standardErrors'; import { BaseError } from '../../types/Error'; -const i18nKey = 'cms.handleFieldsJs'; +const i18nKey = 'lib.cms.handleFieldsJs'; export class FieldsJs { projectDir: string; @@ -85,7 +85,11 @@ export class FieldsJs { }); }); }).catch((e: BaseError) => { - throwErrorWithMessage(`${i18nKey}.convertFieldsJs`, { filePath }, e); + throwErrorWithMessage( + `${i18nKey}.convertFieldsJs.errors.errorConverting`, + { filePath }, + e + ); }); } @@ -96,7 +100,9 @@ export class FieldsJs { */ saveOutput(): void { if (!this.outputPath || !fs.existsSync(this.outputPath)) { - throwErrorWithMessage(`${i18nKey}.saveOutput`, { path: this.filePath }); + throwErrorWithMessage(`${i18nKey}.saveOutput.errors.saveFailed`, { + path: this.filePath, + }); } const relativePath = path.relative( this.rootWriteDir, @@ -111,7 +117,7 @@ export class FieldsJs { fs.copyFileSync(this.outputPath, savePath); } catch (err) { throwErrorWithMessage( - `${i18nKey}.saveOutput`, + `${i18nKey}.saveOutput.errors.saveFailed`, { path: savePath }, err as BaseError ); @@ -163,7 +169,11 @@ export function createTmpDirSync(prefix: string): string { try { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix)); } catch (err) { - throwErrorWithMessage(`${i18nKey}.createTmpDirSync`, {}, err as BaseError); + throwErrorWithMessage( + `${i18nKey}.createTmpDirSync.errors.writeFailed`, + {}, + err as BaseError + ); } return tmpDir; } @@ -173,7 +183,7 @@ export function cleanupTmpDirSync(tmpDir: string): void { fs.rm(tmpDir, { recursive: true }, err => { if (err) { throwErrorWithMessage( - `${i18nKey}.cleanupTmpDirSync`, + `${i18nKey}.cleanupTmpDirSync.errors.deleteFailed`, {}, err as BaseError ); diff --git a/lib/cms/modules.ts b/lib/cms/modules.ts index af4a4f80..fe64f421 100644 --- a/lib/cms/modules.ts +++ b/lib/cms/modules.ts @@ -13,6 +13,8 @@ import { } from '../../utils/cms/modules'; import { PathInput } from '../../types/Modules'; +const i18nKey = 'lib.cms.modules'; + // Ids for testing export const ValidationIds = { SRC_REQUIRED: 'SRC_REQUIRED', @@ -119,7 +121,7 @@ export async function createModule( ) { const logger = makeTypedLogger( logCallbacks, - 'modules.createModule' + `${i18nKey}.createModule` ); const writeModuleMeta = ( { contentTypes, moduleLabel, global }: ModuleDefinition, @@ -164,7 +166,7 @@ export async function createModule( !name || name.endsWith('.module') ? name : `${name}.module`; const destPath = path.join(dest, folderName); if (!options.allowExistingDir && fs.existsSync(destPath)) { - throwErrorWithMessage('modules.writeModuleMeta', { + throwErrorWithMessage(`${i18nKey}.createModule.errors.writeModuleMeta`, { path: destPath, }); } else { diff --git a/lib/cms/processFieldsJs.ts b/lib/cms/processFieldsJs.ts index 8bb0fe52..fdee3404 100644 --- a/lib/cms/processFieldsJs.ts +++ b/lib/cms/processFieldsJs.ts @@ -9,7 +9,7 @@ import { throwError, throwErrorWithMessage } from '../../errors/standardErrors'; import { FieldsJs } from './handleFieldsJS'; import { i18n } from '../../utils/lang'; -const i18nKey = 'processFieldsJs'; +const i18nKey = 'lib.cms.processFieldsJs'; const { dirName, fieldOptions, filePath, writeDir } = process.env; const baseName = path.basename(filePath!); @@ -108,7 +108,7 @@ async function dynamicImport(filePath: string): Promise { return exported; } else { if (getExt(filePath) == 'mjs') { - throwErrorWithMessage(`${i18nKey}.invalidMjsFile`); + throwErrorWithMessage(`${i18nKey}.errors.invalidMjsFile`); } return require(filePath); } diff --git a/lib/cms/templates.ts b/lib/cms/templates.ts index c229427a..4d892448 100644 --- a/lib/cms/templates.ts +++ b/lib/cms/templates.ts @@ -5,6 +5,8 @@ import { throwErrorWithMessage } from '../../errors/standardErrors'; import { debug, makeTypedLogger } from '../../utils/logger'; import { LogCallbacksArg } from '../../types/LogCallbacks'; +const i18nKey = 'lib.cms.templates'; + // Matches the .html file extension, excluding module.html const TEMPLATE_EXTENSION_REGEX = new RegExp(/(?( logCallbacks, - 'templates.logging' + `${i18nKey}.createTemplate` ); logger('creatingFile', { path: filePath }); diff --git a/lib/cms/uploadFolder.ts b/lib/cms/uploadFolder.ts index f7249ac2..84888149 100644 --- a/lib/cms/uploadFolder.ts +++ b/lib/cms/uploadFolder.ts @@ -23,6 +23,8 @@ import { FILE_TYPES, FILE_UPLOAD_RESULT_TYPES } from '../../constants/files'; import { FileType, UploadFolderResults } from '../../types/Files'; import { Mode } from '../../types/Files'; +const i18nKey = 'lib.cms.uploadFolder'; + const queue = new PQueue({ concurrency: 10, }); @@ -129,7 +131,7 @@ export async function uploadFolder( ): Promise> { const logger = makeTypedLogger( logCallbacks, - 'cms.uploadFolder' + `${i18nKey}.uploadFolder` ); const { saveOutput, convertFields } = commandOptions; const tmpDir = convertFields @@ -169,7 +171,7 @@ export async function uploadFolder( ); const destPath = convertToUnixPath(path.join(dest, relativePath)); return async () => { - debug('cms.uploadFolder.attempt', { + debug(`${i18nKey}.uploadFolder.attempt`, { file: originalFilePath || '', destPath, }); @@ -184,7 +186,7 @@ export async function uploadFolder( if (isFatalError(error)) { throw error; } - debug('cms.uploadFolder.failed', { file, destPath }); + debug(`${i18nKey}.uploadFolder.failed`, { file, destPath }); if (error.response && error.response.body) { console.debug(error.response.body); } else { @@ -207,7 +209,7 @@ export async function uploadFolder( .addAll( failures.map(({ file, destPath }) => { return async () => { - debug('cms.uploadFolder.retry', { file, destPath }); + debug(`${i18nKey}.uploadFolder.retry`, { file, destPath }); try { await upload(accountId, file, destPath, apiOptions); logger('success', { file, destPath }); @@ -217,7 +219,7 @@ export async function uploadFolder( file, }; } catch (err) { - debug('cms.uploadFolder.retryFailed', { file, destPath }); + debug(`${i18nKey}.uploadFolder.retryFailed`, { file, destPath }); const error = err as StatusCodeError; if (isFatalError(error)) { throw error; diff --git a/lib/cms/watch.ts b/lib/cms/watch.ts index 00abaef5..e8928c47 100644 --- a/lib/cms/watch.ts +++ b/lib/cms/watch.ts @@ -20,6 +20,8 @@ import { FileMapperInputOptions, Mode } from '../../types/Files'; import { UploadFolderResults } from '../../types/Files'; import { StatusCodeError } from '../../types/Error'; +const i18nKey = 'lib.cms.watch'; + const watchCallbackKeys = [ 'notifyOfThemePreview', 'uploadSuccess', @@ -40,7 +42,7 @@ function _notifyOfThemePreview( accountId: number, logCallbacks?: WatchLogCallbacks ): void { - const logger = makeLogger(logCallbacks, 'watch'); + const logger = makeLogger(logCallbacks, i18nKey); if (queue.size > 0) return; const previewUrl = getThemePreviewUrl(filePath, accountId); if (!previewUrl) return; @@ -66,7 +68,7 @@ async function uploadFile( mode: Mode | null = null, logCallbacks?: WatchLogCallbacks ): Promise { - const logger = makeLogger(logCallbacks, 'watch'); + const logger = makeLogger(logCallbacks, i18nKey); const src = options.src; const absoluteSrcPath = path.resolve(getCwd(), file); @@ -82,11 +84,11 @@ async function uploadFile( ); if (!isAllowedExtension(file) && !convertFields) { - debug('watch.skipUnsupportedExtension', { file }); + debug(`${i18nKey}.skipUnsupportedExtension`, { file }); return; } if (shouldIgnoreFile(file)) { - debug('watch.skipIgnoreRule', { file }); + debug(`${i18nKey}.skipIgnoreRule`, { file }); return; } @@ -105,7 +107,7 @@ async function uploadFile( const fileToUpload = convertFields && fieldsJs?.outputPath ? fieldsJs.outputPath : file; - debug('watch.uploadAttempt', { file, dest }); + debug(`${i18nKey}.uploadAttempt`, { file, dest }); const apiOptions = getFileMapperQueryValues(mode, options); queue.add(() => { return upload(accountId, fileToUpload, dest, apiOptions) @@ -114,11 +116,14 @@ async function uploadFile( notifyOfThemePreview(file, accountId, logCallbacks); }) .catch(() => { - debug('watch.uploadFailed', { file, dest }); - debug('watch.uploadRetry', { file, dest }); + debug(`${i18nKey}.uploadFailed`, { file, dest }); + debug(`${i18nKey}.uploadRetry`, { file, dest }); return upload(accountId, file, dest, apiOptions).catch( (error: StatusCodeError) => { - debug('watch.uploadFailed', { file, dest }); + debug(`${i18nKey}.uploadFailed`, { + file, + dest, + }); throwApiUploadError(error, { accountId, request: dest, @@ -136,13 +141,13 @@ async function deleteRemoteFile( remoteFilePath: string, logCallbacks?: WatchLogCallbacks ): Promise { - const logger = makeLogger(logCallbacks, 'watch'); + const logger = makeLogger(logCallbacks, i18nKey); if (shouldIgnoreFile(filePath)) { - debug('watch.skipIgnoreRule', { file: filePath }); + debug(`${i18nKey}.skipIgnoreRule`, { file: filePath }); return; } - debug('watch.deleteAttempt', { remoteFilePath }); + debug(`${i18nKey}.deleteAttempt`, { remoteFilePath }); return queue.add(() => { return deleteFile(accountId, remoteFilePath) .then(() => { @@ -150,7 +155,9 @@ async function deleteRemoteFile( notifyOfThemePreview(filePath, accountId, logCallbacks); }) .catch((error: StatusCodeError) => { - debug('watch.deleteFailed', { remoteFilePath }); + debug(`${i18nKey}.deleteFailed`, { + remoteFilePath, + }); throwApiError(error, { accountId, request: remoteFilePath, @@ -191,7 +198,7 @@ export function watch( onQueueAddError?: ErrorHandler, logCallbacks?: WatchLogCallbacks ) { - const logger = makeLogger(logCallbacks, 'watch'); + const logger = makeLogger(logCallbacks, i18nKey); const regex = new RegExp(`^${escapeRegExp(src)}`); if (notify) { ignoreFile(notify); @@ -259,11 +266,11 @@ export function watch( const remotePath = getDesignManagerPath(filePath); if (shouldIgnoreFile(filePath)) { - debug('watch.skipIgnoreRule', { file: filePath }); + debug(`${i18nKey}.skipIgnoreRule`, { file: filePath }); return; } - debug('watch.deleteAttemptWithType', { + debug(`${i18nKey}.deleteAttemptWithType`, { type, remoteFilePath: remotePath, }); diff --git a/lib/fileMapper.ts b/lib/fileMapper.ts index 3c130b55..55e8026e 100644 --- a/lib/fileMapper.ts +++ b/lib/fileMapper.ts @@ -26,6 +26,8 @@ import { BaseError, StatusCodeError } from '../types/Error'; import { LogCallbacksArg } from '../types/LogCallbacks'; import { makeTypedLogger } from '../utils/logger'; +const i18nKey = 'lib.fileMapper'; + const queue = new PQueue({ concurrency: 10, }); @@ -92,7 +94,7 @@ function validateFileMapperNode(node: FileMapperNode): void { } catch (err) { json = node; } - throwTypeErrorWithMessage('filemapper.invalidNode', { + throwTypeErrorWithMessage(`${i18nKey}.errors.invalidNode`, { json: JSON.stringify(json), }); } @@ -194,7 +196,7 @@ async function fetchAndWriteFileStream( ): Promise { const logger = makeTypedLogger( logCallbacks, - 'filemapper' + i18nKey ); if (typeof srcPath !== 'string' || !srcPath.trim()) { return; @@ -204,7 +206,7 @@ async function fetchAndWriteFileStream( return; } if (!isAllowedExtension(srcPath)) { - throwErrorWithMessage('filemapper.invalidFileType', { srcPath }); + throwErrorWithMessage(`${i18nKey}.errors.invalidFileType`, { srcPath }); } let node: FileMapperNode; try { @@ -235,7 +237,7 @@ async function writeFileMapperNode( ): Promise { const logger = makeTypedLogger( logCallbacks, - 'filemapper' + i18nKey ); const localFilepath = convertToLocalFileSystemPath(path.resolve(filepath)); if (await skipExisting(localFilepath, options.overwrite)) { @@ -285,12 +287,12 @@ async function downloadFile( ): Promise { const logger = makeTypedLogger( logCallbacks, - 'filemapper' + i18nKey ); const { isFile, isHubspot } = getTypeDataFromPath(src); try { if (!isFile) { - throw new Error(`Invalid request for file: "${src}"`); + throwErrorWithMessage(`${i18nKey}.errors.invalidRequest`, { src }); } const dest = path.resolve(destPath); const cwd = getCwd(); @@ -326,10 +328,10 @@ async function downloadFile( } catch (err) { const error = err as StatusCodeError; if (isHubspot && isTimeout(err as StatusCodeError)) { - throwErrorWithMessage('filemapper.assetTimeout', {}, error); + throwErrorWithMessage(`${i18nKey}.errors.assetTimeout`, {}, error); } else { throwErrorWithMessage( - 'filemapper.failedToFetchFile', + `${i18nKey}.errors.failedToFetchFile`, { src, dest: destPath }, error ); @@ -346,11 +348,13 @@ export async function fetchFolderFromApi( ): Promise { const logger = makeTypedLogger( logCallbacks, - 'filemapper' + i18nKey ); const { isRoot, isFolder, isHubspot } = getTypeDataFromPath(src); if (!isFolder) { - throwErrorWithMessage('filemapper.invalidFetchFolderRequest', { src }); + throwErrorWithMessage(`${i18nKey}.errors.invalidFetchFolderRequest`, { + src, + }); } try { const srcPath = isRoot ? '@root' : src; @@ -363,7 +367,7 @@ export async function fetchFolderFromApi( } catch (err) { const error = err as StatusCodeError; if (isHubspot && isTimeout(error)) { - throwErrorWithMessage('filemapper.assetTimeout', {}, error); + throwErrorWithMessage(`${i18nKey}.errors.assetTimeout`, {}, error); } else { throwStatusCodeError(error, { accountId, @@ -383,7 +387,7 @@ async function downloadFolder( ) { const logger = makeTypedLogger( logCallbacks, - 'filemapper' + i18nKey ); try { const node = await fetchFolderFromApi( @@ -431,11 +435,11 @@ async function downloadFolder( dest, }); } else { - throwErrorWithMessage('filemapper.incompleteFetch', { src }); + throwErrorWithMessage(`${i18nKey}.errors.incompleteFetch`, { src }); } } catch (err) { throwErrorWithMessage( - 'filemapper.failedToFetchFolder', + `${i18nKey}.errors.failedToFetchFolder`, { src, dest: destPath }, err as StatusCodeError ); diff --git a/lib/github.ts b/lib/github.ts index 089276c8..f45ea267 100644 --- a/lib/github.ts +++ b/lib/github.ts @@ -13,6 +13,8 @@ import { GithubReleaseData, GithubRepoFile } from '../types/Github'; import { ValueOf } from '../types/Utils'; import { LogCallbacksArg } from '../types/LogCallbacks'; +const i18nKey = 'lib.github'; + declare global { // eslint-disable-next-line no-var var githubToken: string; @@ -32,7 +34,7 @@ export async function fetchJsonFromRepository( ): Promise { try { const URL = `https://raw.githubusercontent.com/${repoPath}/${ref}/${filePath}`; - debug('github.fetchJsonFromRepository', { url: URL }); + debug(`${i18nKey}.fetchJsonFromRepository`, { url: URL }); const { data } = await axios.get(URL, { headers: { ...DEFAULT_USER_AGENT_HEADERS, ...GITHUB_AUTH_HEADERS }, @@ -40,7 +42,7 @@ export async function fetchJsonFromRepository( return data; } catch (err) { throwErrorWithMessage( - 'github.fetchJsonFromRepository', + `${i18nKey}.fetchJsonFromRepository.errors.fetchFail`, {}, err as BaseError ); @@ -66,7 +68,7 @@ export async function fetchReleaseData( } catch (err) { const error = err as BaseError; throwErrorWithMessage( - 'github.fetchReleaseData', + `${i18nKey}.fetchReleaseData.errors.fetchFail`, { tag: tag || 'latest' }, error ); @@ -84,7 +86,10 @@ async function downloadGithubRepoZip( try { let zipUrl: string; if (releaseType === GITHUB_RELEASE_TYPES.REPOSITORY) { - debug('github.downloadGithubRepoZip.fetching', { releaseType, repoPath }); + debug(`${i18nKey}.downloadGithubRepoZip.fetching`, { + releaseType, + repoPath, + }); zipUrl = `https://api.github.com/repos/${repoPath}/zipball${ ref ? `/${ref}` : '' }`; @@ -92,15 +97,19 @@ async function downloadGithubRepoZip( const releaseData = await fetchReleaseData(repoPath, tag); zipUrl = releaseData.zipball_url; const { name } = releaseData; - debug('github.downloadGithubRepoZip.fetchingName', { name }); + debug(`${i18nKey}.downloadGithubRepoZip.fetchingName`, { name }); } const { data } = await axios.get(zipUrl, { headers: { ...DEFAULT_USER_AGENT_HEADERS, ...GITHUB_AUTH_HEADERS }, }); - debug('github.downloadGithubRepoZip.completed'); + debug(`${i18nKey}.downloadGithubRepoZip.completed`); return data; } catch (err) { - throwErrorWithMessage('github.downloadGithubRepoZip', {}, err as BaseError); + throwErrorWithMessage( + `${i18nKey}.downloadGithubRepoZip.errors.fetchFail`, + {}, + err as BaseError + ); } } @@ -123,7 +132,7 @@ export async function cloneGithubRepo( ): Promise { const logger = makeTypedLogger( logCallbacks, - 'github.cloneGithubRepo' + `${i18nKey}.cloneGithubRepo` ); const { themeVersion, projectVersion, releaseType, ref } = options; const tag = projectVersion || themeVersion; @@ -193,7 +202,7 @@ export async function downloadGithubRepoContents( return Promise.resolve(); } - debug('github.downloadGithubRepoContents.downloading', { + debug(`${i18nKey}.downloadGithubRepoContents.downloading`, { contentPiecePath, downloadUrl: download_url, downloadPath, @@ -215,7 +224,7 @@ export async function downloadGithubRepoContents( const error = e as BaseError; if (error?.error?.message) { throwErrorWithMessage( - 'github.downloadGithubRepoContents', + `${i18nKey}.downloadGithubRepoContents.errors.fetchFail`, { errorMessage: error.error.message, }, diff --git a/lib/gitignore.ts b/lib/gitignore.ts index e5e60b91..a68c19bb 100644 --- a/lib/gitignore.ts +++ b/lib/gitignore.ts @@ -6,6 +6,8 @@ import { DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME } from '../constants/config'; import { throwErrorWithMessage } from '../errors/standardErrors'; import { BaseError } from '../types/Error'; +const i18nKey = 'lib.gitignore'; + const GITIGNORE_FILE = '.gitignore'; export function checkAndAddConfigToGitignore(configPath: string): void { @@ -25,6 +27,6 @@ export function checkAndAddConfigToGitignore(configPath: string): void { const updatedContents = `${gitignoreContents.trim()}\n\n# HubSpot config file\n${DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME}\n`; writeFileSync(gitignoreFilePath, updatedContents); } catch (e) { - throwErrorWithMessage('utils.git.configIgnore', {}, e as BaseError); + throwErrorWithMessage(`${i18nKey}.errors.configIgnore`, {}, e as BaseError); } } diff --git a/lib/hubdb.ts b/lib/hubdb.ts index f99ab263..7d3684e6 100644 --- a/lib/hubdb.ts +++ b/lib/hubdb.ts @@ -15,9 +15,11 @@ import { FetchRowsResponse, Row, Table } from '../types/Hubdb'; import { throwErrorWithMessage } from '../errors/standardErrors'; import { BaseError } from '../types/Error'; +const i18nKey = 'lib.hubdb'; + function validateJsonPath(src: string): void { if (path.extname(src) !== '.json') { - throwErrorWithMessage('hubdb.invalidJsonPath'); + throwErrorWithMessage(`${i18nKey}.errors.invalidJsonPath`); } } @@ -27,11 +29,15 @@ function validateJsonFile(src: string): void { try { stats = fs.statSync(src); } catch (err) { - throwErrorWithMessage('hubdb.invalidJsonFile', { src }, err as BaseError); + throwErrorWithMessage( + `${i18nKey}.errors.invalidJsonFile`, + { src }, + err as BaseError + ); } if (!stats.isFile()) { - throwErrorWithMessage('hubdb.invalidJsonFile', { src }); + throwErrorWithMessage(`${i18nKey}.errors.invalidJsonFile`, { src }); } validateJsonPath(src); diff --git a/lib/logging/git.ts b/lib/logging/git.ts index ed0a4e7c..b5e1eb25 100644 --- a/lib/logging/git.ts +++ b/lib/logging/git.ts @@ -8,7 +8,7 @@ import { DEFAULT_HUBSPOT_CONFIG_YAML_FILE_NAME } from '../../constants/config'; const GITIGNORE_FILE = '.gitignore'; -const i18nKey = 'debug.git'; +const i18nKey = 'lib.logging.git'; export function checkAndWarnGitInclusion(configPath: string): void { try { diff --git a/lib/logging/logs.ts b/lib/logging/logs.ts index 90a380c1..30108771 100644 --- a/lib/logging/logs.ts +++ b/lib/logging/logs.ts @@ -1,6 +1,9 @@ import moment from 'moment'; import chalk from 'chalk'; import { logger, Styles } from './logger'; +import { i18n } from '../../utils/lang'; + +const i18nKey = 'lib.logging.logs'; const SEPARATOR = ' - '; const LOG_STATUS_COLORS = { @@ -8,7 +11,7 @@ const LOG_STATUS_COLORS = { ERROR: Styles.error, UNHANDLED_ERROR: Styles.error, HANDLED_ERROR: Styles.error, -} +}; type Log = { log: string; @@ -18,20 +21,20 @@ type Log = { error: { type: string; message: string; - stackTrace: Array> - } -} + stackTrace: Array>; + }; +}; type Options = { compact: boolean; insertions: { header: string; - } -} + }; +}; type LogsResponse = { - results: Array -} + results: Array; +}; function errorHandler(log: Log, options: Options): string { return `${formatLogHeader(log, options)}${formatError(log, options)}`; @@ -44,7 +47,7 @@ const logHandler = { SUCCESS: (log: Log, options: Options): string => { return `${formatLogHeader(log, options)}${formatSuccess(log, options)}`; }, -} +}; function formatSuccess(log: Log, options: Options): string { if (!log.log || options.compact) { @@ -93,11 +96,18 @@ function processLog(log: Log, options: Options): string | void { try { return logHandler[log.status](log, options); } catch (e) { - logger.error(`Unable to process log ${JSON.stringify(log)}`); + logger.error( + i18n(`${i18nKey}.unableToProcessLog`, { + log: JSON.stringify(log), + }) + ); } } -function processLogs(logsResp: LogsResponse, options: Options): string | undefined { +function processLogs( + logsResp: LogsResponse, + options: Options +): string | undefined { if (!logsResp || (logsResp.results && !logsResp.results.length)) { return 'No logs found.'; } else if (logsResp.results && logsResp.results.length) { @@ -110,5 +120,5 @@ function processLogs(logsResp: LogsResponse, options: Options): string | undefin } export function outputLogs(logsResp: LogsResponse, options: Options): void { - logger.log(processLogs(logsResp, options)) + logger.log(processLogs(logsResp, options)); } diff --git a/lib/oauth.ts b/lib/oauth.ts index 985505bf..ee112fbd 100644 --- a/lib/oauth.ts +++ b/lib/oauth.ts @@ -9,12 +9,14 @@ import { makeTypedLogger } from '../utils/logger'; import { getAccountIdentifier } from '../utils/getAccountIdentifier'; import { updateAccountConfig, writeConfig } from '../config'; +const i18nKey = 'lib.oauth'; + const oauthManagers = new Map(); function writeOauthTokenInfo(accountConfig: FlatAccountFields): void { const accountId = getAccountIdentifier(accountConfig); - debug('oauth.writeTokenInfo', { portalId: accountId || '' }); + debug(`${i18nKey}.writeTokenInfo`, { portalId: accountId || '' }); updateAccountConfig(accountConfig); writeConfig(); @@ -43,7 +45,7 @@ export function addOauthToAccountConfig( ) { const logger = makeTypedLogger( logCallbacks, - 'oauth.addOauthToAccountConfig' + `${i18nKey}.addOauthToAccountConfig` ); logger('init'); try { diff --git a/lib/personalAccessKey.ts b/lib/personalAccessKey.ts index 1414b51e..3804291f 100644 --- a/lib/personalAccessKey.ts +++ b/lib/personalAccessKey.ts @@ -19,6 +19,8 @@ import { updateDefaultAccount, } from '../config'; +const i18nKey = 'lib.personalAccessKey'; + const refreshRequests = new Map(); function getRefreshKey(personalAccessKey: string, expiration?: string): string { @@ -45,7 +47,7 @@ export async function getAccessToken( const error = e as StatusCodeError; if (error.response && error.response.body) { throwAuthErrorWithMessage( - 'personalAccessKey.invalidPersonalAccessKey', + `${i18nKey}.errors.invalidPersonalAccessKey`, { errorMessage: error.response.body.message || '' }, error ); @@ -123,7 +125,7 @@ export async function accessTokenForPersonalAccessKey( ): Promise { const account = getAccountConfig(accountId) as PersonalAccessKeyAccount; if (!account) { - throwErrorWithMessage('personalAccessKey.accountNotFound', { accountId }); + throwErrorWithMessage(`${i18nKey}.errors.accountNotFound`, { accountId }); } const { auth, personalAccessKey, env } = account; const authTokenInfo = auth && auth.tokenInfo; diff --git a/lib/sandboxes.ts b/lib/sandboxes.ts index 135f0677..86077634 100644 --- a/lib/sandboxes.ts +++ b/lib/sandboxes.ts @@ -19,7 +19,7 @@ import { import { throwErrorWithMessage } from '../errors/standardErrors'; import { BaseError } from '../types/Error'; -const i18nKey = 'sandboxes'; +const i18nKey = 'lib.sandboxes'; export async function createSandbox( accountId: number, @@ -37,7 +37,11 @@ export async function createSandbox( ...resp, }; } catch (err) { - throwErrorWithMessage(`${i18nKey}.createSandbox`, {}, err as BaseError); + throwErrorWithMessage( + `${i18nKey}.errors.createSandbox`, + {}, + err as BaseError + ); } } @@ -48,7 +52,11 @@ export async function deleteSandbox( try { await _deleteSandbox(parentAccountId, sandboxAccountId); } catch (err) { - throwErrorWithMessage(`${i18nKey}.deleteSandbox`, {}, err as BaseError); + throwErrorWithMessage( + `${i18nKey}.errors.deleteSandbox`, + {}, + err as BaseError + ); } return { @@ -65,7 +73,7 @@ export async function getSandboxUsageLimits( return resp && resp.usage; } catch (err) { throwErrorWithMessage( - `${i18nKey}.getSandboxUsageLimits`, + `${i18nKey}.errors.getSandboxUsageLimits`, {}, err as BaseError ); @@ -81,7 +89,11 @@ export async function initiateSync( try { return await _initiateSync(fromHubId, toHubId, tasks, sandboxHubId); } catch (err) { - throwErrorWithMessage(`${i18nKey}.initiateSync`, {}, err as BaseError); + throwErrorWithMessage( + `${i18nKey}.errors.initiateSync`, + {}, + err as BaseError + ); } } @@ -92,7 +104,11 @@ export async function fetchTaskStatus( try { return await _fetchTaskStatus(accountId, taskId); } catch (err) { - throwErrorWithMessage(`${i18nKey}.fetchTaskStatus`, {}, err as BaseError); + throwErrorWithMessage( + `${i18nKey}.errors.fetchTaskStatus`, + {}, + err as BaseError + ); } } @@ -104,6 +120,6 @@ export async function fetchTypes( const resp = await _fetchTypes(accountId, toHubId); return resp && resp.results; } catch (err) { - throwErrorWithMessage(`${i18nKey}.fetchTypes`, {}, err as BaseError); + throwErrorWithMessage(`${i18nKey}.errors.fetchTypes`, {}, err as BaseError); } } diff --git a/lib/trackUsage.ts b/lib/trackUsage.ts index 0f12debc..aae6b82c 100644 --- a/lib/trackUsage.ts +++ b/lib/trackUsage.ts @@ -3,13 +3,14 @@ import http from '../http'; import { getAccountConfig, getEnv } from '../config'; import { FILE_MAPPER_API_PATH } from '../api/fileMapper'; +const i18nKey = 'lib.trackUsage'; + export async function trackUsage( eventName: string, eventClass: string, meta = {}, accountId: number ): Promise { - const i18nKey = 'api.filemapper.trackUsage'; const usageEvent = { accountId, eventName, diff --git a/models/OAuth2Manager.ts b/models/OAuth2Manager.ts index b4142111..aff7dac7 100644 --- a/models/OAuth2Manager.ts +++ b/models/OAuth2Manager.ts @@ -45,7 +45,7 @@ class OAuth2Manager { async accessToken(): Promise { if (!this.account.auth.tokenInfo?.refreshToken) { - throwErrorWithMessage(`${i18nKey}.missingRefreshToken`, { + throwErrorWithMessage(`${i18nKey}.errors.missingRefreshToken`, { accountId: getAccountIdentifier(this.account)!, }); } @@ -118,7 +118,7 @@ class OAuth2Manager { const error = e as StatusCodeError; if (error.response) { throwAuthErrorWithMessage( - `${i18nKey}.auth`, + `${i18nKey}.errors.auth`, { token: error.response.body.message || '', }, diff --git a/utils/cms/modules.ts b/utils/cms/modules.ts index a261a06e..0ef02675 100644 --- a/utils/cms/modules.ts +++ b/utils/cms/modules.ts @@ -4,6 +4,8 @@ import { MODULE_EXTENSION } from '../../constants/extensions'; import { throwTypeErrorWithMessage } from '../../errors/standardErrors'; import { PathInput } from '../../types/Modules'; +const i18nKey = 'utils.cms.modules'; + const isBool = (x: boolean | undefined) => !!x === x; export function isPathInput(pathInput?: PathInput): boolean { @@ -16,7 +18,7 @@ export function isPathInput(pathInput?: PathInput): boolean { function throwInvalidPathInput(pathInput: PathInput): void { if (isPathInput(pathInput)) return; - throwTypeErrorWithMessage('modules.throwInvalidPathInput'); + throwTypeErrorWithMessage(`${i18nKey}.throwInvalidPathInput`); } export function isModuleFolder(pathInput: PathInput): boolean { diff --git a/utils/logger.ts b/utils/logger.ts index 62dbbf40..00d179c2 100644 --- a/utils/logger.ts +++ b/utils/logger.ts @@ -37,5 +37,5 @@ export function debug( identifier: string, interpolation?: { [key: string]: string | number } ): void { - console.debug(i18n(`debug.${identifier}`, interpolation)); + console.debug(i18n(identifier, interpolation)); } diff --git a/utils/notify.ts b/utils/notify.ts index 8406ab71..3850de1f 100644 --- a/utils/notify.ts +++ b/utils/notify.ts @@ -5,6 +5,8 @@ import debounce from 'debounce'; import { throwErrorWithMessage } from '../errors/standardErrors'; import { BaseError } from '../types/Error'; +const i18nKey = 'utils.notify'; + const notifyQueue: Array = []; const notifyPromises: Array> = []; const debouncedWaitForActionsToCompleteAndWriteQueueToFile = debounce( @@ -50,7 +52,7 @@ function notifyFilePath(filePathToNotify: string, outputToWrite: string): void { fs.appendFileSync(filePathToNotify, outputToWrite); } catch (e) { throwErrorWithMessage( - 'utils.notify.filePath', + `${i18nKey}.errors.filePath`, { filePath: filePathToNotify }, e as BaseError ); From 74595b87c5752f98e07b25cafdb959c15df34786 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Mon, 6 Nov 2023 16:18:57 -0500 Subject: [PATCH 2/9] Set up lang json + types --- lang/en.json | 365 +++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 6 +- types/Lang.ts | 6 + types/Utils.ts | 12 ++ 4 files changed, 386 insertions(+), 3 deletions(-) create mode 100644 lang/en.json create mode 100644 types/Lang.ts diff --git a/lang/en.json b/lang/en.json new file mode 100644 index 00000000..15727e7c --- /dev/null +++ b/lang/en.json @@ -0,0 +1,365 @@ +{ + "en": { + "lib": { + "trackUsage": { + "invalidEvent": "Usage tracking event {{ eventName }} is not a valid event type.", + "sendingEventAuthenticated": "Sending usage event to authenticated endpoint", + "sendingEventUnauthenticated": "Sending usage event to unauthenticated endpoint" + }, + "archive": { + "extractZip": { + "init": "Extracting project source...", + "success": "Completed project source extraction.", + "errors": { + "write": "An error occured writing temp project source.", + "extract": "An error occured extracting project source." + } + }, + "copySourceToDest": { + "init": "Copying project source...", + "sourceEmpty": "Project source is empty", + "success": "Completed copying project source.", + "error": "An error occured copying project source to {{ dest }}." + }, + "cleanupTempDir": { + "error": "Failed to clean up temp dir: {{ tmpDir }}" + } + }, + "gitignore": { + "errors": { + "configIgnore": "Unable to determine if config file is properly ignored by git." + } + }, + "github": { + "fetchJsonFromRepository": { + "fetching": "Fetching {{ url }}...", + "errors": { + "fetchFail": "An error occured fetching JSON file." + } + }, + "fetchReleaseData": { + "errors": { + "fetchFail": "Failed fetching release data for {{ tag }} project." + } + }, + "downloadGithubRepoZip": { + "fetching": "Fetching {{ releaseType }} with name {{ repoName }}...", + "fetchingName": "Fetching {{ name }}...", + "completed": "Completed project fetch.", + "errors": { + "fetchFail": "An error occurred fetching the project source." + } + }, + "cloneGithubRepo": { + "success": "Your new {{ type }} has been created in {{ dest }}" + }, + "downloadGithubRepoContents": { + "downloading": "Downloading content piece: {{ contentPiecePath }} from {{ downloadUrl }} to {{ downloadPath }}", + "errors": { + "fetchFail": "Failed to fetch contents: {{ errorMessage }}" + } + } + }, + "hubdb": { + "errors": { + "invalidJsonPath": "The HubDB table file must be a '.json' file", + "invalidJsonFile": "The '{{{ src }}' path is not a path to a file" + } + }, + "personalAccessKey": { + "errors": { + "accountNotFound": "Account with id {{ accountId }} does not exist.", + "invalidPersonalAccessKey": "Error while retrieving new access token: {{ errorMessage }}" + } + }, + "sandboxes": { + "errors": { + "createSandbox": "There was an error creating your sandbox.", + "deleteSandbox": "There was an error deleting your sandbox.", + "getSandboxUsageLimits": "There was an error fetching sandbox usage limits.", + "initiateSync": "There was an error initiating the sandbox sync.", + "fetchTaskStatus": "There was an error fetching the task status while syncing sandboxes.", + "fetchTypes": "There was an error fetching sandbox types." + } + }, + "cms": { + "modules": { + "createModule": { + "creatingModule": "Creating module at {{ path }}", + "creatingPath": "Creating {{ path }}", + "errors": { + "writeModuleMeta": "The {{ path }} path already exists" + } + } + }, + "functions": { + "updateExistingConfig": { + "unableToReadFile": "The file {{ configFilePath }} could not be read", + "invalidJSON": "The file {{ configFilePath }} is not valid JSON", + "couldNotUpdateFile": "The file {{ configFilePath }} could not be updated", + "errors": { + "configIsNotObjectError": "The existing {{ configFilePath }} is not an object", + "endpointAreadyExistsError": "The endpoint {{ endpointPath }} already exists in {{ configFilePath }}" + } + }, + "createFunction": { + "destPathAlreadyExists": "The {{ path }} path already exists", + "createdDest": "Created {{ path }}", + "failedToCreateFile": "The file {{ configFilePath }} could not be created", + "createdFunctionFile": "Created {{ path }}", + "createdConfigFile": "Created {{ path }}", + "success": "A function for the endpoint '/_hcms/api/{{ endpointPath }}' has been created. Upload {{ folderName }} to try it out", + "errors": { + "nestedConfigError": "Cannot create a functions directory inside '{{ ancestorConfigPath }}'", + "jsFileConflictError": "The JavaScript file at '{{ functionFilePath }}'' already exists" + } + } + }, + "handleFieldsJs": { + "convertFieldsJs": { + "creating": "Creating child process with pid {{ pid }}", + "terminating": "Child process with pid {{ pid }} has been terminated", + "errors": { + "errorConverting": "There was an error converting '{{ filePath }}'" + } + }, + "saveOutput": { + "errors": { + "saveFailed": "There was an error saving the json output of {{ path }}" + } + }, + "createTmpDirSync": { + "errors": { + "writeFailed": "An error occured writing temporary project source." + } + }, + "cleanupTmpDirSync": { + "errors": { + "deleteFailed": "There was an error deleting the temporary project source" + } + } + }, + "uploadFolder": { + "uploadFolder": { + "success": "Uploaded file \"{{ file}}\" to \"{{ destPath }}\"", + "attempt": "Attempting to upload file \"{{ file }}\" to \"{{ destPath }}\"", + "failed": "Uploading file \"{{ file }}\" to \"{{ destPath }}\" failed so scheduled retry", + "retry": "Retrying to upload file \"{{ file }}\" to \"{{ destPath }}\"", + "retryFailed": "Uploading file \"{{ file }}\" to \"{{ destPath }}\" failed" + } + }, + "templates": { + "createTemplate": { + "creatingFile": "Creating file at {{ path }}", + "creatingPath": "Making {{ path }} if needed", + "errors": { + "pathExists": "The {{ path }} path already exists" + } + } + }, + "processFieldsJs": { + "converting": "Converting \"{{ src }}\" to \"{{ dest }}\".", + "converted": "Finished converting \"{{ src }}\" to \"{{ dest }}\".", + "errors": { + "invalidMjsFile": ".mjs files are only supported when using Node 13.2.0+" + } + }, + "watch": { + "notifyOfThemePreview": "To preview this theme, visit: {{ previewUrl }}", + "skipUnsupportedExtension": "Skipping {{ file }} due to unsupported extension", + "skipIgnoreRule": "Skipping {{ file }} due to an ignore rule", + "uploadAttempt": "Attempting to upload file \"{{ file }}\" to \"{{ dest }}\"", + "uploadSuccess": "Uploaded file {{ file }} to {{ dest }}", + "uploadFailed": "Uploading file {{ file }} to {{ dest }} failed", + "uploadRetry": "Retrying to upload file \"{{ file }}\" to \"{{ dest }}\"", + "deleteAttempt": "Attempting to delete file {{ remoteFilePath }}", + "deleteAttemptWithType": "Attempting to delete {{ type }} {{ remoteFilePath }}", + "deleteSuccess": "Deleted file {{ remoteFilePath }}", + "deleteSuccessWithType": "Deleted {{ type }} {{ remoteFilePath }}", + "deleteFailed": "Deleting file {{ remoteFilePath }} failed", + "folderUploadSuccess": "Completed uploading files in {{ src }} to {{ dest }} in {{ accountId }}", + "ready": "Watcher is ready and watching {{ src }}. Any changes detected will be automatically uploaded and overwrite the current version in the developer file system." + } + }, + "logging": { + "git": { + "securityIssue": "Security Issue Detected", + "configFileTracked": "The HubSpot config file can be tracked by git.", + "fileName": "File: \"{{ configPath }}\"", + "remediate": "To remediate:", + "moveConfig": "- Move the config file to your home directory: '{{ homeDir }}'", + "addGitignore": "- Add gitignore pattern '{{ configPath }}' to a .gitignore file in root of your repository.", + "noRemote": "- Ensure that the config file has not already been pushed to a remote repository.", + "checkFailed": "Unable to determine if config file is properly ignored by git." + }, + "logs": { + "unableToProcessLog": "Unable to process log {{ log }}" + } + }, + "oauth": { + "writeTokenInfo": "Updating Oauth2 token info for portalId: {{ portalId }}", + "addOauthToAccountConfig": { + "init": "Updating configuration", + "success": "Configuration updated" + } + }, + "fileMapper": { + "skippedExisting": "Skipped existing {{ filepath }}", + "wroteFolder": "Wrote folder {{ filepath }}", + "completedFetch": "Completed fetch of file \"{{ src }}\"{{ version }} to \"{{ dest }}\" from the Design Manager", + "folderFetch": "Fetched \"{{ src }}\" from account {{ accountId }} from the Design Manager successfully", + "completedFolderFetch": "Completed fetch of folder \"{{ src }}\"{{ version }} to \"{{ dest }}\" from the Design Manager", + "errors": { + "invalidRequest": "Invalid request for file: {{ src }}", + "invalidNode": "Invalid FileMapperNode: {{ json }}", + "invalidFileType": "Invalid file type requested: {{ srcPath }}", + "assetTimeout": "HubSpot assets are unavailable at the moment. Please wait a few minutes and try again.", + "failedToFetchFile": "Failed fetch of file \"{{ src }}\" to \"{{ dest }}\" from the Design Manager", + "failedToFetchFolder": "Failed fetch of folder \"{{ src }}\" to \"{{ dest }}\" from the Design Manager", + "invalidFetchFolderRequest": "Invalid request for folder: \"{{ src }}\"", + "incompleteFetch": "Not all files in folder \"{{ src }}\" were successfully fetched. Re-run the last command to try again" + } + } + }, + "config": { + "cliConfiguration": { + "errors": { + "noConfigLoaded": "No config loaded." + }, + "load": { + "configFromEnv": "Loaded config from environment variables for {{ accountId }}", + "configFromFile": "Loaded config from configuration file.", + "empty": "The config file was empty. Initializing an empty config." + }, + "validate": { + "noConfig": "Valiation failed: No config was found.", + "noConfigAccounts": "Valiation failed: config.accounts[] is not defined.", + "emptyAccountConfig": "Valiation failed: config.accounts[] has an empty entry.", + "noAccountId": "Valiation failed: config.accounts[] has an entry missing accountId.", + "duplicateAccountIds": "Valiation failed: config.accounts[] has multiple entries with {{ accountId }}.", + "duplicateAccountNames": "Valiation failed: config.accounts[] has multiple entries with {{ accountName }}.", + "nameContainsSpaces": "Valiation failed: config.name {{ accountName }} cannot contain spaces." + }, + "updateAccount": { + "noConfigToUpdate": "No config to update.", + "updating": "Updating account config for {{ accountId }}", + "addingConfigEntry": "Adding account config entry for {{ accountId }}", + "errors": { + "accountIdRequired": "An accountId is required to update the config" + } + }, + "updateDefaultAccount": { + "errors": { + "invalidInput": "A 'defaultAccount' with value of number or string is required to update the config." + } + }, + "renameAccount": { + "errors": { + "invalidName": "Cannot find account with identifier {{ currentName }}" + } + }, + "removeAccountFromConfig": { + "deleting": "Deleting config for {{ accountId }}", + "errors": { + "invalidId": "Unable to find account for {{ nameOrId }}." + } + }, + "updateDefaultMode": { + "errors": { + "invalidMode": "The mode {{ defaultMode }} is invalid. Valid values are {{ validModes }}." + } + }, + "updateHttpTimeout": { + "errors": { + "invalidTimeout": "The value {{ timeout }} is invalid. The value must be a number greater than {{ minTimeout }}." + } + }, + "updateAllowUsageTracking": { + "errors": { + "invalidInput": "Unable to update allowUsageTracking. The value {{ isEnabled }} is invalid. The value must be a boolean." + } + } + }, + "configFile": { + "errorReading": "Config file could not be read: {{ configPath }}", + "writeSuccess": "Successfully wrote updated config data to {{ configPath }}", + "errorLoading": "A configuration file could not be found at {{ configPath }}.", + "errors": { + "parsing": "Config file could not be parsed" + } + }, + "configUtils": { + "unknownType": "Unknown auth type {{ type }}" + }, + "environment": { + "loadConfig": { + "missingAccountId": "Unable to load config from environment variables: Missing accountId", + "missingEnv": "Unable to load config from environment variables: Missing env", + "unknownAuthType": "Unable to load config from environment variables: Unknown auth type" + } + } + }, + "models": { + "OAuth2Manager": { + "fetchingAccessToken": "Fetching access token for accountId {{ accountId }} for clientId {{ clientId }}", + "updatingTokenInfo": "Persisting updated tokenInfo for accountId {{ accountId }} for clientId {{ clientId }}", + "refreshingAccessToken": "Waiting for access token for accountId {{ accountId }} for clientId {{ clientId }} to be fetched", + "errors": { + "missingRefreshToken": "The account {{ accountId }} has not been authenticated with Oauth2", + "auth": "Error while retrieving new token: {{ token }}" + } + } + }, + "utils": { + "notify": { + "errors": { + "filePath": "Unable to notify file '{{ filePath }}'" + } + }, + "cms": { + "modules": { + "throwInvalidPathInput": "Expected Path Input" + } + } + }, + "http": { + "index": { + "createGetRequestStream": { + "onWrite": "Wrote file {{ filepath }}" + }, + "errors": { + "withOauth": "Oauth manager for account {{ accountId }} not found.", + "withAuth": "Account with id {{ accountId }} not found." + } + } + }, + "errors": { + "fileSystemErrors": { + "readAction": "reading from", + "writeAction": "writing to", + "otherAction": "accessing", + "unknownFilepath": "a file or folder", + "baseMessage": "An error occurred while {{ fileAction }} {{ filepath }}." + }, + "apiErrors": { + "messageDetail": "{{ request }} in account {{ accountId }}", + "unableToUpload": "Unable to upload \"{{ payload }}.", + "codes": { + "400": "The {{ messageDetail }} was bad.", + "401": "The {{ messageDetail }} was unauthorized.", + "403": "The {{ messageDetail }} was forbidden.", + "404": "The {{ messageDetail }} was not found.", + "429": "The {{ messageDetail }} surpassed the rate limit. Retry in one minute.", + "503": "The {{ messageDetail }} could not be handled at this time. Please try again or visit https://help.hubspot.com/ to submit a ticket or contact HubSpot Support if the issue persists.", + "403MissingScope": "Couldn't run the project command because there are scopes missing in your production account. To update scopes, deactivate your current personal access key for {{ accountId }}, and generate a new one. Then run `hs auth` to update the CLI with the new key.", + "403Gating": "The current target account {{ accountId }} does not have access to HubSpot projects. To opt in to the CRM Development Beta and use projects, visit https://app.hubspot.com/l/whats-new/betas?productUpdateId=13860216.", + "404Request": "The {{ action }} failed because \"{{ request }}\" was not found in account {{ accountId }}.", + "500Generic": "The {{ messageDetail }} failed due to a server error. Please try again or visit https://help.hubspot.com/ to submit a ticket or contact HubSpot Support if the issue persists.", + "400Generic": "The {{ messageDetail }} failed due to a client error.", + "generic": "The {{ messageDetail }} failed." + } + }, + "generic": "A {{ name }} has occurred" + } + } +} diff --git a/package.json b/package.json index 0cd81741..83e50b2e 100644 --- a/package.json +++ b/package.json @@ -38,13 +38,13 @@ "@types/node": "^18.14.2", "@types/prettier": "^3.0.0", "@types/unixify": "^1.0.0", - "@typescript-eslint/eslint-plugin": "^5.54.0", - "@typescript-eslint/parser": "^5.59.7", + "@typescript-eslint/eslint-plugin": "^6.10.0", + "@typescript-eslint/parser": "^6.10.0", "eslint": "^8.35.0", "husky": "^8.0.0", "jest": "^29.5.0", "ts-jest": "^29.0.5", - "typescript": "^4.9.5" + "typescript": "^5.2.2" }, "exports": { "./*": "./lib/*.js", diff --git a/types/Lang.ts b/types/Lang.ts new file mode 100644 index 00000000..49314df8 --- /dev/null +++ b/types/Lang.ts @@ -0,0 +1,6 @@ +import lang from '../lang/en.json'; +import { Leaves } from './Utils'; + +type LanguageData = typeof lang.en; + +export type LangKey = Leaves; diff --git a/types/Utils.ts b/types/Utils.ts index 5f2cf2cf..a83e4c18 100644 --- a/types/Utils.ts +++ b/types/Utils.ts @@ -1 +1,13 @@ export type ValueOf = T[keyof T]; + +type Join = K extends string | number + ? P extends string | number + ? `${K}${'' extends P ? '' : '.'}${P}` + : never + : never; + +export type Leaves = [10] extends [never] + ? never + : T extends object + ? { [K in keyof T]-?: Join> }[keyof T] + : ''; From 4672e339bf601fc247e4bdfcb3a1361d546144f0 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Mon, 6 Nov 2023 17:09:14 -0500 Subject: [PATCH 3/9] typed lang keys for debug and throwErrorWithMessage --- errors/standardErrors.ts | 7 +- lang/en.json | 610 +++++++++++++++++++-------------------- types/Lang.ts | 8 +- utils/lang.ts | 44 ++- utils/logger.ts | 3 +- 5 files changed, 333 insertions(+), 339 deletions(-) diff --git a/errors/standardErrors.ts b/errors/standardErrors.ts index 11714d67..f88a335e 100644 --- a/errors/standardErrors.ts +++ b/errors/standardErrors.ts @@ -3,6 +3,7 @@ import { i18n } from '../utils/lang'; import { throwStatusCodeError } from './apiErrors'; import { BaseError, StatusCodeError } from '../types/Error'; +import { LangKey } from '../types/Lang'; export function isSystemError(err: BaseError): boolean { return err.errno != null && err.code != null && err.syscall != null; @@ -14,7 +15,7 @@ export function isFatalError(err: BaseError): boolean { function genericThrowErrorWithMessage( ErrorType: ErrorConstructor, - identifier: string, + identifier: LangKey, interpolation?: { [key: string]: string | number }, cause?: BaseError ): never { @@ -29,7 +30,7 @@ function genericThrowErrorWithMessage( * @throws */ export function throwErrorWithMessage( - identifier: string, + identifier: LangKey, interpolation?: { [key: string]: string | number }, cause?: BaseError ): never { @@ -40,7 +41,7 @@ export function throwErrorWithMessage( * @throws */ export function throwTypeErrorWithMessage( - identifier: string, + identifier: LangKey, interpolation?: { [key: string]: string | number }, cause?: BaseError ): never { diff --git a/lang/en.json b/lang/en.json index 15727e7c..82280f09 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1,365 +1,363 @@ { - "en": { - "lib": { - "trackUsage": { - "invalidEvent": "Usage tracking event {{ eventName }} is not a valid event type.", - "sendingEventAuthenticated": "Sending usage event to authenticated endpoint", - "sendingEventUnauthenticated": "Sending usage event to unauthenticated endpoint" - }, - "archive": { - "extractZip": { - "init": "Extracting project source...", - "success": "Completed project source extraction.", - "errors": { - "write": "An error occured writing temp project source.", - "extract": "An error occured extracting project source." - } - }, - "copySourceToDest": { - "init": "Copying project source...", - "sourceEmpty": "Project source is empty", - "success": "Completed copying project source.", - "error": "An error occured copying project source to {{ dest }}." - }, - "cleanupTempDir": { - "error": "Failed to clean up temp dir: {{ tmpDir }}" - } - }, - "gitignore": { + "lib": { + "trackUsage": { + "invalidEvent": "Usage tracking event {{ eventName }} is not a valid event type.", + "sendingEventAuthenticated": "Sending usage event to authenticated endpoint", + "sendingEventUnauthenticated": "Sending usage event to unauthenticated endpoint" + }, + "archive": { + "extractZip": { + "init": "Extracting project source...", + "success": "Completed project source extraction.", "errors": { - "configIgnore": "Unable to determine if config file is properly ignored by git." + "write": "An error occured writing temp project source.", + "extract": "An error occured extracting project source." } }, - "github": { - "fetchJsonFromRepository": { - "fetching": "Fetching {{ url }}...", - "errors": { - "fetchFail": "An error occured fetching JSON file." - } - }, - "fetchReleaseData": { - "errors": { - "fetchFail": "Failed fetching release data for {{ tag }} project." - } - }, - "downloadGithubRepoZip": { - "fetching": "Fetching {{ releaseType }} with name {{ repoName }}...", - "fetchingName": "Fetching {{ name }}...", - "completed": "Completed project fetch.", - "errors": { - "fetchFail": "An error occurred fetching the project source." - } - }, - "cloneGithubRepo": { - "success": "Your new {{ type }} has been created in {{ dest }}" - }, - "downloadGithubRepoContents": { - "downloading": "Downloading content piece: {{ contentPiecePath }} from {{ downloadUrl }} to {{ downloadPath }}", - "errors": { - "fetchFail": "Failed to fetch contents: {{ errorMessage }}" - } - } + "copySourceToDest": { + "init": "Copying project source...", + "sourceEmpty": "Project source is empty", + "success": "Completed copying project source.", + "error": "An error occured copying project source to {{ dest }}." }, - "hubdb": { + "cleanupTempDir": { + "error": "Failed to clean up temp dir: {{ tmpDir }}" + } + }, + "gitignore": { + "errors": { + "configIgnore": "Unable to determine if config file is properly ignored by git." + } + }, + "github": { + "fetchJsonFromRepository": { + "fetching": "Fetching {{ url }}...", "errors": { - "invalidJsonPath": "The HubDB table file must be a '.json' file", - "invalidJsonFile": "The '{{{ src }}' path is not a path to a file" + "fetchFail": "An error occured fetching JSON file." } }, - "personalAccessKey": { + "fetchReleaseData": { "errors": { - "accountNotFound": "Account with id {{ accountId }} does not exist.", - "invalidPersonalAccessKey": "Error while retrieving new access token: {{ errorMessage }}" + "fetchFail": "Failed fetching release data for {{ tag }} project." } }, - "sandboxes": { + "downloadGithubRepoZip": { + "fetching": "Fetching {{ releaseType }} with name {{ repoName }}...", + "fetchingName": "Fetching {{ name }}...", + "completed": "Completed project fetch.", "errors": { - "createSandbox": "There was an error creating your sandbox.", - "deleteSandbox": "There was an error deleting your sandbox.", - "getSandboxUsageLimits": "There was an error fetching sandbox usage limits.", - "initiateSync": "There was an error initiating the sandbox sync.", - "fetchTaskStatus": "There was an error fetching the task status while syncing sandboxes.", - "fetchTypes": "There was an error fetching sandbox types." - } - }, - "cms": { - "modules": { - "createModule": { - "creatingModule": "Creating module at {{ path }}", - "creatingPath": "Creating {{ path }}", - "errors": { - "writeModuleMeta": "The {{ path }} path already exists" - } - } - }, - "functions": { - "updateExistingConfig": { - "unableToReadFile": "The file {{ configFilePath }} could not be read", - "invalidJSON": "The file {{ configFilePath }} is not valid JSON", - "couldNotUpdateFile": "The file {{ configFilePath }} could not be updated", - "errors": { - "configIsNotObjectError": "The existing {{ configFilePath }} is not an object", - "endpointAreadyExistsError": "The endpoint {{ endpointPath }} already exists in {{ configFilePath }}" - } - }, - "createFunction": { - "destPathAlreadyExists": "The {{ path }} path already exists", - "createdDest": "Created {{ path }}", - "failedToCreateFile": "The file {{ configFilePath }} could not be created", - "createdFunctionFile": "Created {{ path }}", - "createdConfigFile": "Created {{ path }}", - "success": "A function for the endpoint '/_hcms/api/{{ endpointPath }}' has been created. Upload {{ folderName }} to try it out", - "errors": { - "nestedConfigError": "Cannot create a functions directory inside '{{ ancestorConfigPath }}'", - "jsFileConflictError": "The JavaScript file at '{{ functionFilePath }}'' already exists" - } - } - }, - "handleFieldsJs": { - "convertFieldsJs": { - "creating": "Creating child process with pid {{ pid }}", - "terminating": "Child process with pid {{ pid }} has been terminated", - "errors": { - "errorConverting": "There was an error converting '{{ filePath }}'" - } - }, - "saveOutput": { - "errors": { - "saveFailed": "There was an error saving the json output of {{ path }}" - } - }, - "createTmpDirSync": { - "errors": { - "writeFailed": "An error occured writing temporary project source." - } - }, - "cleanupTmpDirSync": { - "errors": { - "deleteFailed": "There was an error deleting the temporary project source" - } - } - }, - "uploadFolder": { - "uploadFolder": { - "success": "Uploaded file \"{{ file}}\" to \"{{ destPath }}\"", - "attempt": "Attempting to upload file \"{{ file }}\" to \"{{ destPath }}\"", - "failed": "Uploading file \"{{ file }}\" to \"{{ destPath }}\" failed so scheduled retry", - "retry": "Retrying to upload file \"{{ file }}\" to \"{{ destPath }}\"", - "retryFailed": "Uploading file \"{{ file }}\" to \"{{ destPath }}\" failed" - } - }, - "templates": { - "createTemplate": { - "creatingFile": "Creating file at {{ path }}", - "creatingPath": "Making {{ path }} if needed", - "errors": { - "pathExists": "The {{ path }} path already exists" - } - } - }, - "processFieldsJs": { - "converting": "Converting \"{{ src }}\" to \"{{ dest }}\".", - "converted": "Finished converting \"{{ src }}\" to \"{{ dest }}\".", - "errors": { - "invalidMjsFile": ".mjs files are only supported when using Node 13.2.0+" - } - }, - "watch": { - "notifyOfThemePreview": "To preview this theme, visit: {{ previewUrl }}", - "skipUnsupportedExtension": "Skipping {{ file }} due to unsupported extension", - "skipIgnoreRule": "Skipping {{ file }} due to an ignore rule", - "uploadAttempt": "Attempting to upload file \"{{ file }}\" to \"{{ dest }}\"", - "uploadSuccess": "Uploaded file {{ file }} to {{ dest }}", - "uploadFailed": "Uploading file {{ file }} to {{ dest }} failed", - "uploadRetry": "Retrying to upload file \"{{ file }}\" to \"{{ dest }}\"", - "deleteAttempt": "Attempting to delete file {{ remoteFilePath }}", - "deleteAttemptWithType": "Attempting to delete {{ type }} {{ remoteFilePath }}", - "deleteSuccess": "Deleted file {{ remoteFilePath }}", - "deleteSuccessWithType": "Deleted {{ type }} {{ remoteFilePath }}", - "deleteFailed": "Deleting file {{ remoteFilePath }} failed", - "folderUploadSuccess": "Completed uploading files in {{ src }} to {{ dest }} in {{ accountId }}", - "ready": "Watcher is ready and watching {{ src }}. Any changes detected will be automatically uploaded and overwrite the current version in the developer file system." - } - }, - "logging": { - "git": { - "securityIssue": "Security Issue Detected", - "configFileTracked": "The HubSpot config file can be tracked by git.", - "fileName": "File: \"{{ configPath }}\"", - "remediate": "To remediate:", - "moveConfig": "- Move the config file to your home directory: '{{ homeDir }}'", - "addGitignore": "- Add gitignore pattern '{{ configPath }}' to a .gitignore file in root of your repository.", - "noRemote": "- Ensure that the config file has not already been pushed to a remote repository.", - "checkFailed": "Unable to determine if config file is properly ignored by git." - }, - "logs": { - "unableToProcessLog": "Unable to process log {{ log }}" + "fetchFail": "An error occurred fetching the project source." } }, - "oauth": { - "writeTokenInfo": "Updating Oauth2 token info for portalId: {{ portalId }}", - "addOauthToAccountConfig": { - "init": "Updating configuration", - "success": "Configuration updated" - } + "cloneGithubRepo": { + "success": "Your new {{ type }} has been created in {{ dest }}" }, - "fileMapper": { - "skippedExisting": "Skipped existing {{ filepath }}", - "wroteFolder": "Wrote folder {{ filepath }}", - "completedFetch": "Completed fetch of file \"{{ src }}\"{{ version }} to \"{{ dest }}\" from the Design Manager", - "folderFetch": "Fetched \"{{ src }}\" from account {{ accountId }} from the Design Manager successfully", - "completedFolderFetch": "Completed fetch of folder \"{{ src }}\"{{ version }} to \"{{ dest }}\" from the Design Manager", + "downloadGithubRepoContents": { + "downloading": "Downloading content piece: {{ contentPiecePath }} from {{ downloadUrl }} to {{ downloadPath }}", "errors": { - "invalidRequest": "Invalid request for file: {{ src }}", - "invalidNode": "Invalid FileMapperNode: {{ json }}", - "invalidFileType": "Invalid file type requested: {{ srcPath }}", - "assetTimeout": "HubSpot assets are unavailable at the moment. Please wait a few minutes and try again.", - "failedToFetchFile": "Failed fetch of file \"{{ src }}\" to \"{{ dest }}\" from the Design Manager", - "failedToFetchFolder": "Failed fetch of folder \"{{ src }}\" to \"{{ dest }}\" from the Design Manager", - "invalidFetchFolderRequest": "Invalid request for folder: \"{{ src }}\"", - "incompleteFetch": "Not all files in folder \"{{ src }}\" were successfully fetched. Re-run the last command to try again" + "fetchFail": "Failed to fetch contents: {{ errorMessage }}" } } }, - "config": { - "cliConfiguration": { - "errors": { - "noConfigLoaded": "No config loaded." - }, - "load": { - "configFromEnv": "Loaded config from environment variables for {{ accountId }}", - "configFromFile": "Loaded config from configuration file.", - "empty": "The config file was empty. Initializing an empty config." - }, - "validate": { - "noConfig": "Valiation failed: No config was found.", - "noConfigAccounts": "Valiation failed: config.accounts[] is not defined.", - "emptyAccountConfig": "Valiation failed: config.accounts[] has an empty entry.", - "noAccountId": "Valiation failed: config.accounts[] has an entry missing accountId.", - "duplicateAccountIds": "Valiation failed: config.accounts[] has multiple entries with {{ accountId }}.", - "duplicateAccountNames": "Valiation failed: config.accounts[] has multiple entries with {{ accountName }}.", - "nameContainsSpaces": "Valiation failed: config.name {{ accountName }} cannot contain spaces." - }, - "updateAccount": { - "noConfigToUpdate": "No config to update.", - "updating": "Updating account config for {{ accountId }}", - "addingConfigEntry": "Adding account config entry for {{ accountId }}", + "hubdb": { + "errors": { + "invalidJsonPath": "The HubDB table file must be a '.json' file", + "invalidJsonFile": "The '{{{ src }}' path is not a path to a file" + } + }, + "personalAccessKey": { + "errors": { + "accountNotFound": "Account with id {{ accountId }} does not exist.", + "invalidPersonalAccessKey": "Error while retrieving new access token: {{ errorMessage }}" + } + }, + "sandboxes": { + "errors": { + "createSandbox": "There was an error creating your sandbox.", + "deleteSandbox": "There was an error deleting your sandbox.", + "getSandboxUsageLimits": "There was an error fetching sandbox usage limits.", + "initiateSync": "There was an error initiating the sandbox sync.", + "fetchTaskStatus": "There was an error fetching the task status while syncing sandboxes.", + "fetchTypes": "There was an error fetching sandbox types." + } + }, + "cms": { + "modules": { + "createModule": { + "creatingModule": "Creating module at {{ path }}", + "creatingPath": "Creating {{ path }}", "errors": { - "accountIdRequired": "An accountId is required to update the config" + "writeModuleMeta": "The {{ path }} path already exists" } - }, - "updateDefaultAccount": { + } + }, + "functions": { + "updateExistingConfig": { + "unableToReadFile": "The file {{ configFilePath }} could not be read", + "invalidJSON": "The file {{ configFilePath }} is not valid JSON", + "couldNotUpdateFile": "The file {{ configFilePath }} could not be updated", "errors": { - "invalidInput": "A 'defaultAccount' with value of number or string is required to update the config." + "configIsNotObjectError": "The existing {{ configFilePath }} is not an object", + "endpointAreadyExistsError": "The endpoint {{ endpointPath }} already exists in {{ configFilePath }}" } }, - "renameAccount": { + "createFunction": { + "destPathAlreadyExists": "The {{ path }} path already exists", + "createdDest": "Created {{ path }}", + "failedToCreateFile": "The file {{ configFilePath }} could not be created", + "createdFunctionFile": "Created {{ path }}", + "createdConfigFile": "Created {{ path }}", + "success": "A function for the endpoint '/_hcms/api/{{ endpointPath }}' has been created. Upload {{ folderName }} to try it out", "errors": { - "invalidName": "Cannot find account with identifier {{ currentName }}" + "nestedConfigError": "Cannot create a functions directory inside '{{ ancestorConfigPath }}'", + "jsFileConflictError": "The JavaScript file at '{{ functionFilePath }}'' already exists" } - }, - "removeAccountFromConfig": { - "deleting": "Deleting config for {{ accountId }}", + } + }, + "handleFieldsJs": { + "convertFieldsJs": { + "creating": "Creating child process with pid {{ pid }}", + "terminating": "Child process with pid {{ pid }} has been terminated", "errors": { - "invalidId": "Unable to find account for {{ nameOrId }}." + "errorConverting": "There was an error converting '{{ filePath }}'" } }, - "updateDefaultMode": { + "saveOutput": { "errors": { - "invalidMode": "The mode {{ defaultMode }} is invalid. Valid values are {{ validModes }}." + "saveFailed": "There was an error saving the json output of {{ path }}" } }, - "updateHttpTimeout": { + "createTmpDirSync": { "errors": { - "invalidTimeout": "The value {{ timeout }} is invalid. The value must be a number greater than {{ minTimeout }}." + "writeFailed": "An error occured writing temporary project source." } }, - "updateAllowUsageTracking": { + "cleanupTmpDirSync": { "errors": { - "invalidInput": "Unable to update allowUsageTracking. The value {{ isEnabled }} is invalid. The value must be a boolean." + "deleteFailed": "There was an error deleting the temporary project source" } } }, - "configFile": { - "errorReading": "Config file could not be read: {{ configPath }}", - "writeSuccess": "Successfully wrote updated config data to {{ configPath }}", - "errorLoading": "A configuration file could not be found at {{ configPath }}.", - "errors": { - "parsing": "Config file could not be parsed" + "uploadFolder": { + "uploadFolder": { + "success": "Uploaded file \"{{ file}}\" to \"{{ destPath }}\"", + "attempt": "Attempting to upload file \"{{ file }}\" to \"{{ destPath }}\"", + "failed": "Uploading file \"{{ file }}\" to \"{{ destPath }}\" failed so scheduled retry", + "retry": "Retrying to upload file \"{{ file }}\" to \"{{ destPath }}\"", + "retryFailed": "Uploading file \"{{ file }}\" to \"{{ destPath }}\" failed" } }, - "configUtils": { - "unknownType": "Unknown auth type {{ type }}" + "templates": { + "createTemplate": { + "creatingFile": "Creating file at {{ path }}", + "creatingPath": "Making {{ path }} if needed", + "errors": { + "pathExists": "The {{ path }} path already exists" + } + } }, - "environment": { - "loadConfig": { - "missingAccountId": "Unable to load config from environment variables: Missing accountId", - "missingEnv": "Unable to load config from environment variables: Missing env", - "unknownAuthType": "Unable to load config from environment variables: Unknown auth type" + "processFieldsJs": { + "converting": "Converting \"{{ src }}\" to \"{{ dest }}\".", + "converted": "Finished converting \"{{ src }}\" to \"{{ dest }}\".", + "errors": { + "invalidMjsFile": ".mjs files are only supported when using Node 13.2.0+" } + }, + "watch": { + "notifyOfThemePreview": "To preview this theme, visit: {{ previewUrl }}", + "skipUnsupportedExtension": "Skipping {{ file }} due to unsupported extension", + "skipIgnoreRule": "Skipping {{ file }} due to an ignore rule", + "uploadAttempt": "Attempting to upload file \"{{ file }}\" to \"{{ dest }}\"", + "uploadSuccess": "Uploaded file {{ file }} to {{ dest }}", + "uploadFailed": "Uploading file {{ file }} to {{ dest }} failed", + "uploadRetry": "Retrying to upload file \"{{ file }}\" to \"{{ dest }}\"", + "deleteAttempt": "Attempting to delete file {{ remoteFilePath }}", + "deleteAttemptWithType": "Attempting to delete {{ type }} {{ remoteFilePath }}", + "deleteSuccess": "Deleted file {{ remoteFilePath }}", + "deleteSuccessWithType": "Deleted {{ type }} {{ remoteFilePath }}", + "deleteFailed": "Deleting file {{ remoteFilePath }} failed", + "folderUploadSuccess": "Completed uploading files in {{ src }} to {{ dest }} in {{ accountId }}", + "ready": "Watcher is ready and watching {{ src }}. Any changes detected will be automatically uploaded and overwrite the current version in the developer file system." } }, - "models": { - "OAuth2Manager": { - "fetchingAccessToken": "Fetching access token for accountId {{ accountId }} for clientId {{ clientId }}", - "updatingTokenInfo": "Persisting updated tokenInfo for accountId {{ accountId }} for clientId {{ clientId }}", - "refreshingAccessToken": "Waiting for access token for accountId {{ accountId }} for clientId {{ clientId }} to be fetched", - "errors": { - "missingRefreshToken": "The account {{ accountId }} has not been authenticated with Oauth2", - "auth": "Error while retrieving new token: {{ token }}" - } + "logging": { + "git": { + "securityIssue": "Security Issue Detected", + "configFileTracked": "The HubSpot config file can be tracked by git.", + "fileName": "File: \"{{ configPath }}\"", + "remediate": "To remediate:", + "moveConfig": "- Move the config file to your home directory: '{{ homeDir }}'", + "addGitignore": "- Add gitignore pattern '{{ configPath }}' to a .gitignore file in root of your repository.", + "noRemote": "- Ensure that the config file has not already been pushed to a remote repository.", + "checkFailed": "Unable to determine if config file is properly ignored by git." + }, + "logs": { + "unableToProcessLog": "Unable to process log {{ log }}" + } + }, + "oauth": { + "writeTokenInfo": "Updating Oauth2 token info for portalId: {{ portalId }}", + "addOauthToAccountConfig": { + "init": "Updating configuration", + "success": "Configuration updated" } }, - "utils": { - "notify": { + "fileMapper": { + "skippedExisting": "Skipped existing {{ filepath }}", + "wroteFolder": "Wrote folder {{ filepath }}", + "completedFetch": "Completed fetch of file \"{{ src }}\"{{ version }} to \"{{ dest }}\" from the Design Manager", + "folderFetch": "Fetched \"{{ src }}\" from account {{ accountId }} from the Design Manager successfully", + "completedFolderFetch": "Completed fetch of folder \"{{ src }}\"{{ version }} to \"{{ dest }}\" from the Design Manager", + "errors": { + "invalidRequest": "Invalid request for file: {{ src }}", + "invalidNode": "Invalid FileMapperNode: {{ json }}", + "invalidFileType": "Invalid file type requested: {{ srcPath }}", + "assetTimeout": "HubSpot assets are unavailable at the moment. Please wait a few minutes and try again.", + "failedToFetchFile": "Failed fetch of file \"{{ src }}\" to \"{{ dest }}\" from the Design Manager", + "failedToFetchFolder": "Failed fetch of folder \"{{ src }}\" to \"{{ dest }}\" from the Design Manager", + "invalidFetchFolderRequest": "Invalid request for folder: \"{{ src }}\"", + "incompleteFetch": "Not all files in folder \"{{ src }}\" were successfully fetched. Re-run the last command to try again" + } + } + }, + "config": { + "cliConfiguration": { + "errors": { + "noConfigLoaded": "No config loaded." + }, + "load": { + "configFromEnv": "Loaded config from environment variables for {{ accountId }}", + "configFromFile": "Loaded config from configuration file.", + "empty": "The config file was empty. Initializing an empty config." + }, + "validate": { + "noConfig": "Valiation failed: No config was found.", + "noConfigAccounts": "Valiation failed: config.accounts[] is not defined.", + "emptyAccountConfig": "Valiation failed: config.accounts[] has an empty entry.", + "noAccountId": "Valiation failed: config.accounts[] has an entry missing accountId.", + "duplicateAccountIds": "Valiation failed: config.accounts[] has multiple entries with {{ accountId }}.", + "duplicateAccountNames": "Valiation failed: config.accounts[] has multiple entries with {{ accountName }}.", + "nameContainsSpaces": "Valiation failed: config.name {{ accountName }} cannot contain spaces." + }, + "updateAccount": { + "noConfigToUpdate": "No config to update.", + "updating": "Updating account config for {{ accountId }}", + "addingConfigEntry": "Adding account config entry for {{ accountId }}", "errors": { - "filePath": "Unable to notify file '{{ filePath }}'" + "accountIdRequired": "An accountId is required to update the config" } }, - "cms": { - "modules": { - "throwInvalidPathInput": "Expected Path Input" + "updateDefaultAccount": { + "errors": { + "invalidInput": "A 'defaultAccount' with value of number or string is required to update the config." } - } - }, - "http": { - "index": { - "createGetRequestStream": { - "onWrite": "Wrote file {{ filepath }}" - }, + }, + "renameAccount": { "errors": { - "withOauth": "Oauth manager for account {{ accountId }} not found.", - "withAuth": "Account with id {{ accountId }} not found." + "invalidName": "Cannot find account with identifier {{ currentName }}" + } + }, + "removeAccountFromConfig": { + "deleting": "Deleting config for {{ accountId }}", + "errors": { + "invalidId": "Unable to find account for {{ nameOrId }}." + } + }, + "updateDefaultMode": { + "errors": { + "invalidMode": "The mode {{ defaultMode }} is invalid. Valid values are {{ validModes }}." } - } - }, - "errors": { - "fileSystemErrors": { - "readAction": "reading from", - "writeAction": "writing to", - "otherAction": "accessing", - "unknownFilepath": "a file or folder", - "baseMessage": "An error occurred while {{ fileAction }} {{ filepath }}." }, - "apiErrors": { - "messageDetail": "{{ request }} in account {{ accountId }}", - "unableToUpload": "Unable to upload \"{{ payload }}.", - "codes": { - "400": "The {{ messageDetail }} was bad.", - "401": "The {{ messageDetail }} was unauthorized.", - "403": "The {{ messageDetail }} was forbidden.", - "404": "The {{ messageDetail }} was not found.", - "429": "The {{ messageDetail }} surpassed the rate limit. Retry in one minute.", - "503": "The {{ messageDetail }} could not be handled at this time. Please try again or visit https://help.hubspot.com/ to submit a ticket or contact HubSpot Support if the issue persists.", - "403MissingScope": "Couldn't run the project command because there are scopes missing in your production account. To update scopes, deactivate your current personal access key for {{ accountId }}, and generate a new one. Then run `hs auth` to update the CLI with the new key.", - "403Gating": "The current target account {{ accountId }} does not have access to HubSpot projects. To opt in to the CRM Development Beta and use projects, visit https://app.hubspot.com/l/whats-new/betas?productUpdateId=13860216.", - "404Request": "The {{ action }} failed because \"{{ request }}\" was not found in account {{ accountId }}.", - "500Generic": "The {{ messageDetail }} failed due to a server error. Please try again or visit https://help.hubspot.com/ to submit a ticket or contact HubSpot Support if the issue persists.", - "400Generic": "The {{ messageDetail }} failed due to a client error.", - "generic": "The {{ messageDetail }} failed." + "updateHttpTimeout": { + "errors": { + "invalidTimeout": "The value {{ timeout }} is invalid. The value must be a number greater than {{ minTimeout }}." + } + }, + "updateAllowUsageTracking": { + "errors": { + "invalidInput": "Unable to update allowUsageTracking. The value {{ isEnabled }} is invalid. The value must be a boolean." } + } + }, + "configFile": { + "errorReading": "Config file could not be read: {{ configPath }}", + "writeSuccess": "Successfully wrote updated config data to {{ configPath }}", + "errorLoading": "A configuration file could not be found at {{ configPath }}.", + "errors": { + "parsing": "Config file could not be parsed" + } + }, + "configUtils": { + "unknownType": "Unknown auth type {{ type }}" + }, + "environment": { + "loadConfig": { + "missingAccountId": "Unable to load config from environment variables: Missing accountId", + "missingEnv": "Unable to load config from environment variables: Missing env", + "unknownAuthType": "Unable to load config from environment variables: Unknown auth type" + } + } + }, + "models": { + "OAuth2Manager": { + "fetchingAccessToken": "Fetching access token for accountId {{ accountId }} for clientId {{ clientId }}", + "updatingTokenInfo": "Persisting updated tokenInfo for accountId {{ accountId }} for clientId {{ clientId }}", + "refreshingAccessToken": "Waiting for access token for accountId {{ accountId }} for clientId {{ clientId }} to be fetched", + "errors": { + "missingRefreshToken": "The account {{ accountId }} has not been authenticated with Oauth2", + "auth": "Error while retrieving new token: {{ token }}" + } + } + }, + "utils": { + "notify": { + "errors": { + "filePath": "Unable to notify file '{{ filePath }}'" + } + }, + "cms": { + "modules": { + "throwInvalidPathInput": "Expected Path Input" + } + } + }, + "http": { + "index": { + "createGetRequestStream": { + "onWrite": "Wrote file {{ filepath }}" }, - "generic": "A {{ name }} has occurred" + "errors": { + "withOauth": "Oauth manager for account {{ accountId }} not found.", + "withAuth": "Account with id {{ accountId }} not found." + } } + }, + "errors": { + "fileSystemErrors": { + "readAction": "reading from", + "writeAction": "writing to", + "otherAction": "accessing", + "unknownFilepath": "a file or folder", + "baseMessage": "An error occurred while {{ fileAction }} {{ filepath }}." + }, + "apiErrors": { + "messageDetail": "{{ request }} in account {{ accountId }}", + "unableToUpload": "Unable to upload \"{{ payload }}.", + "codes": { + "400": "The {{ messageDetail }} was bad.", + "401": "The {{ messageDetail }} was unauthorized.", + "403": "The {{ messageDetail }} was forbidden.", + "404": "The {{ messageDetail }} was not found.", + "429": "The {{ messageDetail }} surpassed the rate limit. Retry in one minute.", + "503": "The {{ messageDetail }} could not be handled at this time. Please try again or visit https://help.hubspot.com/ to submit a ticket or contact HubSpot Support if the issue persists.", + "403MissingScope": "Couldn't run the project command because there are scopes missing in your production account. To update scopes, deactivate your current personal access key for {{ accountId }}, and generate a new one. Then run `hs auth` to update the CLI with the new key.", + "403Gating": "The current target account {{ accountId }} does not have access to HubSpot projects. To opt in to the CRM Development Beta and use projects, visit https://app.hubspot.com/l/whats-new/betas?productUpdateId=13860216.", + "404Request": "The {{ action }} failed because \"{{ request }}\" was not found in account {{ accountId }}.", + "500Generic": "The {{ messageDetail }} failed due to a server error. Please try again or visit https://help.hubspot.com/ to submit a ticket or contact HubSpot Support if the issue persists.", + "400Generic": "The {{ messageDetail }} failed due to a client error.", + "generic": "The {{ messageDetail }} failed." + } + }, + "generic": "A {{ name }} has occurred" } } diff --git a/types/Lang.ts b/types/Lang.ts index 49314df8..251272d6 100644 --- a/types/Lang.ts +++ b/types/Lang.ts @@ -1,6 +1,10 @@ import lang from '../lang/en.json'; import { Leaves } from './Utils'; -type LanguageData = typeof lang.en; +export type GenericLanguageObject = { + [key: string]: string | GenericLanguageObject; +}; -export type LangKey = Leaves; +export type LanguageObject = typeof lang; + +export type LangKey = Leaves; diff --git a/utils/lang.ts b/utils/lang.ts index ac85b519..e601d654 100644 --- a/utils/lang.ts +++ b/utils/lang.ts @@ -1,42 +1,33 @@ -import { join } from 'path'; -import { existsSync, readFileSync } from 'fs-extra'; -import { load } from 'js-yaml'; +import en from '../lang/en.json'; +import { LanguageObject, GenericLanguageObject, LangKey } from '../types/Lang'; -const MISSING_LANGUAGE_DATA_PREFIX = '[Missing language data]'; - -type LanguageObject = { - [key: string]: LanguageObject | string; +const LANGUAGES: { [language: string]: LanguageObject } = { + en, }; -let locale = ''; -let languageObj: LanguageObject; +const MISSING_LANGUAGE_DATA_PREFIX = '[Missing language data]'; + +let languageObj: GenericLanguageObject | string; -function loadLanguageFromYaml(): void { +function loadLanguageForLocale(): void { if (languageObj) return; try { const nodeLocale = Intl.DateTimeFormat() .resolvedOptions() .locale.split('-')[0]; - const languageFilePath = join(__dirname, `../lang/${nodeLocale}.lyaml`); - const languageFileExists = existsSync(languageFilePath); - - // Fall back to using the default language file - locale = languageFileExists ? nodeLocale : 'en'; - languageObj = load( - readFileSync(join(__dirname, `../lang/${locale}.lyaml`), 'utf8') - ) as LanguageObject; + + languageObj = LANGUAGES[nodeLocale] || LANGUAGES.en; } catch (e) { throw new Error(`Error loading language data: ${e}`); } } -function getTextValue(lookupDotNotation: string): string { - const lookupProps = [locale, ...lookupDotNotation.split('.')]; - const missingTextData = `${MISSING_LANGUAGE_DATA_PREFIX}: ${lookupProps.join( - '.' - )}`; - let textValue = languageObj as LanguageObject | string; +function getTextValue(lookupDotNotation: LangKey): string { + const lookupProps = lookupDotNotation.split('.'); + const missingTextData = `${MISSING_LANGUAGE_DATA_PREFIX}: ${lookupDotNotation}`; + + let textValue = languageObj; let previouslyCheckedProp = lookupProps[0]; lookupProps.forEach(prop => { @@ -115,11 +106,11 @@ export function interpolate( } export function i18n( - lookupDotNotation: string, + lookupDotNotation: LangKey, options: { [identifier: string]: string | number } = {} ) { if (!languageObj) { - loadLanguageFromYaml(); + loadLanguageForLocale(); } if (typeof lookupDotNotation !== 'string') { @@ -135,6 +126,5 @@ export function i18n( } export const setLangData = (newLocale: string, newLangObj: LanguageObject) => { - locale = newLocale; languageObj = newLangObj; }; diff --git a/utils/logger.ts b/utils/logger.ts index 00d179c2..7d9ffd96 100644 --- a/utils/logger.ts +++ b/utils/logger.ts @@ -1,5 +1,6 @@ import { i18n } from './lang'; import { LogCallbacks } from '../types/LogCallbacks'; +import { LangKey } from '../types/Lang'; export function log( key: T, @@ -34,7 +35,7 @@ export function makeTypedLogger( } export function debug( - identifier: string, + identifier: LangKey, interpolation?: { [key: string]: string | number } ): void { console.debug(i18n(identifier, interpolation)); From d669385996b42b37c0e3ed97a55a53dbd5d3d4c9 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Tue, 7 Nov 2023 13:03:52 -0500 Subject: [PATCH 4/9] Refactor makeTypedLogger to support typed lang keys --- config/CLIConfiguration.ts | 36 ++++++++++++++++++++----------- http/index.ts | 10 ++++----- lib/cms/functions.ts | 44 +++++++++++++++++++++++--------------- lib/cms/modules.ts | 9 +++----- lib/cms/templates.ts | 9 ++++---- lib/cms/uploadFolder.ts | 12 +++++------ lib/cms/watch.ts | 36 ++++++++++++++++++++----------- lib/fileMapper.ts | 41 +++++++++++++---------------------- lib/github.ts | 10 ++++----- lib/oauth.ts | 10 ++++----- utils/logger.ts | 14 ++++-------- 11 files changed, 118 insertions(+), 113 deletions(-) diff --git a/config/CLIConfiguration.ts b/config/CLIConfiguration.ts index e0ec4d59..022a536c 100644 --- a/config/CLIConfiguration.ts +++ b/config/CLIConfiguration.ts @@ -127,10 +127,8 @@ class CLIConfiguration { validate( logCallbacks?: LogCallbacksArg ): boolean { - const validateLogger = makeTypedLogger( - logCallbacks, - `${i18nKey}.validate` - ); + const validateLogger = + makeTypedLogger(logCallbacks); if (!this.config) { validateLogger('noConfig'); @@ -154,22 +152,34 @@ class CLIConfiguration { return false; } if (accountIdsMap[accountConfig.accountId]) { - validateLogger('duplicateAccountIds', { - accountId: accountConfig.accountId, - }); + validateLogger( + 'duplicateAccountIds', + `${i18nKey}.validate.duplicateAccountIds`, + { + accountId: accountConfig.accountId, + } + ); return false; } if (accountConfig.name) { if (accountNamesMap[accountConfig.name]) { - validateLogger('duplicateAccountNames', { - accountName: accountConfig.name, - }); + validateLogger( + 'duplicateAccountNames', + `${i18nKey}.validate.duplicateAccountNames`, + { + accountName: accountConfig.name, + } + ); return false; } if (/\s+/.test(accountConfig.name)) { - validateLogger('nameContainsSpaces', { - accountName: accountConfig.name, - }); + validateLogger( + 'nameContainsSpaces', + `${i18nKey}.validate.nameContainsSpaces`, + { + accountName: accountConfig.name, + } + ); return false; } accountNamesMap[accountConfig.name] = true; diff --git a/http/index.ts b/http/index.ts index d9f3de97..9eeaad0f 100644 --- a/http/index.ts +++ b/http/index.ts @@ -173,10 +173,8 @@ function createGetRequestStream(contentType: string) { ): Promise => { const { query, ...rest } = options; const axiosConfig = addQueryParams(rest, query); - const logger = makeTypedLogger( - logCallbacks, - `${i18nKey}.createGetRequestStream` - ); + const logger = + makeTypedLogger(logCallbacks); // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve, reject) => { @@ -217,7 +215,9 @@ function createGetRequestStream(contentType: string) { reject(err); }); writeStream.on('close', async () => { - logger('onWrite', { filepath }); + logger('onWrite', `${i18nKey}.createGetRequestStream.onWrite`, { + filepath, + }); resolve(res); }); } else { diff --git a/lib/cms/functions.ts b/lib/cms/functions.ts index 378e3295..ba3b5092 100644 --- a/lib/cms/functions.ts +++ b/lib/cms/functions.ts @@ -156,10 +156,8 @@ export async function createFunction( options: FunctionOptions, logCallbacks?: LogCallbacksArg ): Promise { - const logger = makeTypedLogger( - logCallbacks, - `${i18nKey}.createFunction` - ); + const logger = + makeTypedLogger(logCallbacks); const { functionsFolder, filename, endpointPath, endpointMethod } = functionInfo; @@ -186,12 +184,16 @@ export async function createFunction( const destPath = path.join(dest, folderName); if (fs.existsSync(destPath)) { - logger('destPathAlreadyExists', { - path: destPath, - }); + logger( + 'destPathAlreadyExists', + `${i18nKey}.createFunction.destPathAlreadyExists`, + { + path: destPath, + } + ); } else { fs.mkdirp(destPath); - logger('createdDest', { + logger('createdDest', `${i18nKey}.createFunction.createdDest`, { path: destPath, }); } @@ -213,9 +215,13 @@ export async function createFunction( functionFilePath ); - logger('createdFunctionFile', { - path: functionFilePath, - }); + logger( + 'createdFunctionFile', + `${i18nKey}.createFunction.createdFunctionFile`, + { + path: functionFilePath, + } + ); if (fs.existsSync(configFilePath)) { updateExistingConfig(configFilePath, { @@ -224,10 +230,14 @@ export async function createFunction( functionFile, }); - logger('createdFunctionFile', { - path: functionFilePath, - }); - logger('success', { + logger( + 'createdFunctionFile', + `${i18nKey}.createFunction.createdFunctionFile`, + { + path: functionFilePath, + } + ); + logger('success', `${i18nKey}.createFunction.success`, { endpointPath: endpointPath, folderName, }); @@ -244,10 +254,10 @@ export async function createFunction( write: true, }); } - logger('createdConfigFile', { + logger('createdConfigFile', `${i18nKey}.createFunction.createdConfigFile`, { path: configFilePath, }); - logger('success', { + logger('success', `${i18nKey}.createFunction.success`, { endpointPath: endpointPath, folderName, }); diff --git a/lib/cms/modules.ts b/lib/cms/modules.ts index fe64f421..c8ce0e2b 100644 --- a/lib/cms/modules.ts +++ b/lib/cms/modules.ts @@ -119,10 +119,7 @@ export async function createModule( }, logCallbacks?: LogCallbacksArg ) { - const logger = makeTypedLogger( - logCallbacks, - `${i18nKey}.createModule` - ); + const logger = makeTypedLogger(logCallbacks); const writeModuleMeta = ( { contentTypes, moduleLabel, global }: ModuleDefinition, dest: string @@ -170,13 +167,13 @@ export async function createModule( path: destPath, }); } else { - logger('creatingPath', { + logger('creatingPath', `${i18nKey}.createModule.creatingPath`, { path: destPath, }); fs.ensureDirSync(destPath); } - logger('creatingModule', { + logger('creatingModule', `${i18nKey}.createModule.creatingModule`, { path: destPath, }); diff --git a/lib/cms/templates.ts b/lib/cms/templates.ts index 4d892448..01f5cae2 100644 --- a/lib/cms/templates.ts +++ b/lib/cms/templates.ts @@ -70,11 +70,10 @@ export async function createTemplate( debug(`${i18nKey}.createTemplate.creatingPath`, { path: dest }); fs.mkdirp(dest); - const logger = makeTypedLogger( - logCallbacks, - `${i18nKey}.createTemplate` - ); - logger('creatingFile', { path: filePath }); + const logger = makeTypedLogger(logCallbacks); + logger('creatingFile', `${i18nKey}.createTemplate.creatingFile`, { + path: filePath, + }); await downloadGithubRepoContents( 'HubSpot/cms-sample-assets', diff --git a/lib/cms/uploadFolder.ts b/lib/cms/uploadFolder.ts index 84888149..1bf09b30 100644 --- a/lib/cms/uploadFolder.ts +++ b/lib/cms/uploadFolder.ts @@ -129,10 +129,7 @@ export async function uploadFolder( mode: Mode | null = null, logCallbacks?: LogCallbacksArg ): Promise> { - const logger = makeTypedLogger( - logCallbacks, - `${i18nKey}.uploadFolder` - ); + const logger = makeTypedLogger(logCallbacks); const { saveOutput, convertFields } = commandOptions; const tmpDir = convertFields ? createTmpDirSync('hubspot-temp-fieldsjs-output-') @@ -177,7 +174,7 @@ export async function uploadFolder( }); try { await upload(accountId, file, destPath, apiOptions); - logger('success', { + logger('success', `${i18nKey}.uploadFolder.success`, { file: originalFilePath || '', destPath, }); @@ -212,7 +209,10 @@ export async function uploadFolder( debug(`${i18nKey}.uploadFolder.retry`, { file, destPath }); try { await upload(accountId, file, destPath, apiOptions); - logger('success', { file, destPath }); + logger('success', `${i18nKey}.uploadFolder.success`, { + file, + destPath, + }); return { resultType: FILE_UPLOAD_RESULT_TYPES.SUCCESS, error: null, diff --git a/lib/cms/watch.ts b/lib/cms/watch.ts index e8928c47..ff7be93b 100644 --- a/lib/cms/watch.ts +++ b/lib/cms/watch.ts @@ -42,12 +42,14 @@ function _notifyOfThemePreview( accountId: number, logCallbacks?: WatchLogCallbacks ): void { - const logger = makeLogger(logCallbacks, i18nKey); + const logger = makeLogger(logCallbacks); if (queue.size > 0) return; const previewUrl = getThemePreviewUrl(filePath, accountId); if (!previewUrl) return; - logger('notifyOfThemePreview', { previewUrl }); + logger('notifyOfThemePreview', `${i18nKey}.notifyOfThemePreview`, { + previewUrl, + }); } const notifyOfThemePreview = debounce(_notifyOfThemePreview, 1000); @@ -68,7 +70,7 @@ async function uploadFile( mode: Mode | null = null, logCallbacks?: WatchLogCallbacks ): Promise { - const logger = makeLogger(logCallbacks, i18nKey); + const logger = makeLogger(logCallbacks); const src = options.src; const absoluteSrcPath = path.resolve(getCwd(), file); @@ -112,7 +114,7 @@ async function uploadFile( queue.add(() => { return upload(accountId, fileToUpload, dest, apiOptions) .then(() => { - logger('uploadSuccess', { file, dest }); + logger('uploadSuccess', `${i18nKey}.uploadSuccess`, { file, dest }); notifyOfThemePreview(file, accountId, logCallbacks); }) .catch(() => { @@ -141,7 +143,7 @@ async function deleteRemoteFile( remoteFilePath: string, logCallbacks?: WatchLogCallbacks ): Promise { - const logger = makeLogger(logCallbacks, i18nKey); + const logger = makeLogger(logCallbacks); if (shouldIgnoreFile(filePath)) { debug(`${i18nKey}.skipIgnoreRule`, { file: filePath }); return; @@ -151,7 +153,7 @@ async function deleteRemoteFile( return queue.add(() => { return deleteFile(accountId, remoteFilePath) .then(() => { - logger('deleteSuccess', { remoteFilePath }); + logger('deleteSuccess', `${i18nKey}.deleteSuccess`, { remoteFilePath }); notifyOfThemePreview(filePath, accountId, logCallbacks); }) .catch((error: StatusCodeError) => { @@ -198,7 +200,7 @@ export function watch( onQueueAddError?: ErrorHandler, logCallbacks?: WatchLogCallbacks ) { - const logger = makeLogger(logCallbacks, i18nKey); + const logger = makeLogger(logCallbacks); const regex = new RegExp(`^${escapeRegExp(src)}`); if (notify) { ignoreFile(notify); @@ -225,7 +227,11 @@ export function watch( filePaths, mode || null ).then(result => { - logger('folderUploadSuccess', { src, dest, accountId }); + logger('folderUploadSuccess', `${i18nKey}.folderUploadSuccess`, { + src, + dest, + accountId, + }); if (postInitialUploadCallback) { postInitialUploadCallback(result); } @@ -237,7 +243,7 @@ export function watch( } watcher.on('ready', () => { - logger('ready', { src }); + logger('ready', `${i18nKey}.ready`, { src }); }); watcher.on('add', async (filePath: string) => { @@ -281,10 +287,14 @@ export function watch( remotePath, logCallbacks ).then(() => { - logger('deleteSuccessWithType', { - type, - remoteFilePath: remotePath, - }); + logger( + 'deleteSuccessWithType', + `${i18nKey}.deleteSuccessWithType`, + { + type, + remoteFilePath: remotePath, + } + ); }); if (onQueueAddError) { diff --git a/lib/fileMapper.ts b/lib/fileMapper.ts index 55e8026e..24579e07 100644 --- a/lib/fileMapper.ts +++ b/lib/fileMapper.ts @@ -194,15 +194,12 @@ async function fetchAndWriteFileStream( options: FileMapperInputOptions = {}, logCallbacks?: LogCallbacksArg ): Promise { - const logger = makeTypedLogger( - logCallbacks, - i18nKey - ); + const logger = makeTypedLogger(logCallbacks); if (typeof srcPath !== 'string' || !srcPath.trim()) { return; } if (await skipExisting(filepath, options.overwrite)) { - logger('skippedExisting', { filepath }); + logger('skippedExisting', `${i18nKey}.skippedExisting`, { filepath }); return; } if (!isAllowedExtension(srcPath)) { @@ -235,13 +232,12 @@ async function writeFileMapperNode( options: FileMapperInputOptions = {}, logCallbacks?: LogCallbacksArg ): Promise { - const logger = makeTypedLogger( - logCallbacks, - i18nKey - ); + const logger = makeTypedLogger(logCallbacks); const localFilepath = convertToLocalFileSystemPath(path.resolve(filepath)); if (await skipExisting(localFilepath, options.overwrite)) { - logger('skippedExisting', { filepath: localFilepath }); + logger('skippedExisting', `${i18nKey}.skippedExisting`, { + filepath: localFilepath, + }); return true; } if (!node.folder) { @@ -261,7 +257,9 @@ async function writeFileMapperNode( } try { await fs.ensureDir(localFilepath); - logger('wroteFolder', { filepath: localFilepath }); + logger('wroteFolder', `${i18nKey}.wroteFolder`, { + filepath: localFilepath, + }); } catch (err) { throwFileSystemError(err as BaseError, { filepath: localFilepath, @@ -285,10 +283,7 @@ async function downloadFile( options: FileMapperInputOptions = {}, logCallbacks?: LogCallbacksArg ): Promise { - const logger = makeTypedLogger( - logCallbacks, - i18nKey - ); + const logger = makeTypedLogger(logCallbacks); const { isFile, isHubspot } = getTypeDataFromPath(src); try { if (!isFile) { @@ -320,7 +315,7 @@ async function downloadFile( logCallbacks ); await queue.onIdle(); - logger('completedFetch', { + logger('completedFetch', `${i18nKey}.completedFetch`, { src, version: getAssetVersionIdentifier(options.assetVersion, src), dest, @@ -346,10 +341,7 @@ export async function fetchFolderFromApi( options: FileMapperInputOptions = {}, logCallbacks?: LogCallbacksArg ): Promise { - const logger = makeTypedLogger( - logCallbacks, - i18nKey - ); + const logger = makeTypedLogger(logCallbacks); const { isRoot, isFolder, isHubspot } = getTypeDataFromPath(src); if (!isFolder) { throwErrorWithMessage(`${i18nKey}.errors.invalidFetchFolderRequest`, { @@ -362,7 +354,7 @@ export async function fetchFolderFromApi( const node = isHubspot ? await downloadDefault(accountId, srcPath, queryValues) : await download(accountId, srcPath, queryValues); - logger('folderFetch', { src, accountId }); + logger('folderFetch', `${i18nKey}.folderFetch`, { src, accountId }); return node; } catch (err) { const error = err as StatusCodeError; @@ -385,10 +377,7 @@ async function downloadFolder( options: FileMapperInputOptions = {}, logCallbacks?: LogCallbacksArg ) { - const logger = makeTypedLogger( - logCallbacks, - i18nKey - ); + const logger = makeTypedLogger(logCallbacks); try { const node = await fetchFolderFromApi( accountId, @@ -429,7 +418,7 @@ async function downloadFolder( await queue.onIdle(); if (success) { - logger('completedFolderFetch', { + logger('completedFolderFetch', `${i18nKey}.completedFolderFetch`, { src, version: getAssetVersionIdentifier(options.assetVersion, src), dest, diff --git a/lib/github.ts b/lib/github.ts index f45ea267..35b2fe92 100644 --- a/lib/github.ts +++ b/lib/github.ts @@ -34,7 +34,7 @@ export async function fetchJsonFromRepository( ): Promise { try { const URL = `https://raw.githubusercontent.com/${repoPath}/${ref}/${filePath}`; - debug(`${i18nKey}.fetchJsonFromRepository`, { url: URL }); + debug(`${i18nKey}.fetchJsonFromRepository.fetching`, { url: URL }); const { data } = await axios.get(URL, { headers: { ...DEFAULT_USER_AGENT_HEADERS, ...GITHUB_AUTH_HEADERS }, @@ -130,10 +130,8 @@ export async function cloneGithubRepo( options: CloneGithubRepoOptions = {}, logCallbacks?: LogCallbacksArg ): Promise { - const logger = makeTypedLogger( - logCallbacks, - `${i18nKey}.cloneGithubRepo` - ); + const logger = + makeTypedLogger(logCallbacks); const { themeVersion, projectVersion, releaseType, ref } = options; const tag = projectVersion || themeVersion; const zip = await downloadGithubRepoZip(repoPath, tag, releaseType, ref); @@ -141,7 +139,7 @@ export async function cloneGithubRepo( const success = await extractZipArchive(zip, repoName, dest, { sourceDir }); if (success) { - logger('success', { type, dest }); + logger('success', `${i18nKey}.cloneGithubRepo.success`, { type, dest }); } return success; } diff --git a/lib/oauth.ts b/lib/oauth.ts index ee112fbd..e1464843 100644 --- a/lib/oauth.ts +++ b/lib/oauth.ts @@ -43,18 +43,16 @@ export function addOauthToAccountConfig( oauth: OAuth2Manager, logCallbacks: LogCallbacksArg ) { - const logger = makeTypedLogger( - logCallbacks, - `${i18nKey}.addOauthToAccountConfig` - ); - logger('init'); + const logger = + makeTypedLogger(logCallbacks); + logger('init', `${i18nKey}.addOauthToAccountConfig.init`); try { updateAccountConfig({ ...oauth.toObj(), authType: AUTH_METHODS.oauth.value, }); writeConfig(); - logger('success'); + logger('success', `${i18nKey}.addOauthToAccountConfig.success`); } catch (err) { throwError(err as BaseError); } diff --git a/utils/logger.ts b/utils/logger.ts index 7d9ffd96..c09d80e5 100644 --- a/utils/logger.ts +++ b/utils/logger.ts @@ -5,7 +5,7 @@ import { LangKey } from '../types/Lang'; export function log( key: T, callbacks?: LogCallbacks, - debugKey?: string, + debugKey?: LangKey, debugInterpolation?: { [key: string]: string | number } ): void { if (callbacks && callbacks[key]) { @@ -17,21 +17,15 @@ export function log( } export function makeTypedLogger( - callbacks?: LogCallbacks, - debugKey?: string + callbacks?: LogCallbacks ) { type ValidateCallbackKeys = T[number]; return ( key: ValidateCallbackKeys, + debugKey?: LangKey, debugInterpolation?: { [key: string]: string | number } - ) => - log( - key, - callbacks, - `${debugKey}.${key}`, - debugInterpolation - ); + ) => log(key, callbacks, debugKey, debugInterpolation); } export function debug( From 0e9734b5154e04fa3c1b463f11ccd5afa15c13d7 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Tue, 7 Nov 2023 13:13:18 -0500 Subject: [PATCH 5/9] Fix lang key type errors --- lang/en.json | 4 +++- lib/cms/processFieldsJs.ts | 11 ++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lang/en.json b/lang/en.json index 82280f09..f35f4856 100644 --- a/lang/en.json +++ b/lang/en.json @@ -160,7 +160,9 @@ "converting": "Converting \"{{ src }}\" to \"{{ dest }}\".", "converted": "Finished converting \"{{ src }}\" to \"{{ dest }}\".", "errors": { - "invalidMjsFile": ".mjs files are only supported when using Node 13.2.0+" + "invalidMjsFile": ".mjs files are only supported when using Node 13.2.0+", + "notFunction": "There was an error loading JS file \"{{ path }}\". Expected type \"Function\" but received type \"{{ returned }}\". Make sure that your default export is a function.", + "notArray": "There was an error loading JS file \"{{ path }}\". Expected type \"Array\" but received type \"{{ returned }}\" . Make sure that your function returns an array" } }, "watch": { diff --git a/lib/cms/processFieldsJs.ts b/lib/cms/processFieldsJs.ts index fdee3404..1ac9505a 100644 --- a/lib/cms/processFieldsJs.ts +++ b/lib/cms/processFieldsJs.ts @@ -14,11 +14,6 @@ const i18nKey = 'lib.cms.processFieldsJs'; const { dirName, fieldOptions, filePath, writeDir } = process.env; const baseName = path.basename(filePath!); -const FieldErrors = { - IsNotFunction: 'IsNotFunction', - DoesNotReturnArray: 'DoesNotReturnArray', -}; - //TODO - Figure out agnostic logging console.info( i18n(`${i18nKey}.converting`, { @@ -39,14 +34,16 @@ const fieldsPromise = dynamicImport(filePath!).catch(e => throwError(e)); fieldsPromise.then(fieldsFunc => { const fieldsFuncType = typeof fieldsFunc; if (fieldsFuncType !== 'function') { - throwErrorWithMessage(`${i18nKey}.${FieldErrors.IsNotFunction}`, { + throwErrorWithMessage(`${i18nKey}.errors.notFunction`, { path: filePath!, + returned: fieldsFuncType, }); } return Promise.resolve(fieldsFunc(fieldOptions)).then(fields => { if (!Array.isArray(fields)) { - throwErrorWithMessage(`${i18nKey}.${FieldErrors.DoesNotReturnArray}`, { + throwErrorWithMessage(`${i18nKey}.errors.notArray`, { path: filePath!, + returned: typeof fields, }); } From ec15e4f932d21e7da1f32548ae49afbff7878115 Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Tue, 7 Nov 2023 13:13:37 -0500 Subject: [PATCH 6/9] Delete en.yaml --- lang/en.lyaml | 264 -------------------------------------------------- 1 file changed, 264 deletions(-) delete mode 100644 lang/en.lyaml diff --git a/lang/en.lyaml b/lang/en.lyaml deleted file mode 100644 index 7ef15269..00000000 --- a/lang/en.lyaml +++ /dev/null @@ -1,264 +0,0 @@ -en: - lib: - trackUsage: - invalidEvent: "Usage tracking event {{ eventName }} is not a valid event type." - sendingEventAuthenticated: "Sending usage event to authenticated endpoint" - sendingEventUnauthenticated: "Sending usage event to unauthenticated endpoint" - archive: - extractZip: - init: "Extracting project source..." - success: "Completed project source extraction." - errors: - write: "An error occured writing temp project source." - extract: "An error occured extracting project source." - copySourceToDest: - init: "Copying project source..." - sourceEmpty: "Project source is empty" - success: "Completed copying project source." - error: "An error occured copying project source to {{ dest }}." - cleanupTempDir: - error: "Failed to clean up temp dir: {{ tmpDir }}" - gitignore: - errors: - configIgnore: "Unable to determine if config file is properly ignored by git." - github: - fetchJsonFromRepository: - fetching: "Fetching {{ url }}..." - errors: - fetchFail: "An error occured fetching JSON file." - fetchReleaseData: - errors: - fetchFail: "Failed fetching release data for {{ tag }} project." - downloadGithubRepoZip: - fetching: "Fetching {{ releaseType }} with name {{ repoName }}..." - fetchingName: "Fetching {{ name }}..." - completed: "Completed project fetch." - errors: - fetchFail: "An error occurred fetching the project source." - cloneGithubRepo: - success: "Your new {{ type }} has been created in {{ dest }}" - downloadGithubRepoContents: - downloading: "Downloading content piece: {{ contentPiecePath }} from {{ downloadUrl }} to {{ downloadPath }}" - errors: - fetchFail: "Failed to fetch contents: {{ errorMessage }}" - hubdb: - errors: - invalidJsonPath: "The HubDB table file must be a '.json' file" - invalidJsonFile: "The '{{{ src }}' path is not a path to a file" - personalAccessKey: - errors: - accountNotFound: "Account with id {{ accountId }} does not exist." - invalidPersonalAccessKey: "Error while retrieving new access token: {{ errorMessage }}" - sandboxes: - errors: - createSandbox: "There was an error creating your sandbox." - deleteSandbox: "There was an error deleting your sandbox." - getSandboxUsageLimits: "There was an error fetching sandbox usage limits." - initiateSync: "There was an error initiating the sandbox sync." - fetchTaskStatus: "There was an error fetching the task status while syncing sandboxes." - fetchTypes: "There was an error fetching sandbox types." - cms: - modules: - createModule: - creatingModule: "Creating module at {{ path }}" - creatingPath: "Creating {{ path }}" - errors: - writeModuleMeta: "The {{ path }} path already exists" - functions: - updateExistingConfig: - unableToReadFile: "The file {{ configFilePath }} could not be read" - invalidJSON: "The file {{ configFilePath }} is not valid JSON" - couldNotUpdateFile: "The file {{ configFilePath }} could not be updated" - errors: - configIsNotObjectError: "The existing {{ configFilePath }} is not an object" - endpointAreadyExistsError: "The endpoint {{ endpointPath }} already exists in {{ configFilePath }}" - createFunction: - destPathAlreadyExists: "The {{ path }} path already exists" - createdDest: "Created {{ path }}" - failedToCreateFile: "The file {{ configFilePath }} could not be created" - createdFunctionFile: "Created {{ path }}" - createdConfigFile: "Created {{ path }}" - success: "A function for the endpoint '/_hcms/api/{{ endpointPath }}' has been created. Upload {{ folderName }} to try it out" - errors: - nestedConfigError: "Cannot create a functions directory inside '{{ ancestorConfigPath }}'" - jsFileConflictError: "The JavaScript file at '{{ functionFilePath }}'' already exists" - handleFieldsJs: - convertFieldsJs: - creating: "Creating child process with pid {{ pid }}" - terminating: "Child process with pid {{ pid }} has been terminated" - errors: - errorConverting: "There was an error converting '{{ filePath }}'" - saveOutput: - errors: - saveFailed: "There was an error saving the json output of {{ path }}" - createTmpDirSync: - errors: - writeFailed: "An error occured writing temporary project source." - cleanupTmpDirSync: - errors: - deleteFailed: "There was an error deleting the temporary project source" - uploadFolder: - uploadFolder: - success: 'Uploaded file "{{ file}}" to "{{ destPath }}"' - attempt: 'Attempting to upload file "{{ file }}" to "{{ destPath }}"' - failed: 'Uploading file "{{ file }}" to "{{ destPath }}" failed so scheduled retry' - retry: 'Retrying to upload file "{{ file }}" to "{{ destPath }}"' - retryFailed: 'Uploading file "{{ file }}" to "{{ destPath }}" failed' - templates: - createTemplate: - creatingFile: "Creating file at {{ path }}" - creatingPath: "Making {{ path }} if needed" - errors: - pathExists: "The {{ path }} path already exists" - processFieldsJs: - converting: 'Converting "{{ src }}" to "{{ dest }}".' - converted: 'Finished converting "{{ src }}" to "{{ dest }}".' - errors: - invalidMjsFile: ".mjs files are only supported when using Node 13.2.0+" - watch: - notifyOfThemePreview: "To preview this theme, visit: {{ previewUrl }}" - skipUnsupportedExtension: "Skipping {{ file }} due to unsupported extension" - skipIgnoreRule: "Skipping {{ file }} due to an ignore rule" - uploadAttempt: 'Attempting to upload file "{{ file }}" to "{{ dest }}"' - uploadSuccess: "Uploaded file {{ file }} to {{ dest }}" - uploadFailed: "Uploading file {{ file }} to {{ dest }} failed" - uploadRetry: 'Retrying to upload file "{{ file }}" to "{{ dest }}"' - deleteAttempt: "Attempting to delete file {{ remoteFilePath }}" - deleteAttemptWithType: "Attempting to delete {{ type }} {{ remoteFilePath }}" - deleteSuccess: "Deleted file {{ remoteFilePath }}" - deleteSuccessWithType: "Deleted {{ type }} {{ remoteFilePath }}" - deleteFailed: "Deleting file {{ remoteFilePath }} failed" - folderUploadSuccess: "Completed uploading files in {{ src }} to {{ dest }} in {{ accountId }}" - ready: "Watcher is ready and watching {{ src }}. Any changes detected will be automatically uploaded and overwrite the current version in the developer file system." - logging: - git: - securityIssue: "Security Issue Detected" - configFileTracked: "The HubSpot config file can be tracked by git." - fileName: 'File: "{{ configPath }}"' - remediate: "To remediate:" - moveConfig: "- Move the config file to your home directory: '{{ homeDir }}'" - addGitignore: "- Add gitignore pattern '{{ configPath }}' to a .gitignore file in root of your repository." - noRemote: "- Ensure that the config file has not already been pushed to a remote repository." - checkFailed: "Unable to determine if config file is properly ignored by git." - logs: - unableToProcessLog: "Unable to process log {{ log }}" - oauth: - writeTokenInfo: "Updating Oauth2 token info for portalId: {{ portalId }}" - addOauthToAccountConfig: - init: "Updating configuration" - success: "Configuration updated" - fileMapper: - skippedExisting: "Skipped existing {{ filepath }}" - wroteFolder: "Wrote folder {{ filepath }}" - completedFetch: 'Completed fetch of file "{{ src }}"{{ version }} to "{{ dest }}" from the Design Manager' - folderFetch: 'Fetched "{{ src }}" from account {{ accountId }} from the Design Manager successfully' - completedFolderFetch: 'Completed fetch of folder "{{ src }}"{{ version }} to "{{ dest }}" from the Design Manager' - errors: - invalidRequest: "Invalid request for file: {{ src }}" - invalidNode: "Invalid FileMapperNode: {{ json }}" - invalidFileType: "Invalid file type requested: {{ srcPath }}" - assetTimeout: "HubSpot assets are unavailable at the moment. Please wait a few minutes and try again." - failedToFetchFile: 'Failed fetch of file "{{ src }}" to "{{ dest }}" from the Design Manager' - failedToFetchFolder: 'Failed fetch of folder "{{ src }}" to "{{ dest }}" from the Design Manager' - invalidFetchFolderRequest: 'Invalid request for folder: "{{ src }}"' - incompleteFetch: 'Not all files in folder "{{ src }}" were successfully fetched. Re-run the last command to try again' - config: - cliConfiguration: - errors: - noConfigLoaded: "No config loaded." - load: - configFromEnv: "Loaded config from environment variables for {{ accountId }}" - configFromFile: "Loaded config from configuration file." - empty: "The config file was empty. Initializing an empty config." - validate: - noConfig: "Valiation failed: No config was found." - noConfigAccounts: "Valiation failed: config.accounts[] is not defined." - emptyAccountConfig: "Valiation failed: config.accounts[] has an empty entry." - noAccountId: "Valiation failed: config.accounts[] has an entry missing accountId." - duplicateAccountIds: "Valiation failed: config.accounts[] has multiple entries with {{ accountId }}." - duplicateAccountNames: "Valiation failed: config.accounts[] has multiple entries with {{ accountName }}." - nameContainsSpaces: "Valiation failed: config.name {{ accountName }} cannot contain spaces." - updateAccount: - noConfigToUpdate: "No config to update." - updating: "Updating account config for {{ accountId }}" - addingConfigEntry: "Adding account config entry for {{ accountId }}" - errors: - accountIdRequired: "An accountId is required to update the config" - updateDefaultAccount: - errors: - invalidInput: "A 'defaultAccount' with value of number or string is required to update the config." - renameAccount: - errors: - invalidName: "Cannot find account with identifier {{ currentName }}" - removeAccountFromConfig: - deleting: "Deleting config for {{ accountId }}" - errors: - invalidId: "Unable to find account for {{ nameOrId }}." - updateDefaultMode: - errors: - invalidMode: "The mode {{ defaultMode }} is invalid. Valid values are {{ validModes }}." - updateHttpTimeout: - errors: - invalidTimeout: "The value {{ timeout }} is invalid. The value must be a number greater than {{ minTimeout }}." - updateAllowUsageTracking: - errors: - invalidInput: "Unable to update allowUsageTracking. The value {{ isEnabled }} is invalid. The value must be a boolean." - configFile: - errorReading: "Config file could not be read: {{ configPath }}" - writeSuccess: "Successfully wrote updated config data to {{ configPath }}" - errorLoading: "A configuration file could not be found at {{ configPath }}." - errors: - parsing: "Config file could not be parsed" - configUtils: - unknownType: "Unknown auth type {{ type }}" - environment: - loadConfig: - missingAccountId: "Unable to load config from environment variables: Missing accountId" - missingEnv: "Unable to load config from environment variables: Missing env" - unknownAuthType: "Unable to load config from environment variables: Unknown auth type" - models: - OAuth2Manager: - fetchingAccessToken: "Fetching access token for accountId {{ accountId }} for clientId {{ clientId }}" - updatingTokenInfo: "Persisting updated tokenInfo for accountId {{ accountId }} for clientId {{ clientId }}" - refreshingAccessToken: "Waiting for access token for accountId {{ accountId }} for clientId {{ clientId }} to be fetched" - errors: - missingRefreshToken: "The account {{ accountId }} has not been authenticated with Oauth2" - auth: "Error while retrieving new token: {{ token }}" - utils: - notify: - errors: - filePath: "Unable to notify file '{{ filePath }}'" - cms: - modules: - throwInvalidPathInput: "Expected Path Input" - http: - index: - createGetRequestStream: - onWrite: "Wrote file {{ filepath }}" - errors: - withOauth: "Oauth manager for account {{ accountId }} not found." - withAuth: "Account with id {{ accountId }} not found." - errors: - fileSystemErrors: - readAction: "reading from" - writeAction: "writing to" - otherAction: "accessing" - unknownFilepath: "a file or folder" - baseMessage: "An error occurred while {{ fileAction }} {{ filepath }}." - apiErrors: - messageDetail: "{{ request }} in account {{ accountId }}" - unableToUpload: 'Unable to upload "{{ payload }}.' - codes: - 400: "The {{ messageDetail }} was bad." - 401: "The {{ messageDetail }} was unauthorized." - 403MissingScope: "Couldn't run the project command because there are scopes missing in your production account. To update scopes, deactivate your current personal access key for {{ accountId }}, and generate a new one. Then run `hs auth` to update the CLI with the new key." - 403Gating: "The current target account {{ accountId }} does not have access to HubSpot projects. To opt in to the CRM Development Beta and use projects, visit https://app.hubspot.com/l/whats-new/betas?productUpdateId=13860216." - 403: "The {{ messageDetail }} was forbidden." - 404Request: 'The {{ action }} failed because "{{ request }}" was not found in account {{ accountId }}.' - 404: "The {{ messageDetail }} was not found." - 429: "The {{ messageDetail }} surpassed the rate limit. Retry in one minute." - 503: "The {{ messageDetail }} could not be handled at this time. Please try again or visit https://help.hubspot.com/ to submit a ticket or contact HubSpot Support if the issue persists." - 500Generic: "The {{ messageDetail }} failed due to a server error. Please try again or visit https://help.hubspot.com/ to submit a ticket or contact HubSpot Support if the issue persists." - 400Generic: "The {{ messageDetail }} failed due to a client error." - generic: "The {{ messageDetail }} failed." - generic: "A {{ name }} has occurred" From ffa10644e778caf9a2f5a9edd09e52c483cef9bd Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Tue, 7 Nov 2023 13:16:24 -0500 Subject: [PATCH 7/9] Revert TS version bump --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 83e50b2e..0cd81741 100644 --- a/package.json +++ b/package.json @@ -38,13 +38,13 @@ "@types/node": "^18.14.2", "@types/prettier": "^3.0.0", "@types/unixify": "^1.0.0", - "@typescript-eslint/eslint-plugin": "^6.10.0", - "@typescript-eslint/parser": "^6.10.0", + "@typescript-eslint/eslint-plugin": "^5.54.0", + "@typescript-eslint/parser": "^5.59.7", "eslint": "^8.35.0", "husky": "^8.0.0", "jest": "^29.5.0", "ts-jest": "^29.0.5", - "typescript": "^5.2.2" + "typescript": "^4.9.5" }, "exports": { "./*": "./lib/*.js", From 408568e44539fd864a0a29f2ea609a041d2bf03e Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Tue, 7 Nov 2023 14:53:57 -0500 Subject: [PATCH 8/9] use logger to debug --- utils/logger.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utils/logger.ts b/utils/logger.ts index c09d80e5..48f58320 100644 --- a/utils/logger.ts +++ b/utils/logger.ts @@ -1,4 +1,5 @@ import { i18n } from './lang'; +import { logger } from '../lib/logging/logger'; import { LogCallbacks } from '../types/LogCallbacks'; import { LangKey } from '../types/Lang'; @@ -32,5 +33,5 @@ export function debug( identifier: LangKey, interpolation?: { [key: string]: string | number } ): void { - console.debug(i18n(identifier, interpolation)); + logger.debug(i18n(identifier, interpolation)); } From 1808ca26ab3e2e7378811d8944cafe32e253472d Mon Sep 17 00:00:00 2001 From: Camden Phalen Date: Tue, 7 Nov 2023 14:58:07 -0500 Subject: [PATCH 9/9] Fix type errors in tests --- errors/__tests__/standardErrors.ts | 12 +++++++++--- errors/standardErrors.ts | 2 +- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/errors/__tests__/standardErrors.ts b/errors/__tests__/standardErrors.ts index e18852d0..179479b8 100644 --- a/errors/__tests__/standardErrors.ts +++ b/errors/__tests__/standardErrors.ts @@ -54,21 +54,27 @@ describe('standardErrors', () => { describe('throwErrorWithMessage', () => { it('throws error with message', () => { const error = newError(); - expect(() => throwErrorWithMessage('', {}, error)).toThrow(); + expect(() => + throwErrorWithMessage('errors.generic', {}, error) + ).toThrow(); }); }); describe('throwTypeErrorWithMessage', () => { it('throws type error with message', () => { const error = newError(); - expect(() => throwTypeErrorWithMessage('', {}, error)).toThrow(); + expect(() => + throwTypeErrorWithMessage('errors.generic', {}, error) + ).toThrow(); }); }); describe('throwAuthErrorWithMessage', () => { it('throws auth error with message', () => { const error = newError() as StatusCodeError; - expect(() => throwAuthErrorWithMessage('', {}, error)).toThrow(); + expect(() => + throwAuthErrorWithMessage('errors.generic', {}, error) + ).toThrow(); }); }); diff --git a/errors/standardErrors.ts b/errors/standardErrors.ts index f88a335e..a32df121 100644 --- a/errors/standardErrors.ts +++ b/errors/standardErrors.ts @@ -52,7 +52,7 @@ export function throwTypeErrorWithMessage( * @throws */ export function throwAuthErrorWithMessage( - identifier: string, + identifier: LangKey, interpolation?: { [key: string]: string | number }, cause?: StatusCodeError ): never {