From e2617bd7383ded6340c4fedebb635b175a25b5ec Mon Sep 17 00:00:00 2001 From: Tomek M Date: Wed, 16 Aug 2023 16:45:19 -0700 Subject: [PATCH] Tidied up template commands (#76) * Tidied up template commands - named functions - reviewed `any` - reviewed custom types - removed global (module) state from the push command --- package-lock.json | 32 ++ package.json | 1 + src/commands/templates/preview.ts | 96 ++-- src/commands/templates/pull.ts | 147 +++---- src/commands/templates/push.ts | 437 ++++++++----------- src/types/Template.ts | 81 +--- src/utils.ts | 118 ++--- test/unit/commands/templates/helpers.test.ts | 11 +- 8 files changed, 406 insertions(+), 517 deletions(-) diff --git a/package-lock.json b/package-lock.json index 62f9e2b..f655bde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "socket.io": "^2.4.1", "table": "^6.8.0", "traverse": "^0.6.6", + "ts-invariant": "^0.10.3", "untildify": "^4.0.0", "watch": "^0.13.0", "yargonaut": "^1.1.4", @@ -4472,6 +4473,22 @@ "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" }, + "node_modules/ts-invariant": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", + "integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ts-invariant/node_modules/tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" + }, "node_modules/ts-node": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.0.3.tgz", @@ -8223,6 +8240,21 @@ "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" }, + "ts-invariant": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.10.3.tgz", + "integrity": "sha512-uivwYcQaxAucv1CzRp2n/QdYPo4ILf9VXgH19zEIjFx2EJufV16P0JtJVpYHy89DItG6Kwj2oIUjrcK5au+4tQ==", + "requires": { + "tslib": "^2.1.0" + }, + "dependencies": { + "tslib": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz", + "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==" + } + } + }, "ts-node": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-8.0.3.tgz", diff --git a/package.json b/package.json index 2c23f37..7edf819 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "socket.io": "^2.4.1", "table": "^6.8.0", "traverse": "^0.6.6", + "ts-invariant": "^0.10.3", "untildify": "^4.0.0", "watch": "^0.13.0", "yargonaut": "^1.1.4", diff --git a/src/commands/templates/preview.ts b/src/commands/templates/preview.ts index 28ca8a8..4a4b800 100644 --- a/src/commands/templates/preview.ts +++ b/src/commands/templates/preview.ts @@ -1,16 +1,17 @@ import chalk from 'chalk' -import { existsSync } from 'fs-extra' -import { filter, find, replace, debounce } from 'lodash' +import path from 'path' import untildify from 'untildify' import express from 'express' -import { createMonitor } from 'watch' import consolidate from 'consolidate' +import { filter, find, replace, debounce } from 'lodash' +import { createMonitor } from 'watch' import { ServerClient } from 'postmark' -import { createManifest } from './helpers' -import { TemplatePreviewArguments } from '../../types' import { TemplateValidationOptions } from 'postmark/dist/client/models' -import { log, validateToken } from '../../utils' -import path from 'path' + +import { fatalError, log, validateToken } from '../../utils' + +import { validatePushDirectory } from './push' +import { createManifest } from './helpers' const previewPath = path.join(__dirname, 'preview') const templateLinks = '' @@ -29,39 +30,29 @@ export const builder = { alias: 'p', }, } -export const handler = (args: TemplatePreviewArguments) => exec(args) -/** - * Execute the command - */ -const exec = (args: TemplatePreviewArguments) => { - const { serverToken } = args - - return validateToken(serverToken).then(token => { - validateDirectory(token, args) - }) +interface TemplatePreviewArguments { + serverToken: string + templatesdirectory: string + port: number } +export async function handler(args: TemplatePreviewArguments): Promise { + const serverToken = await validateToken(args.serverToken) -const validateDirectory = ( - serverToken: string, - args: TemplatePreviewArguments -) => { - const { templatesdirectory } = args - const rootPath: string = untildify(templatesdirectory) - - // Check if path exists - if (!existsSync(rootPath)) { - log('The provided path does not exist', { error: true }) - return process.exit(1) + try { + validatePushDirectory(args.templatesdirectory) + } catch (e) { + return fatalError(e) } return preview(serverToken, args) } + /** * Preview */ -const preview = (serverToken: string, args: TemplatePreviewArguments) => { +async function preview(serverToken: string, args: TemplatePreviewArguments): Promise { const { port, templatesdirectory } = args log(`${title} Starting template preview server...`) @@ -77,7 +68,7 @@ const preview = (serverToken: string, args: TemplatePreviewArguments) => { // Static assets app.use(express.static(`${previewPath}/assets`)) - const updateEvent = () => { + function updateEvent() { // Generate new manifest manifest = createManifest(templatesdirectory) @@ -187,11 +178,9 @@ const preview = (serverToken: string, args: TemplatePreviewArguments) => { log(divider) }) - const validateTemplateRequest = ( - version: 'html' | 'text', + function validateTemplateRequest(version: 'html' | 'text', payload: TemplateValidationOptions, - res: express.Response - ) => { + res: express.Response) { const versionKey = version === 'html' ? 'HtmlBody' : 'TextBody' // Make request to Postmark @@ -199,8 +188,7 @@ const preview = (serverToken: string, args: TemplatePreviewArguments) => { .validateTemplate(payload) .then(result => { if (result[versionKey].ContentIsValid) { - const renderedContent = - result[versionKey].RenderedContent + templateLinks + const renderedContent = result[versionKey].RenderedContent + templateLinks io.emit('subject', { ...result.Subject, rawSubject: payload.Subject }) // Render raw source if HTML @@ -220,8 +208,9 @@ const preview = (serverToken: string, args: TemplatePreviewArguments) => { } } -const combineTemplate = (layout: string, template: string): string => - replace(layout, /({{{)(.?@content.?)(}}})/g, template) +function combineTemplate(layout: string, template: string): string { + return replace(layout, /({{{)(.?@content.?)(}}})/g, template) +} /* Console helpers */ @@ -230,7 +219,7 @@ const divider = chalk.gray('-'.repeat(34)) /* Render Templates */ -const getSource = (version: 'html' | 'text', template: any, layout?: any) => { +function getSource(version: 'html' | 'text', template: any, layout?: any) { const versionKey = version === 'html' ? 'HtmlBody' : 'TextBody' if (layout) return combineTemplate(layout[versionKey], template[versionKey]) @@ -238,28 +227,31 @@ const getSource = (version: 'html' | 'text', template: any, layout?: any) => { return template[versionKey] } -const renderTemplateText = (res: express.Response, body: string) => - consolidate.ejs(`${previewPath}/templateText.ejs`, { body }, (err, html) => - renderTemplateContents(res, err, html) +function renderTemplateText(res: express.Response, body: string) { + return consolidate.ejs( + `${previewPath}/templateText.ejs`, + { body }, + (err, html) => renderTemplateContents(res, err, html) ) +} -const renderTemplateInvalid = (res: express.Response, errors: any) => - consolidate.ejs( +function renderTemplateInvalid(res: express.Response, errors: any) { + return consolidate.ejs( `${previewPath}/templateInvalid.ejs`, { errors }, (err, html) => renderTemplateContents(res, err, html) ) +} -const renderTemplate404 = (res: express.Response, version: string) => - consolidate.ejs(`${previewPath}/template404.ejs`, { version }, (err, html) => - renderTemplateContents(res, err, html) +function renderTemplate404(res: express.Response, version: string) { + return consolidate.ejs( + `${previewPath}/template404.ejs`, + { version }, + (err, html) => renderTemplateContents(res, err, html) ) +} -const renderTemplateContents = ( - res: express.Response, - err: any, - html: string -) => { +function renderTemplateContents(res: express.Response, err: any, html: string) { if (err) return res.send(err) return res.send(html) diff --git a/src/commands/templates/pull.ts b/src/commands/templates/pull.ts index 836fcd7..c8d6cf3 100644 --- a/src/commands/templates/pull.ts +++ b/src/commands/templates/pull.ts @@ -1,18 +1,14 @@ +import ora from 'ora' +import untildify from 'untildify' +import invariant from 'ts-invariant' import { join } from 'path' import { outputFileSync, existsSync, ensureDirSync } from 'fs-extra' import { prompt } from 'inquirer' -import ora from 'ora' -import untildify from 'untildify' import { ServerClient } from 'postmark' +import type { Template, Templates } from 'postmark/dist/client/models' -import { - ProcessTemplatesOptions, - Template, - TemplateListOptions, - TemplatePullArguments, - MetaFile, -} from '../../types' -import { log, validateToken, pluralize } from '../../utils' +import { MetaFile } from '../../types' +import { log, validateToken, pluralize, logError, fatalError } from '../../utils' export const command = 'pull [options]' export const desc = 'Pull templates from a server to ' @@ -32,23 +28,22 @@ export const builder = { describe: 'Overwrite templates if they already exist', }, } -export const handler = (args: TemplatePullArguments) => exec(args) - -/** - * Execute the command - */ -const exec = (args: TemplatePullArguments) => { - const { serverToken } = args - return validateToken(serverToken).then(token => { - pull(token, args) - }) +interface TemplatePullArguments { + serverToken: string + requestHost: string + outputdirectory: string + overwrite: boolean +} +export async function handler(args: TemplatePullArguments): Promise { + const serverToken = await validateToken(args.serverToken) + pull(serverToken, args) } /** * Begin pulling the templates */ -const pull = (serverToken: string, args: TemplatePullArguments) => { +async function pull(serverToken: string, args: TemplatePullArguments): Promise { const { outputdirectory, overwrite, requestHost } = args // Check if directory exists @@ -66,33 +61,34 @@ const pull = (serverToken: string, args: TemplatePullArguments) => { /** * Ask user to confirm overwrite */ -const overwritePrompt = ( - serverToken: string, - outputdirectory: string, - requestHost: string -) => { - return prompt([ +async function overwritePrompt(serverToken: string, outputdirectory: string, requestHost: string): Promise { + const answer = await prompt<{overwrite: boolean}>([ { type: 'confirm', name: 'overwrite', default: false, message: `Overwrite the files in ${outputdirectory}?`, }, - ]).then((answer: any) => { - if (answer.overwrite) { - return fetchTemplateList({ - sourceServer: serverToken, - outputDir: outputdirectory, - requestHost: requestHost, - }) - } - }) + ]) + if (answer.overwrite) { + return fetchTemplateList({ + sourceServer: serverToken, + outputDir: outputdirectory, + requestHost: requestHost, + }) + } } + +interface TemplateListOptions { + sourceServer: string + requestHost: string + outputDir: string +} /** * Fetch template list from PM */ -const fetchTemplateList = (options: TemplateListOptions) => { +async function fetchTemplateList(options: TemplateListOptions) { const { sourceServer, outputDir, requestHost } = options const spinner = ora('Pulling templates from Postmark...').start() const client = new ServerClient(sourceServer) @@ -100,35 +96,32 @@ const fetchTemplateList = (options: TemplateListOptions) => { client.setClientOptions({ requestHost }) } - client - .getTemplates({ count: 300 }) - .then(response => { - if (response.TotalCount === 0) { - spinner.stop() - log('There are no templates on this server.', { error: true }) - process.exit(1) - } else { - processTemplates({ - spinner, - client, - outputDir: outputDir, - totalCount: response.TotalCount, - templates: response.Templates, - }) - } - }) - .catch((error: any) => { + try { + const templates = await client.getTemplates({ count: 300 }) + + if (templates.TotalCount === 0) { spinner.stop() - log(error, { error: true }) - process.exit(1) - }) + return fatalError('There are no templates on this server.') + } else { + await processTemplates({ spinner, client, outputDir, templates }) + } + } catch (err) { + spinner.stop() + return fatalError(err) + } } +interface ProcessTemplatesOptions { + spinner: ora.Ora + client: ServerClient + outputDir: string + templates: Templates +} /** * Fetch each template’s content from the server */ -const processTemplates = async (options: ProcessTemplatesOptions) => { - const { spinner, client, outputDir, totalCount, templates } = options +async function processTemplates(options: ProcessTemplatesOptions) { + const { spinner, client, outputDir, templates } = options // Keep track of requests let requestCount = 0 @@ -137,7 +130,7 @@ const processTemplates = async (options: ProcessTemplatesOptions) => { let totalDownloaded = 0 // Iterate through each template and fetch content - for (const template of templates) { + for (const template of templates.Templates) { spinner.text = `Downloading template: ${template.Alias || template.Name}` // Show warning if template doesn't have an alias @@ -149,7 +142,7 @@ const processTemplates = async (options: ProcessTemplatesOptions) => { ) // If this is the last template - if (requestCount === totalCount) spinner.stop() + if (requestCount === templates.TotalCount) spinner.stop() return } @@ -159,11 +152,11 @@ const processTemplates = async (options: ProcessTemplatesOptions) => { requestCount++ // Save template to file system - saveTemplate(outputDir, response, client) + await saveTemplate(outputDir, response, client) totalDownloaded++ // Show feedback when finished saving templates - if (requestCount === totalCount) { + if (requestCount === templates.TotalCount) { spinner.stop() log( @@ -175,9 +168,9 @@ const processTemplates = async (options: ProcessTemplatesOptions) => { { color: 'green' } ) } - } catch (e: any) { + } catch (e) { spinner.stop() - log(e, { error: true }) + logError(e) } } } @@ -186,9 +179,11 @@ const processTemplates = async (options: ProcessTemplatesOptions) => { * Save template * @return An object containing the HTML and Text body */ -const saveTemplate = (outputDir: string, template: Template, client: any) => { - outputDir = - template.TemplateType === 'Layout' ? join(outputDir, '_layouts') : outputDir +async function saveTemplate(outputDir: string, template: Template, client: ServerClient) { + invariant(typeof template.Alias === 'string' && !!template.Alias, 'Template must have an alias') + + outputDir = template.TemplateType === 'Layout' ? join(outputDir, '_layouts') : outputDir + const path: string = untildify(join(outputDir, template.Alias)) ensureDirSync(path) @@ -203,29 +198,29 @@ const saveTemplate = (outputDir: string, template: Template, client: any) => { outputFileSync(join(path, 'content.txt'), template.TextBody) } - let meta: MetaFile = { + const meta: MetaFile = { Name: template.Name, Alias: template.Alias, ...(template.Subject && { Subject: template.Subject }), TemplateType: template.TemplateType, ...(template.TemplateType === 'Standard' && { - LayoutTemplate: template.LayoutTemplate, + LayoutTemplate: template.LayoutTemplate || undefined, }), } // Save suggested template model - client + return client .validateTemplate({ ...(template.HtmlBody && { HtmlBody: template.HtmlBody }), ...(template.TextBody && { TextBody: template.TextBody }), ...meta, }) - .then((result: any) => { + .then((result) => { meta.TestRenderModel = result.SuggestedTemplateModel }) - .catch((error: any) => { - log('Error fetching suggested template model', { error: true }) - log(error, { error: true }) + .catch((error) => { + logError('Error fetching suggested template model') + logError(error) }) .then(() => { // Save the file regardless of success or error when fetching suggested model diff --git a/src/commands/templates/push.ts b/src/commands/templates/push.ts index 8e77635..64686ba 100644 --- a/src/commands/templates/push.ts +++ b/src/commands/templates/push.ts @@ -1,26 +1,21 @@ import chalk from 'chalk' import ora from 'ora' +import untildify from 'untildify' +import invariant from 'ts-invariant' +import { existsSync, statSync } from 'fs-extra' import { find } from 'lodash' import { prompt } from 'inquirer' import { table, getBorderCharacters } from 'table' -import untildify from 'untildify' -import { existsSync } from 'fs-extra' import { ServerClient } from 'postmark' -import { Templates } from 'postmark/dist/client/models' -import { - TemplateManifest, - TemplatePushResults, - TemplatePushReview, - TemplatePushArguments, - ProcessTemplates, -} from '../../types' -import { pluralize, log, validateToken } from '../../utils' +import { TemplateTypes, Templates } from 'postmark/dist/client/models' + +import { TemplateManifest } from '../../types' +import { pluralize, log, validateToken, fatalError, logError } from '../../utils' + import { createManifest, sameContent, templatesDiff } from './helpers' const debug = require('debug')('postmark-cli:templates:push'); -let pushManifest: TemplateManifest[] = [] - export const command = 'push [options]' export const desc = 'Push templates from to a Postmark server' @@ -46,119 +41,125 @@ export const builder = { alias: 'a', }, } -export const handler = (args: TemplatePushArguments) => exec(args) -/** - * Execute the command - */ -const exec = (args: TemplatePushArguments) => { - const { serverToken } = args +type MaybeString = string | null | undefined + +type ReviewItem = [string?, string?, string?, string?] +interface TemplatePushReview { + layouts: ReviewItem[] + templates: ReviewItem[] +} + +const STATUS_ADDED = chalk.green('Added') +const STATUS_MODIFIED = chalk.yellow('Modified') +const STATUS_UNMODIFIED = chalk.gray('Unmodified') - return validateToken(serverToken).then(token => { - validateDirectory(token, args) - }) +interface TemplatePushArguments { + serverToken: string + requestHost: string + templatesdirectory: string + force: boolean + all: boolean +} +export async function handler(args: TemplatePushArguments): Promise { + const serverToken = await validateToken(args.serverToken) + + try { + validatePushDirectory(args.templatesdirectory) + } catch (e) { + return fatalError(e) + } + + return push(serverToken, args) } /** * Check if directory exists before pushing */ -const validateDirectory = ( - serverToken: string, - args: TemplatePushArguments -) => { - const rootPath: string = untildify(args.templatesdirectory) - - // Check if path exists - if (!existsSync(rootPath)) { - log('The provided path does not exist', { error: true }) - return process.exit(1) +export function validatePushDirectory(dir: string): void { + const fullPath: string = untildify(dir) + + if (!existsSync(fullPath)) { + throw new Error(`The provided path "${dir}" does not exist`) } - return push(serverToken, args) + // check if path is a directory + const stats = statSync(fullPath) + if (!stats.isDirectory()) { + throw new Error(`The provided path "${dir}" is not a directory`) + } } /** - * Begin pushing the templates + * Push local templates to Postmark */ -const push = async (serverToken: string, args: TemplatePushArguments) => { - const { templatesdirectory, force, requestHost, all } = args - const spinner = ora('Fetching templates...').start() - const manifest = createManifest(templatesdirectory) - const client = new ServerClient(serverToken) - - if (requestHost !== undefined && requestHost !== '') { - client.setClientOptions({ requestHost }) +async function push(serverToken: string, args: TemplatePushArguments): Promise { + const { templatesdirectory, force, requestHost, all } = args; + + const client = new ServerClient(serverToken); + if (requestHost !== undefined && requestHost !== "") { + client.setClientOptions({ requestHost }); } - // Make sure manifest isn't empty - if (manifest.length > 0) { + const spinner = ora("Fetching templates...").start(); + try { + const manifest = createManifest(templatesdirectory); + + if (manifest.length === 0) { + return fatalError("No templates or layouts were found."); + } + try { - // Get template list from Postmark - const response = await client.getTemplates({count: 300}) - - if (response.TotalCount === 0) { - processTemplates({ - newList: [], - manifest: manifest, - all: all, - force: force, - spinner: spinner, - client: client, - }) - } else { - const newList = await getTemplateContent(client, response, spinner) - processTemplates({ - newList: newList, - manifest: manifest, - all: all, - force: force, - spinner: spinner, - client: client, - }) - } - } catch (error: any) { - spinner.stop() - log(error, { error: true }) - process.exit(1) + const templateList = await client.getTemplates({ count: 300 }); + const newList = templateList.TotalCount === 0 + ? [] + : await getTemplateContent(client, templateList, spinner); + + return await processTemplates({ + newList, + manifest, + all, + force, + spinner, + client, + }); + } catch (error) { + return fatalError(error); } - } else { - spinner.stop() - log('No templates or layouts were found.', { error: true }) - process.exit(1) + } finally { + spinner.stop(); } } + +interface ProcessTemplates { + newList: TemplateManifest[] + manifest: TemplateManifest[] + all: boolean + force: boolean + spinner: ora.Ora + client: ServerClient +} /** * Compare templates and CLI flow */ -const processTemplates = (config: ProcessTemplates) => { - const { newList, manifest, all, force, spinner, client } = config - - compareTemplates(newList, manifest, all) +async function processTemplates({ newList, manifest, all, force, spinner, client }: ProcessTemplates): Promise { + const pushManifest = compareTemplates(newList, manifest, all) spinner.stop() if (pushManifest.length === 0) return log('There are no changes to push.') // Show which templates are changing - printReview(review) + printReview(prepareReview(pushManifest)) // Push templates if force arg is present - if (force) { + if (force || await confirmation()) { spinner.text = 'Pushing templates to Postmark...' spinner.start() return pushTemplates(spinner, client, pushManifest) + } else { + log('Canceling push. Have a good day!') } - - // Ask for user confirmation - confirmation().then(answer => { - if (answer.confirm) { - spinner.text = 'Pushing templates to Postmark...' - spinner.start() - pushTemplates(spinner, client, pushManifest) - } else { - log('Canceling push. Have a good day!') - } - }) } /** @@ -187,67 +188,49 @@ async function getTemplateContent(client: ServerClient, templateList: Templates, /** * Ask user to confirm the push */ -const confirmation = (): Promise => - new Promise((resolve, reject) => { - prompt([ - { - type: 'confirm', - name: 'confirm', - default: false, - message: `Would you like to continue?`, - }, - ]) - .then((answer: any) => resolve(answer)) - .catch((err: any) => reject(err)) - }) +async function confirmation(message = 'Would you like to continue?', defaultResponse = false): Promise { + const answer = await prompt<{confirm: boolean}>([{ + type: 'confirm', + name: 'confirm', + default: defaultResponse, + message, + }]) + return answer.confirm +} /** * Compare templates on server against local */ -const compareTemplates = ( - response: TemplateManifest[], - manifest: TemplateManifest[], - pushAll: boolean -): void => { - // Iterate through manifest - manifest.forEach(template => { - // See if this local template exists on the server - const match = find(response, { Alias: template.Alias }) +function compareTemplates(remote: TemplateManifest[], local: TemplateManifest[], pushAll: boolean): TemplateManifest[] { + const result: TemplateManifest[] = [] + + for (const template of local) { + const match = find(remote, { Alias: template.Alias }) template.New = !match - // New template if (!match) { - template.Status = chalk.green('Added') - return pushTemplatePreview(match, template) - } - - // Set modification status - const modified = wasModified(match, template) - template.Status = modified - ? chalk.yellow('Modified') - : chalk.gray('Unmodified') + template.Status = STATUS_ADDED + result.push(template) + } else { + const modified = wasModified(match, template) + template.Status = modified ? STATUS_MODIFIED : STATUS_UNMODIFIED - // Push all templates if --all argument is present, - // regardless of whether templates were modified - if (pushAll) { - return pushTemplatePreview(match, template) + // Push all templates if --all argument is present, + // regardless of whether templates were modified + if (pushAll || modified) { + result.push(template) + } } + } - // Only push modified templates - if (modified) { - return pushTemplatePreview(match, template) - } - }) + return result; } /** * Check if local template is different than server */ -const wasModified = ( - server: TemplateManifest, - local: TemplateManifest -): boolean => { - const diff = templatesDiff(server, local) +function wasModified(remote: TemplateManifest, local: TemplateManifest): boolean { + const diff = templatesDiff(remote, local) const result = diff.size > 0 debug('Template %o was modified: %o. %o', local.Alias, result, diff) @@ -255,71 +238,60 @@ const wasModified = ( return result } -/** - * Push template details to review table - */ -const pushTemplatePreview = ( - match: any, - template: TemplateManifest -): number => { - pushManifest.push(template) - - let reviewData = [template.Status, template.Name, template.Alias] - - // Push layout to review table - if (template.TemplateType === 'Layout') return review.layouts.push(reviewData) - - // Push template to review table - // Add layout used column - reviewData.push( - layoutUsedLabel( - template.LayoutTemplate, - match ? match.LayoutTemplate : template.LayoutTemplate - ) - ) +function prepareReview(pushManifest: TemplateManifest[]): TemplatePushReview { + const templates: ReviewItem[] = [] + const layouts: ReviewItem[] = [] - return review.templates.push(reviewData) -} + for (const template of pushManifest) { + if (template.TemplateType === TemplateTypes.Layout) { + layouts.push([template.Status, template.Name, template.Alias || undefined]) + continue + } else { + templates.push([ + template.Status, + template.Name, + template.Alias || undefined, + layoutUsedLabel(template.LayoutTemplate, template.LayoutTemplate), + ]) + } + } -/** - * Render the "Layout used" column for Standard templates - */ -const layoutUsedLabel = ( - localLayout: string | null | undefined, - serverLayout: string | null | undefined -): string => { - let label = localLayout || chalk.gray('None') - - if (!sameContent(localLayout, serverLayout)) { - label += chalk.red(` ✘ ${serverLayout || 'None'}`) + return { + templates, + layouts, } - return label + function layoutUsedLabel(localLayout: MaybeString, remoteLayout: MaybeString): string { + let label = localLayout || chalk.gray('None') + + if (!sameContent(localLayout, remoteLayout)) { + label += chalk.red(` ✘ ${remoteLayout || 'None'}`) + } + + return label + } } /** * Show which templates will change after the publish */ -const printReview = (review: TemplatePushReview) => { - const { templates, layouts } = review - +function printReview({ templates, layouts }: TemplatePushReview) { // Table headers const header = [chalk.gray('Status'), chalk.gray('Name'), chalk.gray('Alias')] const templatesHeader = [...header, chalk.gray('Layout used')] // Labels - const templatesLabel = - templates.length > 0 - ? `${templates.length} ${pluralize( - templates.length, - 'template', - 'templates' - )}` - : '' - const layoutsLabel = - layouts.length > 0 - ? `${layouts.length} ${pluralize(layouts.length, 'layout', 'layouts')}` - : '' + const templatesLabel = templates.length > 0 + ? `${templates.length} ${pluralize( + templates.length, + 'template', + 'templates' + )}` + : '' + + const layoutsLabel = layouts.length > 0 + ? `${layouts.length} ${pluralize(layouts.length, 'layout', 'layouts')}` + : '' // Log template and layout files if (templates.length > 0) { @@ -338,9 +310,7 @@ const printReview = (review: TemplatePushReview) => { // Log summary log( chalk.yellow( - `${templatesLabel}${ - templates.length > 0 && layouts.length > 0 ? ' and ' : '' - }${layoutsLabel} will be pushed to Postmark.` + `${templatesLabel}${templates.length > 0 && layouts.length > 0 ? ' and ' : ''}${layoutsLabel} will be pushed to Postmark.` ) ) } @@ -348,78 +318,43 @@ const printReview = (review: TemplatePushReview) => { /** * Push all local templates */ -const pushTemplates = async ( - spinner: any, - client: any, - templates: TemplateManifest[] -) => { +async function pushTemplates(spinner: ora.Ora, client: ServerClient, templates: TemplateManifest[]) { + let failed = 0 + for (const template of templates) { spinner.color = 'yellow' spinner.text = `Pushing template: ${template.Alias}` if (template.New) { try { - const response = await client.createTemplate(template) - pushComplete(true, response, template, spinner, templates.length) - } catch(error: any) { - pushComplete(false, error, template, spinner, templates.length) + await client.createTemplate(template) + } catch (error) { + handleError(error, template) + failed++ } } else { + invariant(template.Alias, 'Template alias is required') try { - const response = await client.editTemplate(template.Alias, template) - pushComplete(true, response, template, spinner, templates.length) - } catch(error: any) { - pushComplete(false, error, template, spinner, templates.length) + await client.editTemplate(template.Alias, template) + } catch (error) { + handleError(error, template) + failed++ } } } -} -/** - * Run each time a push has been completed - */ -const pushComplete = ( - success: boolean, - response: object, - template: TemplateManifest, - spinner: any, - total: number -) => { - // Update counters - results[success ? 'success' : 'failed']++ - const completed = results.success + results.failed - - // Log any errors to the console - if (!success) { - spinner.stop() - log(`\n${template.Alias || template.Name}: ${response.toString()}`, { error: true }) - spinner.start() - } + spinner.stop() - if (completed === total) { - spinner.stop() + log('✅ All finished!', { color: 'green' }) - log('✅ All finished!', { color: 'green' }) - - // Show failures - if (results.failed) { - log( - `⚠️ Failed to push ${results.failed} ${pluralize( - results.failed, - 'template', - 'templates' - )}. Please see the output above for more details.`, - { error: true } - ) - } + if (failed > 0) { + logError( + `⚠️ Failed to push ${failed} ${pluralize(failed, 'template', 'templates')}. Please see the output above for more details.` + ) } -} - -let results: TemplatePushResults = { - success: 0, - failed: 0, -} -let review: TemplatePushReview = { - layouts: [], - templates: [], -} + function handleError(error: unknown, template: TemplateManifest) { + spinner.stop() + logError(`\n${template.Alias || template.Name}: ${error}`) + spinner.start() + } +} \ No newline at end of file diff --git a/src/types/Template.ts b/src/types/Template.ts index 74edf8a..cfe9189 100644 --- a/src/types/Template.ts +++ b/src/types/Template.ts @@ -1,78 +1,23 @@ -import { TemplateInList } from "postmark/dist/client/models" +import { TemplateTypes } from "postmark/dist/client/models" export interface TemplateManifest { Name?: string Subject?: string HtmlBody?: string TextBody?: string - Alias?: string + Alias?: string | null New?: boolean Status?: string - TemplateType: string - LayoutTemplate?: string | null -} - -export interface Template extends TemplateManifest { - Name: string - TemplateId: number - AssociatedServerId?: number - Active: boolean - Alias: string -} - - - -export interface TemplatePushResults { - success: number - failed: number -} - -export interface TemplatePushReview { - layouts: any[] - templates: any[] -} - -export interface ProcessTemplatesOptions { - spinner: any - client: any - outputDir: string - totalCount: number - templates: TemplateInList[] -} - -export interface TemplateListOptions { - sourceServer: string - requestHost: string - outputDir: string -} - -export interface TemplatePullArguments { - serverToken: string - requestHost: string - outputdirectory: string - overwrite: boolean -} - -export interface TemplatePushArguments { - serverToken: string - requestHost: string - templatesdirectory: string - force: boolean - all: boolean -} - -export interface TemplatePreviewArguments { - serverToken: string - templatesdirectory: string - port: number + TemplateType: TemplateTypes + LayoutTemplate?: string } export interface MetaFile { Name: string Alias: string - TemplateType: string + TemplateType: TemplateTypes Subject?: string - LayoutTemplate?: string | null + LayoutTemplate?: string HtmlBody?: string TextBody?: string TestRenderModel?: any @@ -85,17 +30,3 @@ export interface MetaFileTraverse { extension: string type: string } - -export interface TemplateValidationPayload { - TextBody: string - TemplateType: 'Standard' | 'Layout' -} - -export interface ProcessTemplates { - newList: TemplateManifest[] - manifest: TemplateManifest[] - all: boolean - force: boolean - spinner: any - client: any -} diff --git a/src/utils.ts b/src/utils.ts index 4f34f53..d21f699 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -8,28 +8,28 @@ import ora = require('ora') * Bootstrap commands * @returns yargs compatible command options */ -export const cmd = (name: string, desc: string): CommandOptions => ({ - name: name, - command: `${name} [options]`, - desc: desc, - builder: (yargs: Argv) => yargs.commandDir(`commands/${name}`), -}) +export function cmd(name: string, desc: string): CommandOptions { + return ({ + name: name, + command: `${name} [options]`, + desc: desc, + builder: (yargs: Argv) => yargs.commandDir(`commands/${name}`), + }) +} /** * Pluralize a string * @returns The proper string depending on the count */ -export const pluralize = ( - count: number, - singular: string, - plural: string -): string => (count > 1 || count === 0 ? plural : singular) +export function pluralize(count: number, singular: string, plural: string): string { + return (count > 1 || count === 0 ? plural : singular) +} /** * Log stuff to the console * @returns Logging with fancy colors */ -export const log = (text: string, settings?: LogSettings): void => { +export function log(text: string, settings?: LogSettings): void { // Errors if (settings && settings.error) { return console.error(chalk.red(text)) @@ -49,58 +49,52 @@ export const log = (text: string, settings?: LogSettings): void => { return console.log(text) } +export function logError(error: unknown): void { + log(extractErrorMessage(error), { error: true }) +} + +export function fatalError(error: unknown): never { + logError(error) + return process.exit(1) +} + /** * Prompt for server or account tokens - * @returns Promise */ -export const serverTokenPrompt = (account: boolean): Promise => - new Promise((resolve, reject) => { - const tokenType = account ? 'account' : 'server' - - prompt([ - { - type: 'password', - name: 'token', - message: `Please enter your ${tokenType} token`, - mask: '•', - }, - ]).then((answer: any) => { - const { token } = answer - - if (!token) { - log(`Invalid ${tokenType} token`, { error: true }) - process.exit(1) - return reject() - } - - return resolve(token) - }) - }) +async function serverTokenPrompt(forAccount: boolean): Promise { + const tokenType = forAccount ? 'account' : 'server' + const { token } = await prompt<{token: string}>([{ + type: 'password', + name: 'token', + message: `Please enter your ${tokenType} token`, + mask: '•', + }] + ) + + if (!token) { + return fatalError(`Invalid ${tokenType} token`) + } + + return token +} /** * Validates the presence of a server or account token - * @return Promise */ -export const validateToken = ( - token: string, - account: boolean = false -): Promise => - new Promise(resolve => { - // Missing token - if (!token) { - return serverTokenPrompt(account).then(tokenPrompt => - resolve(tokenPrompt) - ) - } - - return resolve(token) - }) +export async function validateToken(token: string, forAccount = false): Promise { + if (!token) { + return serverTokenPrompt(forAccount) + } + + return token +} + /** * Handle starting/stopping spinner and console output */ export class CommandResponse { - private spinner: any + private spinner: ora.Ora public constructor() { this.spinner = ora().clear() @@ -115,13 +109,21 @@ export class CommandResponse { log(text, settings) } - public errorResponse(error: any, showJsonError: boolean = false): void { + public errorResponse(error: unknown): never { this.spinner.stop() - if (showJsonError === true) { - log(JSON.stringify(error), { error: true }) - } - log(error, { error: true }) - process.exit(1) + return fatalError(error) + } +} + +function extractErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.toString() } + + if (typeof error === 'string') { + return error + } + + return `Unknown error: ${error}` } diff --git a/test/unit/commands/templates/helpers.test.ts b/test/unit/commands/templates/helpers.test.ts index 071c009..21bfc56 100644 --- a/test/unit/commands/templates/helpers.test.ts +++ b/test/unit/commands/templates/helpers.test.ts @@ -1,11 +1,12 @@ -import { expect } from "chai"; -import "mocha"; -import { templatesDiff } from "../../../../src/commands/templates/helpers"; -import { TemplateManifest } from "../../../../src/types"; +import 'mocha' +import { expect } from 'chai' +import { TemplateTypes } from 'postmark/dist/client/models' +import { templatesDiff } from '../../../../src/commands/templates/helpers' +import { TemplateManifest } from '../../../../src/types' function makeTemplateManifest(): TemplateManifest { return { - TemplateType: "Standard", + TemplateType: TemplateTypes.Standard, HtmlBody: undefined, TextBody: undefined, Subject: undefined,