diff --git a/packages/cli/commands/theme/preview.js b/packages/cli/commands/theme/preview.js index e5eea629f..58afee978 100644 --- a/packages/cli/commands/theme/preview.js +++ b/packages/cli/commands/theme/preview.js @@ -12,19 +12,28 @@ const { preview } = require('@hubspot/theme-preview-dev-server'); const { getUploadableFileList } = require('../../lib/upload'); const { trackCommandUsage } = require('../../lib/usageTracking'); const { loadAndValidateOptions } = require('../../lib/validation'); -const { previewPrompt } = require('../../lib/prompts/previewPrompt'); +const { + previewPrompt, + previewProjectPrompt, +} = require('../../lib/prompts/previewPrompt'); const { EXIT_CODES } = require('../../lib/enums/exitCodes'); const { FILE_UPLOAD_RESULT_TYPES, } = require('@hubspot/local-dev-lib/constants/files'); -const i18nKey = 'cli.commands.preview'; const cliProgress = require('cli-progress'); const { ApiErrorContext, logApiUploadErrorInstance, } = require('../../lib/errorHandlers/apiErrors'); const { handleExit, handleKeypress } = require('../../lib/process'); +const { getThemeJSONPath } = require('@hubspot/local-dev-lib/cms/themes'); +const { getProjectConfig } = require('../../lib/projects'); +const { + findProjectComponents, + COMPONENT_TYPES, +} = require('../../lib/projectStructure'); +const i18nKey = 'cli.commands.preview'; exports.command = 'preview [--src] [--dest]'; exports.describe = false; // i18n(`${i18nKey}.describe`) - Hiding command @@ -63,6 +72,42 @@ const handleUserInput = () => { }); }; +const determineSrcAndDest = async options => { + let absoluteSrc; + let dest; + const { projectDir, projectConfig } = await getProjectConfig(); + if (!(projectDir && projectConfig)) { + // Not in a project, prompt for src and dest of traditional theme + const previewPromptAnswers = await previewPrompt(options); + const src = options.src || previewPromptAnswers.src; + dest = options.dest || previewPromptAnswers.dest; + absoluteSrc = path.resolve(getCwd(), src); + if (!dest || !validateSrcPath(absoluteSrc)) { + process.exit(EXIT_CODES.ERROR); + } + } else { + // In a project + let themeJsonPath = getThemeJSONPath(); + if (!themeJsonPath) { + const projectComponents = await findProjectComponents(projectDir); + const themeComponents = projectComponents.filter( + c => c.type === COMPONENT_TYPES.hublTheme + ); + if (themeComponents.length === 0) { + logger.error(i18n(`${i18nKey}.errors.noThemeComponents`)); + process.exit(EXIT_CODES.ERROR); + } + const answer = await previewProjectPrompt(themeComponents); + themeJsonPath = `${answer.themeComponentPath}/theme.json`; + } + const { dir: themeDir } = path.parse(themeJsonPath); + absoluteSrc = themeDir; + const { base: themeName } = path.parse(themeDir); + dest = `@projects/${projectConfig.name}/${themeName}`; + } + return { absoluteSrc, dest }; +}; + exports.handler = async options => { const { notify, skipUpload, noSsl, port, debug } = options; @@ -70,18 +115,7 @@ exports.handler = async options => { const accountId = getAccountId(options); - const previewPromptAnswers = await previewPrompt(options); - const src = options.src || previewPromptAnswers.src; - let dest = options.dest || previewPromptAnswers.dest; - if (!dest) { - logger.error(i18n(`${i18nKey}.errors.destinationRequired`)); - return; - } - - const absoluteSrc = path.resolve(getCwd(), src); - if (!validateSrcPath(absoluteSrc)) { - process.exit(EXIT_CODES.ERROR); - } + const { absoluteSrc, dest } = await determineSrcAndDest(options); const filePaths = await getUploadableFileList(absoluteSrc, false); diff --git a/packages/cli/lang/en.lyaml b/packages/cli/lang/en.lyaml index cdc1492d5..2ec53e384 100644 --- a/packages/cli/lang/en.lyaml +++ b/packages/cli/lang/en.lyaml @@ -882,6 +882,7 @@ en: describe: "Upload and watch a theme directory on your computer for changes and start a local development server to preview theme changes on a site" errors: invalidPath: "The path \"{{ path }}\" is not a path to a directory" + noThemeComponents: "Your project has no theme components available to preview." options: src: describe: "Path to the local directory your theme is in, relative to your current working directory" @@ -1222,6 +1223,7 @@ en: previewPrompt: enterSrc: "[--src] Enter a local theme directory to preview." enterDest: "[--dest] Enter the destination path for the src theme in HubSpot Design Tools." + themeProjectSelect: "[--theme] Select which theme to preview." errors: srcRequired: "You must specify a source directory." destRequired: "You must specify a destination directory." diff --git a/packages/cli/lib/projectStructure.js b/packages/cli/lib/projectStructure.js index 1ba16017d..c206ff02a 100644 --- a/packages/cli/lib/projectStructure.js +++ b/packages/cli/lib/projectStructure.js @@ -7,11 +7,13 @@ const { logErrorInstance } = require('./errorHandlers/standardErrors'); const COMPONENT_TYPES = Object.freeze({ privateApp: 'private-app', publicApp: 'public-app', + hublTheme: 'hubl-theme', }); const CONFIG_FILES = { [COMPONENT_TYPES.privateApp]: 'app.json', [COMPONENT_TYPES.publicApp]: 'public-app.json', + [COMPONENT_TYPES.hublTheme]: 'theme.json', }; function getTypeFromConfigFile(configFile) { @@ -102,13 +104,14 @@ async function findProjectComponents(projectSourceDir) { if (Object.values(CONFIG_FILES).includes(base)) { const parsedAppConfig = loadConfigFile(projectFile); - if (parsedAppConfig && parsedAppConfig.name) { + if (parsedAppConfig) { const isLegacy = getIsLegacyApp(parsedAppConfig, dir); + const isHublTheme = base === CONFIG_FILES[COMPONENT_TYPES.hublTheme]; components.push({ type: getTypeFromConfigFile(base), config: parsedAppConfig, - runnable: !isLegacy, + runnable: !isLegacy && !isHublTheme, path: dir, }); } diff --git a/packages/cli/lib/prompts/previewPrompt.js b/packages/cli/lib/prompts/previewPrompt.js index 0865906f2..3741cb1f0 100644 --- a/packages/cli/lib/prompts/previewPrompt.js +++ b/packages/cli/lib/prompts/previewPrompt.js @@ -34,6 +34,24 @@ const previewPrompt = (promptOptions = {}) => { ]); }; +const previewProjectPrompt = async themeComponents => { + return promptUser([ + { + name: 'themeComponentPath', + message: i18n(`${i18nKey}.themeProjectSelect`), + type: 'list', + choices: themeComponents.map(t => { + const themeName = path.basename(t.path); + return { + name: themeName, + value: t.path, + }; + }), + }, + ]); +}; + module.exports = { previewPrompt, + previewProjectPrompt, };