From 0d4fc52ac1b6e4b60b8fc7baab594bb437335d82 Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Wed, 19 Jun 2019 16:57:25 -0700 Subject: [PATCH 01/25] Ability to pull and push layouts [WIP] --- src/commands/templates/pull.ts | 39 ++++++++++++++------------ src/commands/templates/push.ts | 50 ++++++++++++++++++++++++++++++---- src/types/Template.ts | 11 ++++++++ 3 files changed, 77 insertions(+), 23 deletions(-) diff --git a/src/commands/templates/pull.ts b/src/commands/templates/pull.ts index 7dfc14a..3718cef 100644 --- a/src/commands/templates/pull.ts +++ b/src/commands/templates/pull.ts @@ -10,6 +10,7 @@ import { Template, TemplateListOptions, TemplatePullArguments, + MetaFile, } from '../../types' import { log, validateToken, pluralize } from '../../utils' @@ -122,6 +123,9 @@ const processTemplates = (options: ProcessTemplatesOptions) => { // keep track of templates downloaded let totalDownloaded = 0 + // Create empty template and layout directories + createDirectories(outputDir) + // Iterate through each template and fetch content templates.forEach(template => { // Show warning if template doesn't have an alias @@ -174,10 +178,12 @@ const processTemplates = (options: ProcessTemplatesOptions) => { * @return An object containing the HTML and Text body */ const saveTemplate = (outputDir: string, template: Template) => { - template = pruneTemplateObject(template) - // Create the directory - const path: string = untildify(join(outputDir, template.Alias)) + const typePath = + template.TemplateType === 'Standard' ? 'templates' : 'layouts' + const path: string = untildify( + join(join(outputDir, typePath), template.Alias) + ) ensureDirSync(path) @@ -191,21 +197,18 @@ const saveTemplate = (outputDir: string, template: Template) => { outputFileSync(join(path, 'content.txt'), template.TextBody) } - // Create metadata JSON - delete template.HtmlBody - delete template.TextBody - - outputFileSync(join(path, 'meta.json'), JSON.stringify(template, null, 2)) + const meta: MetaFile = { + Name: template.Name, + Alias: template.Alias, + ...(template.Subject && { Subject: template.Subject }), + ...(template.TemplateType === 'Standard' && { + LayoutTemplate: template.LayoutTemplate, + }), + } + outputFileSync(join(path, 'meta.json'), JSON.stringify(meta, null, 2)) } -/** - * Remove unneeded fields on the template object - * @returns the pruned object - */ -const pruneTemplateObject = (template: Template) => { - delete template.AssociatedServerId - delete template.Active - delete template.TemplateId - - return template +const createDirectories = (outputDir: string) => { + ensureDirSync(untildify(join(outputDir, 'templates'))) + ensureDirSync(untildify(join(outputDir, 'layouts'))) } diff --git a/src/commands/templates/push.ts b/src/commands/templates/push.ts index 7ecb756..d5a7202 100644 --- a/src/commands/templates/push.ts +++ b/src/commands/templates/push.ts @@ -57,8 +57,25 @@ const validateDirectory = ( serverToken: string, args: TemplatePushArguments ) => { - if (!existsSync(untildify(args.templatesdirectory))) { - log('Could not find the template directory provided', { error: true }) + 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) + } + + // Check if path is missing templates and layouts folders + if ( + !existsSync(join(rootPath, 'templates')) && + !existsSync(join(rootPath, 'layouts')) + ) { + log( + 'The "templates" and "layouts" folder do not exist in the path provided', + { + error: true, + } + ) return process.exit(1) } @@ -88,6 +105,7 @@ const push = (serverToken: string, args: TemplatePushArguments) => { template.New ? chalk.green('Added') : chalk.yellow('Modified'), template.Name, template.Alias, + template.TemplateType === 'Standard' ? 'Template' : 'Layout', ]) }) @@ -125,6 +143,7 @@ const push = (serverToken: string, args: TemplatePushArguments) => { process.exit(1) }) } else { + spinner.stop() log('No templates were found in this directory', { error: true }) process.exit(1) } @@ -135,17 +154,33 @@ const push = (serverToken: string, args: TemplatePushArguments) => { * @returns An object containing all locally stored templates */ const createManifest = (path: string) => { + const templatesPath = join(path, 'templates') + const layoutsPath = join(path, 'layouts') + + return parseDirectory('layouts', layoutsPath).concat( + parseDirectory('templates', templatesPath) + ) +} + +/** + * Gathers and parses directory of templates or layouts + * @returns An object containing locally stored templates or layouts + */ +const parseDirectory = (type: string, path: string) => { let manifest: TemplateManifest[] = [] - const dirs = readdirSync(path).filter(f => + + const list = readdirSync(path).filter(f => statSync(join(path, f)).isDirectory() ) - dirs.forEach(dir => { + list.forEach(dir => { const metaPath = join(path, join(dir, 'meta.json')) const htmlPath = join(path, join(dir, 'content.html')) const textPath = join(path, join(dir, 'content.txt')) let template: TemplateManifest = {} + template.TemplateType = type === 'templates' ? 'Standard' : 'Layout' + if (existsSync(metaPath)) { template.HtmlBody = existsSync(htmlPath) ? readFileSync(htmlPath, 'utf-8') @@ -169,7 +204,12 @@ const createManifest = (path: string) => { */ const printReview = (review: TemplatePushReview) => { const { files, added, modified } = review - const head = [chalk.gray('Type'), chalk.gray('Name'), chalk.gray('Alias')] + const head = [ + chalk.gray('Change'), + chalk.gray('Name'), + chalk.gray('Alias'), + chalk.gray('Type'), + ] log(table([head, ...files], { border: getBorderCharacters('norc') })) diff --git a/src/types/Template.ts b/src/types/Template.ts index 4b29638..8b54742 100644 --- a/src/types/Template.ts +++ b/src/types/Template.ts @@ -5,6 +5,8 @@ export interface TemplateManifest { TextBody?: string Alias?: string New?: boolean + TemplateType?: string + LayoutTemplate?: string | null } export interface Template extends TemplateManifest { @@ -13,6 +15,8 @@ export interface Template extends TemplateManifest { AssociatedServerId?: number Active: boolean Alias: string + TemplateType: string + LayoutTemplate: string | null } export interface ListTemplate { @@ -62,3 +66,10 @@ export interface TemplatePushArguments { templatesdirectory: string force: boolean } + +export interface MetaFile { + Name: string + Alias: string + Subject?: string + LayoutTemplate?: string | null +} From b4fef10e8a91774bb178fd1d2e2f80591801095c Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Wed, 19 Jun 2019 17:37:27 -0700 Subject: [PATCH 02/25] Update tests [WIP] --- test/integration/templates.pull.test.ts | 21 +++++++++++++++++---- test/integration/templates.push.test.ts | 7 ++++--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/test/integration/templates.pull.test.ts b/test/integration/templates.pull.test.ts index b07176c..d8030c5 100644 --- a/test/integration/templates.pull.test.ts +++ b/test/integration/templates.pull.test.ts @@ -2,6 +2,7 @@ import { expect } from 'chai' import 'mocha' import execa from 'execa' import * as fs from 'fs-extra' +import { join } from 'path' const dirTree = require('directory-tree') import { serverToken, CLICommand, TestDataFolder } from './shared' @@ -23,16 +24,18 @@ describe('Templates command', () => { expect(stdout).to.include('All finished') }) - describe('Folder', () => { + describe('Templates folder', () => { + const path = join(dataFolder, 'templates') + it('templates', async () => { await execa(CLICommand, commandParameters, options) - const templateFolders = dirTree(dataFolder) + const templateFolders = dirTree(path) expect(templateFolders.children.length).to.be.gt(0) }) it('single template - file names', async () => { await execa(CLICommand, commandParameters, options) - const templateFolders = dirTree(dataFolder) + const templateFolders = dirTree(path) const files = templateFolders.children[0].children const names: string[] = files.map((f: any) => { @@ -44,7 +47,7 @@ describe('Templates command', () => { it('single template files - none empty', async () => { await execa(CLICommand, commandParameters, options) - const templateFolders = dirTree(dataFolder) + const templateFolders = dirTree(path) const files = templateFolders.children[0].children let result = files.findIndex((f: any) => { @@ -53,5 +56,15 @@ describe('Templates command', () => { expect(result).to.eq(-1) }) }) + + describe('Layouts folder', () => { + const path = join(dataFolder, 'layouts') + + it('layouts empty', async () => { + await execa(CLICommand, commandParameters, options) + const templateFolders = dirTree(path) + expect(templateFolders.children.length).to.be.eq(0) + }) + }) }) }) diff --git a/test/integration/templates.push.test.ts b/test/integration/templates.push.test.ts index 1daa199..96cc208 100644 --- a/test/integration/templates.push.test.ts +++ b/test/integration/templates.push.test.ts @@ -3,6 +3,7 @@ import 'mocha' import execa from 'execa' import * as fs from 'fs-extra' import { DirectoryTree } from 'directory-tree' +import { join } from 'path' const dirTree = require('directory-tree') import { serverToken, CLICommand, TestDataFolder } from './shared' @@ -30,7 +31,7 @@ describe('Templates command', () => { }) it('console out', async () => { - const templateFolders = dirTree(dataFolder) + const templateFolders = dirTree(join(dataFolder, 'templates')) const files = templateFolders.children[0].children const file: DirectoryTree = files.find((f: DirectoryTree) => { return f.path.includes('txt') @@ -44,7 +45,7 @@ describe('Templates command', () => { }) it('file content', async () => { - let templateFolders = dirTree(dataFolder) + let templateFolders = dirTree(join(dataFolder, 'templates')) let files = templateFolders.children[0].children let file: DirectoryTree = files.find((f: DirectoryTree) => { return f.path.includes('txt') @@ -57,7 +58,7 @@ describe('Templates command', () => { fs.removeSync(dataFolder) await execa(CLICommand, pullCommandParameters, options) - templateFolders = dirTree(dataFolder) + templateFolders = dirTree(join(dataFolder, 'templates')) files = templateFolders.children[0].children file = files.find((f: DirectoryTree) => { return f.path.includes('txt') From 6df5816c480e9124475fd04da80a78dbb73c2069 Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Thu, 20 Jun 2019 08:47:07 -0700 Subject: [PATCH 03/25] Check if template or layout directory exists before parsing Also add some comments --- src/commands/templates/push.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/commands/templates/push.ts b/src/commands/templates/push.ts index d5a7202..71d6f53 100644 --- a/src/commands/templates/push.ts +++ b/src/commands/templates/push.ts @@ -169,19 +169,26 @@ const createManifest = (path: string) => { const parseDirectory = (type: string, path: string) => { let manifest: TemplateManifest[] = [] + // Do not parse if directory does not exist + if (!existsSync(path)) return manifest + + // Get top level directory names const list = readdirSync(path).filter(f => statSync(join(path, f)).isDirectory() ) + // Parse each directory list.forEach(dir => { const metaPath = join(path, join(dir, 'meta.json')) const htmlPath = join(path, join(dir, 'content.html')) const textPath = join(path, join(dir, 'content.txt')) - let template: TemplateManifest = {} - - template.TemplateType = type === 'templates' ? 'Standard' : 'Layout' + let template: TemplateManifest = { + TemplateType: type === 'templates' ? 'Standard' : 'Layout', + } + // Check if meta file exists if (existsSync(metaPath)) { + // Read HTML and Text content from files template.HtmlBody = existsSync(htmlPath) ? readFileSync(htmlPath, 'utf-8') : '' @@ -189,7 +196,9 @@ const parseDirectory = (type: string, path: string) => { ? readFileSync(textPath, 'utf-8') : '' + // Ensure HTML body or Text content exists if (template.HtmlBody !== '' || template.TextBody !== '') { + // Assign contents of meta.json to object template = Object.assign(template, readJsonSync(metaPath)) manifest.push(template) } From a1a71448e3dd0013d39ea1c4cde834790faf4f36 Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Thu, 20 Jun 2019 11:16:22 -0700 Subject: [PATCH 04/25] Disable indentation errors in ESLint due to conflicts with Prettier --- .eslintrc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 7b039ae..6d7e679 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -16,7 +16,7 @@ module.exports = { 'no-console': 'off', eqeqeq: ['error', 'always'], 'linebreak-style': ['error', 'unix'], - '@typescript-eslint/indent': ['error', 2], + '@typescript-eslint/indent': 'off', '@typescript-eslint/member-delimiter-style': [ 'error', { multiline: { delimiter: 'none' } }, From e8e5d8a60a6e1cb4fc8c66f35f4f1269be85f246 Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Thu, 20 Jun 2019 11:16:35 -0700 Subject: [PATCH 05/25] Improve process for pushing templates and layouts --- src/commands/templates/push.ts | 95 ++++++++++++++++++++-------------- src/types/Template.ts | 7 ++- src/utils.ts | 2 +- 3 files changed, 60 insertions(+), 44 deletions(-) diff --git a/src/commands/templates/push.ts b/src/commands/templates/push.ts index 71d6f53..3cf4fee 100644 --- a/src/commands/templates/push.ts +++ b/src/commands/templates/push.ts @@ -100,13 +100,25 @@ const push = (serverToken: string, args: TemplatePushArguments) => { // Compare local templates with server manifest.forEach(template => { template.New = !find(response.Templates, { Alias: template.Alias }) - template.New ? review.added++ : review.modified++ - review.files.push([ + + let reviewData = [ template.New ? chalk.green('Added') : chalk.yellow('Modified'), template.Name, template.Alias, - template.TemplateType === 'Standard' ? 'Template' : 'Layout', - ]) + ] + + if (template.TemplateType === 'Standard') { + // Add layout template column + reviewData.push( + template.LayoutTemplate + ? template.LayoutTemplate + : chalk.gray('None') + ) + + review.templates.push(reviewData) + } else { + review.layouts.push(reviewData) + } }) spinner.stop() @@ -125,7 +137,7 @@ const push = (serverToken: string, args: TemplatePushArguments) => { type: 'confirm', name: 'confirm', default: false, - message: `Are you sure you want to push these templates to Postmark?`, + message: `Would you like to proceed?`, }, ]).then((answer: any) => { if (answer.confirm) { @@ -212,33 +224,46 @@ const parseDirectory = (type: string, path: string) => { * Show which templates will change after the publish */ const printReview = (review: TemplatePushReview) => { - const { files, added, modified } = review - const head = [ - chalk.gray('Change'), - chalk.gray('Name'), - chalk.gray('Alias'), - chalk.gray('Type'), - ] + const { templates, layouts } = review - log(table([head, ...files], { border: getBorderCharacters('norc') })) + // Table headers + const header = [chalk.gray('Change'), chalk.gray('Name'), chalk.gray('Alias')] + const templatesHeader = [...header, chalk.gray('Layout used')] - if (added > 0) { + // 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')}` + : '' + + // Log template and layout files + if (templates.length > 0) { + log(`\n${templatesLabel}`) log( - `${added} ${pluralize(added, 'template', 'templates')} will be added.`, - { color: 'green' } + table([templatesHeader, ...templates], { + border: getBorderCharacters('norc'), + }) ) } - - if (modified > 0) { - log( - `${modified} ${pluralize( - modified, - 'template', - 'templates' - )} will be modified.`, - { color: 'yellow' } - ) + if (layouts.length > 0) { + log(`\n${layoutsLabel}`) + log(table([header, ...layouts], { border: getBorderCharacters('norc') })) } + + // Log summary + log( + `${templatesLabel}${ + templates.length > 0 && layouts.length > 0 ? ' and ' : '' + }${layoutsLabel} will be pushed to Postmark.` + ) } /** @@ -301,26 +326,19 @@ const pushComplete = ( // Log any errors to the console if (!success) { spinner.stop() - log(`\n${template.Name}: ${response.toString()}`, { error: true }) + log(`\n${template.Alias}: ${response.toString()}`, { error: true }) spinner.start() } if (completed === total) { spinner.stop() - log( - `All finished! ${results.success} ${pluralize( - results.success, - 'template was', - 'templates were' - )} pushed to Postmark.`, - { color: 'green' } - ) + log('✅ All finished!', { color: 'green' }) // Show failures if (results.failed) { log( - `Failed to push ${results.failed} ${pluralize( + `⚠️ Failed to push ${results.failed} ${pluralize( results.failed, 'template', 'templates' @@ -337,7 +355,6 @@ let results: TemplatePushResults = { } let review: TemplatePushReview = { - files: [], - added: 0, - modified: 0, + layouts: [], + templates: [], } diff --git a/src/types/Template.ts b/src/types/Template.ts index 8b54742..0535c52 100644 --- a/src/types/Template.ts +++ b/src/types/Template.ts @@ -5,7 +5,7 @@ export interface TemplateManifest { TextBody?: string Alias?: string New?: boolean - TemplateType?: string + TemplateType: string LayoutTemplate?: string | null } @@ -37,9 +37,8 @@ export interface TemplatePushResults { } export interface TemplatePushReview { - files: any[] - added: number - modified: number + layouts: any[] + templates: any[] } export interface ProcessTemplatesOptions { diff --git a/src/utils.ts b/src/utils.ts index 95d771d..89bd718 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -24,7 +24,7 @@ export const pluralize = ( count: number, singular: string, plural: string -): string => (count > 1 ? plural : singular) +): string => (count > 1 || count === 0 ? plural : singular) /** * Log stuff to the console From 84c7f5f18dab95ce391d57a8cb4e854653092c2f Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Thu, 20 Jun 2019 11:17:00 -0700 Subject: [PATCH 06/25] Minor formatting tweak --- src/commands/templates/pull.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/commands/templates/pull.ts b/src/commands/templates/pull.ts index 3718cef..dda5b19 100644 --- a/src/commands/templates/pull.ts +++ b/src/commands/templates/pull.ts @@ -205,9 +205,13 @@ const saveTemplate = (outputDir: string, template: Template) => { LayoutTemplate: template.LayoutTemplate, }), } + outputFileSync(join(path, 'meta.json'), JSON.stringify(meta, null, 2)) } +/** + * Creates empty template and layout directories + */ const createDirectories = (outputDir: string) => { ensureDirSync(untildify(join(outputDir, 'templates'))) ensureDirSync(untildify(join(outputDir, 'layouts'))) From 8fd74da63f8bcf9b89ae68fea45e7860e442808e Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Thu, 20 Jun 2019 17:18:29 -0700 Subject: [PATCH 07/25] Improve review table when pushing --- src/commands/templates/push.ts | 105 +++++++++++++++++++++------------ src/types/Template.ts | 2 + 2 files changed, 69 insertions(+), 38 deletions(-) diff --git a/src/commands/templates/push.ts b/src/commands/templates/push.ts index 3cf4fee..8d972eb 100644 --- a/src/commands/templates/push.ts +++ b/src/commands/templates/push.ts @@ -19,6 +19,7 @@ import { TemplatePushResults, TemplatePushReview, TemplatePushArguments, + Templates, } from '../../types' import { pluralize, log, validateToken } from '../../utils' @@ -97,29 +98,7 @@ const push = (serverToken: string, args: TemplatePushArguments) => { client .getTemplates() .then(response => { - // Compare local templates with server - manifest.forEach(template => { - template.New = !find(response.Templates, { Alias: template.Alias }) - - let reviewData = [ - template.New ? chalk.green('Added') : chalk.yellow('Modified'), - template.Name, - template.Alias, - ] - - if (template.TemplateType === 'Standard') { - // Add layout template column - reviewData.push( - template.LayoutTemplate - ? template.LayoutTemplate - : chalk.gray('None') - ) - - review.templates.push(reviewData) - } else { - review.layouts.push(reviewData) - } - }) + compareTemplates(response, manifest) spinner.stop() printReview(review) @@ -137,7 +116,7 @@ const push = (serverToken: string, args: TemplatePushArguments) => { type: 'confirm', name: 'confirm', default: false, - message: `Would you like to proceed?`, + message: `Would you like to continue?`, }, ]).then((answer: any) => { if (answer.confirm) { @@ -156,30 +135,77 @@ const push = (serverToken: string, args: TemplatePushArguments) => { }) } else { spinner.stop() - log('No templates were found in this directory', { error: true }) + log('No templates or layouts were found.', { error: true }) process.exit(1) } } /** - * Gather up templates on the file system - * @returns An object containing all locally stored templates + * Compare templates on server against local */ -const createManifest = (path: string) => { - const templatesPath = join(path, 'templates') - const layoutsPath = join(path, 'layouts') +const compareTemplates = ( + response: Templates, + manifest: TemplateManifest[] +): void => { + // Iterate through manifest + manifest.forEach(template => { + // See if this local template exists on the server + const match = find(response.Templates, { Alias: template.Alias }) + template.New = !match + + let reviewData = [ + template.New ? chalk.green('Added') : chalk.yellow('Modified'), + template.Name, + template.Alias, + ] + + if (template.TemplateType === 'Standard') { + // Add layout used column + reviewData.push( + layoutUsedLabel( + template.LayoutTemplate, + match ? match.LayoutTemplate : template.LayoutTemplate + ) + ) - return parseDirectory('layouts', layoutsPath).concat( - parseDirectory('templates', templatesPath) - ) + review.templates.push(reviewData) + } else { + review.layouts.push(reviewData) + } + }) } +/** + * Render the "Layout used" column for Standard templates + */ +const layoutUsedLabel = (localLayout: any, serverLayout: any): string => { + let label: string = localLayout ? localLayout : chalk.gray('None') + + // If layout template on server doesn't match local template + if (localLayout !== serverLayout) { + serverLayout = serverLayout ? serverLayout : 'None' + + // Append old server layout to label + label += chalk.red(` ✘ ${serverLayout}`) + } + + return label +} + +/** + * Gather up templates on the file system + */ +const createManifest = (path: string): TemplateManifest[] => [ + ...parseDirectory('layouts', path), + ...parseDirectory('templates', path), +] + /** * Gathers and parses directory of templates or layouts - * @returns An object containing locally stored templates or layouts */ -const parseDirectory = (type: string, path: string) => { +const parseDirectory = (type: string, rootPath: string) => { let manifest: TemplateManifest[] = [] + const path = join(rootPath, type) // Do not parse if directory does not exist if (!existsSync(path)) return manifest @@ -196,6 +222,7 @@ const parseDirectory = (type: string, path: string) => { const textPath = join(path, join(dir, 'content.txt')) let template: TemplateManifest = { TemplateType: type === 'templates' ? 'Standard' : 'Layout', + ...(type === 'templates' && { LayoutTemplate: null }), } // Check if meta file exists @@ -260,9 +287,11 @@ const printReview = (review: TemplatePushReview) => { // Log summary log( - `${templatesLabel}${ - templates.length > 0 && layouts.length > 0 ? ' and ' : '' - }${layoutsLabel} will be pushed to Postmark.` + chalk.yellow( + `${templatesLabel}${ + templates.length > 0 && layouts.length > 0 ? ' and ' : '' + }${layoutsLabel} will be pushed to Postmark.` + ) ) } diff --git a/src/types/Template.ts b/src/types/Template.ts index 0535c52..a0f5460 100644 --- a/src/types/Template.ts +++ b/src/types/Template.ts @@ -24,6 +24,8 @@ export interface ListTemplate { TemplateId: number Name: string Alias?: string | null + TemplateType: string + LayoutTemplate: string | null } export interface Templates { From a91c1b102982768d1fc46c34ccb5b25aa1c5e0d0 Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Mon, 24 Jun 2019 10:59:19 -0700 Subject: [PATCH 08/25] Remove unneeded types --- src/types/Template.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/types/Template.ts b/src/types/Template.ts index a0f5460..4f806b2 100644 --- a/src/types/Template.ts +++ b/src/types/Template.ts @@ -15,8 +15,6 @@ export interface Template extends TemplateManifest { AssociatedServerId?: number Active: boolean Alias: string - TemplateType: string - LayoutTemplate: string | null } export interface ListTemplate { From 01f8990a6f8bcfaac75218c52746132efca0e2fa Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Mon, 24 Jun 2019 11:22:52 -0700 Subject: [PATCH 09/25] Update test --- test/integration/templates.push.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/integration/templates.push.test.ts b/test/integration/templates.push.test.ts index 96cc208..684ce34 100644 --- a/test/integration/templates.push.test.ts +++ b/test/integration/templates.push.test.ts @@ -39,9 +39,7 @@ describe('Templates command', () => { fs.writeFileSync(file.path, `test data ${Date.now().toString()}`, 'utf-8') const { stdout } = await execa(CLICommand, pushCommandParameters, options) - expect(stdout).to.include( - 'All finished! 1 template was pushed to Postmark.' - ) + expect(stdout).to.include('All finished!') }) it('file content', async () => { From 09c5d2eb2b2fee90271675b9a93fc1a0b83d3e10 Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Mon, 24 Jun 2019 11:23:07 -0700 Subject: [PATCH 10/25] Update types --- src/commands/templates/push.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/commands/templates/push.ts b/src/commands/templates/push.ts index 8d972eb..9423f62 100644 --- a/src/commands/templates/push.ts +++ b/src/commands/templates/push.ts @@ -178,7 +178,10 @@ const compareTemplates = ( /** * Render the "Layout used" column for Standard templates */ -const layoutUsedLabel = (localLayout: any, serverLayout: any): string => { +const layoutUsedLabel = ( + localLayout: string | null | undefined, + serverLayout: string | null | undefined +): string => { let label: string = localLayout ? localLayout : chalk.gray('None') // If layout template on server doesn't match local template From c30c98ad1f29ceaf9dd4d5545f12d259d84d1d52 Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Mon, 24 Jun 2019 11:40:48 -0700 Subject: [PATCH 11/25] Update push confirmation screenshot --- media/push-confirm.png | Bin 16025 -> 26743 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/media/push-confirm.png b/media/push-confirm.png index 7f3b523b8e874cda8f8735500f62919eb00a89a3..4ad767681d82cc8fd450aca01509b7d8ef352a98 100644 GIT binary patch literal 26743 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 oa4`PeUdi59MrW7&|2-JLY(TZerI Date: Mon, 24 Jun 2019 12:50:18 -0700 Subject: [PATCH 12/25] Bump version --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4eddab9..a6ed8c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "postmark-cli", - "version": "1.0.0", + "version": "2.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index f00e00f..25ab1fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "postmark-cli", - "version": "1.0.0", + "version": "2.0.0", "description": "A CLI tool for managing templates, sending emails, and fetching servers on Postmark.", "main": "./dist/index.js", "dependencies": { From 575bf05b92e44e2fc6edf8a52197849b90d53e19 Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Tue, 25 Jun 2019 10:20:14 -0700 Subject: [PATCH 13/25] Update postmark.js package --- package-lock.json | 6 +++--- package.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index a6ed8c0..fd328f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1669,9 +1669,9 @@ "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" }, "postmark": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/postmark/-/postmark-2.2.4.tgz", - "integrity": "sha512-qzqcXrKQnSGIScOpCNz2ArC69zHSuVZKo8RLg8Nioz633NIi4aVYeBNBwCn1hfAcQ/g/eCAyTAvVbMYyPmlSzQ==", + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/postmark/-/postmark-2.2.7.tgz", + "integrity": "sha512-vqDqh9/N/weyFQglewU4O2+6OLaEdibZrOyCEubyukfWz5DKUsBk3GGoU4c8P+QLk7DtiX/TAwwsXZ3bgX+fXA==", "requires": { "request": "^2.88.0" } diff --git a/package.json b/package.json index 25ab1fd..c2e1edc 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "inquirer": "^6.2.1", "lodash": "^4.17.11", "ora": "^3.0.0", - "postmark": "^2.2.4", + "postmark": "^2.2.7", "request": "^2.88.0", "table": "^5.2.0", "untildify": "^4.0.0", From ff63f993c4e0865bb61988eacbd798debb11482c Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Tue, 25 Jun 2019 10:26:38 -0700 Subject: [PATCH 14/25] Fix test --- test/unit/utils.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts index a091ddb..c45b9de 100644 --- a/test/unit/utils.test.ts +++ b/test/unit/utils.test.ts @@ -25,6 +25,7 @@ describe('Utilities', () => { const plural = 'templates' it('should return plural', () => { + expect(utils.pluralize(0, singular, plural)).to.eq(singular) expect(utils.pluralize(2, singular, plural)).to.eq(plural) expect(utils.pluralize(5, singular, plural)).to.eq(plural) expect(utils.pluralize(10, singular, plural)).to.eq(plural) @@ -33,7 +34,6 @@ describe('Utilities', () => { it('should return singular', () => { expect(utils.pluralize(1, singular, plural)).to.eq(singular) - expect(utils.pluralize(0, singular, plural)).to.eq(singular) expect(utils.pluralize(-1, singular, plural)).to.eq(singular) }) }) From b33f3081a49f6143b5304581601cee68fd9becc4 Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Tue, 25 Jun 2019 10:32:17 -0700 Subject: [PATCH 15/25] Fix expected return value --- test/unit/utils.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/utils.test.ts b/test/unit/utils.test.ts index c45b9de..c1741ad 100644 --- a/test/unit/utils.test.ts +++ b/test/unit/utils.test.ts @@ -25,7 +25,7 @@ describe('Utilities', () => { const plural = 'templates' it('should return plural', () => { - expect(utils.pluralize(0, singular, plural)).to.eq(singular) + expect(utils.pluralize(0, singular, plural)).to.eq(plural) expect(utils.pluralize(2, singular, plural)).to.eq(plural) expect(utils.pluralize(5, singular, plural)).to.eq(plural) expect(utils.pluralize(10, singular, plural)).to.eq(plural) From c399ebf88ec1671ba1debba894abd7ad81724922 Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Tue, 25 Jun 2019 11:01:58 -0700 Subject: [PATCH 16/25] Fix incompatible types in node 7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit error TS2345: Argument of type '(answer: { code?: string | undefined; }) => void' is not assignable to parameter of type '(value: unknown) => void | PromiseLike'. Types of parameters 'answer' and 'value' are incompatible. Type 'unknown' is not assignable to type '{ code?: string | undefined; }’. --- src/commands/cheats.ts | 2 +- src/commands/templates/pull.ts | 2 +- src/utils.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/cheats.ts b/src/commands/cheats.ts index a528816..f7929a2 100644 --- a/src/commands/cheats.ts +++ b/src/commands/cheats.ts @@ -48,7 +48,7 @@ const cheatInput = (hideMessage: boolean): Promise => choices: choices, message: hideMessage ? '\n' : title, }, - ]).then((answer: { code?: string }) => { + ]).then((answer: any) => { return resolve(answer.code) }) }) diff --git a/src/commands/templates/pull.ts b/src/commands/templates/pull.ts index dda5b19..89a0690 100644 --- a/src/commands/templates/pull.ts +++ b/src/commands/templates/pull.ts @@ -69,7 +69,7 @@ const overwritePrompt = (serverToken: string, outputdirectory: string) => { default: false, message: `Are you sure you want to overwrite the files in ${outputdirectory}?`, }, - ]).then((answer: { overwrite?: boolean }) => { + ]).then((answer: any) => { if (answer.overwrite) { return fetchTemplateList({ sourceServer: serverToken, diff --git a/src/utils.ts b/src/utils.ts index 89bd718..fcd832a 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -65,7 +65,7 @@ export const serverTokenPrompt = (account: boolean): Promise => message: `Please enter your ${tokenType} token`, mask: '•', }, - ]).then((answer: { token?: string }) => { + ]).then((answer: any) => { const { token } = answer if (!token) { From 0caa91f487321e4e8921158b743b466b027d2185 Mon Sep 17 00:00:00 2001 From: Igor Balos Date: Wed, 26 Jun 2019 15:52:06 +0200 Subject: [PATCH 17/25] added layouts tests for pushes and pulls --- test/integration/shared.ts | 41 +++++++++ test/integration/templates.pull.test.ts | 71 ++++++++++----- test/integration/templates.push.test.ts | 109 ++++++++++++++++-------- 3 files changed, 166 insertions(+), 55 deletions(-) diff --git a/test/integration/shared.ts b/test/integration/shared.ts index f5bce07..32ed261 100644 --- a/test/integration/shared.ts +++ b/test/integration/shared.ts @@ -1,4 +1,5 @@ import nconf from 'nconf' +import * as postmark from "postmark"; export const testingKeys = nconf .env() @@ -11,3 +12,43 @@ export const toAddress: string = testingKeys.get('TO_ADDRESS') export const CLICommand: string = './dist/index.js' export const TestDataFolder: string = './test/data' export const PackageJson: any = require('../../package.json') + +// In order to test template syncing, data needs to be created by postmark.js client + +const templatePrefix: string = "testing-template-cli"; + +function templateToCreate(templatePrefix: string) { + return new postmark.Models.CreateTemplateRequest( + `${templatePrefix}-${Date.now()}`, + "Subject", + "Html body", + "Text body", + null, + postmark.Models.TemplateTypes.Standard, + ); +} + +function templateLayoutToCreate(templatePrefix: string) { + return new postmark.Models.CreateTemplateRequest( + `${templatePrefix}-${Date.now()}`, undefined, + "Html body {{{@content}}}", "Text body {{{@content}}}", + null, postmark.Models.TemplateTypes.Layout, + ); +} + +export const createTemplateData = async () => { + const client = new postmark.ServerClient(serverToken); + await client.createTemplate(templateToCreate(templatePrefix)); + await client.createTemplate(templateLayoutToCreate(templatePrefix)); +} + +export const deleteTemplateData = async () => { + const client = new postmark.ServerClient(serverToken); + const templates = await client.getTemplates({count: 50}); + + for (const template of templates.Templates) { + if (template.Name.includes(templatePrefix)) { + await client.deleteTemplate(template.TemplateId); + } + } +} \ No newline at end of file diff --git a/test/integration/templates.pull.test.ts b/test/integration/templates.pull.test.ts index d8030c5..549f2fc 100644 --- a/test/integration/templates.pull.test.ts +++ b/test/integration/templates.pull.test.ts @@ -1,43 +1,54 @@ -import { expect } from 'chai' +import {expect} from 'chai' import 'mocha' import execa from 'execa' import * as fs from 'fs-extra' -import { join } from 'path' +import {join} from 'path' const dirTree = require('directory-tree') -import { serverToken, CLICommand, TestDataFolder } from './shared' +import {serverToken, CLICommand, TestDataFolder, createTemplateData, deleteTemplateData} from './shared' describe('Templates command', () => { - const options: execa.CommonOptions = { - env: { POSTMARK_SERVER_TOKEN: serverToken }, - } + const options: execa.CommonOptions = { env: {POSTMARK_SERVER_TOKEN: serverToken} } const dataFolder: string = TestDataFolder const commandParameters: string[] = ['templates', 'pull', dataFolder] + before(async () => { + await deleteTemplateData() + return createTemplateData() + }) + + after(async () => { + await deleteTemplateData() + }) + afterEach(() => { fs.removeSync(dataFolder) }) describe('Pull', () => { + function retrieveFiles(path: string) { + const folderTree = dirTree(path) + return folderTree.children[0].children + } + it('console out', async () => { - const { stdout } = await execa(CLICommand, commandParameters, options) + const {stdout} = await execa(CLICommand, commandParameters, options) expect(stdout).to.include('All finished') }) - describe('Templates folder', () => { - const path = join(dataFolder, 'templates') + describe('Templates', () => { + const filesPath = join(dataFolder, 'templates') it('templates', async () => { await execa(CLICommand, commandParameters, options) - const templateFolders = dirTree(path) - expect(templateFolders.children.length).to.be.gt(0) + const folderTree = dirTree(filesPath) + expect(folderTree.children.length).to.be.gt(0) }) it('single template - file names', async () => { await execa(CLICommand, commandParameters, options) - const templateFolders = dirTree(path) + const files = retrieveFiles(filesPath) - const files = templateFolders.children[0].children const names: string[] = files.map((f: any) => { return f.name }) @@ -47,8 +58,7 @@ describe('Templates command', () => { it('single template files - none empty', async () => { await execa(CLICommand, commandParameters, options) - const templateFolders = dirTree(path) - const files = templateFolders.children[0].children + const files = retrieveFiles(filesPath) let result = files.findIndex((f: any) => { return f.size <= 0 @@ -57,13 +67,34 @@ describe('Templates command', () => { }) }) - describe('Layouts folder', () => { - const path = join(dataFolder, 'layouts') + describe('Layouts', () => { + const filesPath = join(dataFolder, 'layouts') + + it('layouts', async () => { + await execa(CLICommand, commandParameters, options) + const folderTree = dirTree(filesPath) + expect(folderTree.children.length).to.be.gt(0) + }) + + it('single layout - file names', async () => { + await execa(CLICommand, commandParameters, options) + const files = retrieveFiles(filesPath) + + const names: string[] = files.map((f: any) => { + return f.name + }) + + expect(names).to.members(['content.txt', 'content.html', 'meta.json']) + }) - it('layouts empty', async () => { + it('single layout files - none empty', async () => { await execa(CLICommand, commandParameters, options) - const templateFolders = dirTree(path) - expect(templateFolders.children.length).to.be.eq(0) + const files = retrieveFiles(filesPath) + + let result = files.findIndex((f: any) => { + return f.size <= 0 + }) + expect(result).to.eq(-1) }) }) }) diff --git a/test/integration/templates.push.test.ts b/test/integration/templates.push.test.ts index 684ce34..019437c 100644 --- a/test/integration/templates.push.test.ts +++ b/test/integration/templates.push.test.ts @@ -6,64 +6,103 @@ import { DirectoryTree } from 'directory-tree' import { join } from 'path' const dirTree = require('directory-tree') -import { serverToken, CLICommand, TestDataFolder } from './shared' +import {serverToken, CLICommand, TestDataFolder, createTemplateData, deleteTemplateData} from './shared' describe('Templates command', () => { - const options: execa.CommonOptions = { - env: { POSTMARK_SERVER_TOKEN: serverToken }, - } + const options: execa.CommonOptions = { env: { POSTMARK_SERVER_TOKEN: serverToken }} const dataFolder: string = TestDataFolder - const pushCommandParameters: string[] = [ - 'templates', - 'push', - dataFolder, - '--force', - ] + const pushCommandParameters: string[] = ['templates', 'push', dataFolder, '--force'] const pullCommandParameters: string[] = ['templates', 'pull', dataFolder] + before(async () => { + await deleteTemplateData() + return createTemplateData() + }) + + after(async () => { + await deleteTemplateData() + }) + afterEach(() => { fs.removeSync(dataFolder) }) describe('Push', () => { + function retrieveFiles(path: string) { + const folderTree = dirTree(path) + return folderTree.children[0].children + } + beforeEach(async () => { await execa(CLICommand, pullCommandParameters, options) }) - it('console out', async () => { - const templateFolders = dirTree(join(dataFolder, 'templates')) - const files = templateFolders.children[0].children - const file: DirectoryTree = files.find((f: DirectoryTree) => { - return f.path.includes('txt') + describe('Templates', () => { + const filesPath = join(dataFolder, 'templates') + + it('console out', async () => { + const files = retrieveFiles(filesPath) + const file: DirectoryTree = files.find((f: DirectoryTree) => { + return f.path.includes('txt') + }) + + fs.writeFileSync(file.path, `test data ${Date.now().toString()}`, 'utf-8') + const { stdout } = await execa(CLICommand, pushCommandParameters, options) + expect(stdout).to.include('All finished!') }) - fs.writeFileSync(file.path, `test data ${Date.now().toString()}`, 'utf-8') - const { stdout } = await execa(CLICommand, pushCommandParameters, options) - expect(stdout).to.include('All finished!') - }) + it('file content', async () => { + let files = retrieveFiles(filesPath) + let file: DirectoryTree = files.find((f: DirectoryTree) => { + return f.path.includes('txt') + }) + const contentToPush: string = `test data ${Date.now().toString()}` + + fs.writeFileSync(file.path, contentToPush, 'utf-8') + await execa(CLICommand, pushCommandParameters, options) - it('file content', async () => { - let templateFolders = dirTree(join(dataFolder, 'templates')) - let files = templateFolders.children[0].children - let file: DirectoryTree = files.find((f: DirectoryTree) => { - return f.path.includes('txt') + fs.removeSync(dataFolder) + await execa(CLICommand, pullCommandParameters, options) + + files = retrieveFiles(filesPath) + file = files.find((f: DirectoryTree) => { return f.path.includes('txt') }) + + const content: string = fs.readFileSync(file.path).toString('utf-8') + expect(content).to.equal(contentToPush) }) - const contentToPush: string = `test data ${Date.now().toString()}` + }) - fs.writeFileSync(file.path, contentToPush, 'utf-8') - await execa(CLICommand, pushCommandParameters, options) + describe('Layouts', () => { + const filesPath = join(dataFolder, 'layouts') - fs.removeSync(dataFolder) - await execa(CLICommand, pullCommandParameters, options) + it('console out', async () => { + const files = retrieveFiles(filesPath) + const file: DirectoryTree = files.find((f: DirectoryTree) => { return f.path.includes('txt') }) - templateFolders = dirTree(join(dataFolder, 'templates')) - files = templateFolders.children[0].children - file = files.find((f: DirectoryTree) => { - return f.path.includes('txt') + fs.writeFileSync(file.path, `test data ${Date.now().toString()} {{{@content}}}`, 'utf-8') + const { stdout } = await execa(CLICommand, pushCommandParameters, options) + expect(stdout).to.include('All finished!') }) - const content: string = fs.readFileSync(file.path).toString('utf-8') - expect(content).to.equal(contentToPush) + it('file content', async () => { + let files = retrieveFiles(filesPath) + let file: DirectoryTree = files.find((f: DirectoryTree) => { + return f.path.includes('txt') + }) + const contentToPush: string = `test data ${Date.now().toString()} {{{@content}}}` + + fs.writeFileSync(file.path, contentToPush, 'utf-8') + await execa(CLICommand, pushCommandParameters, options) + + fs.removeSync(dataFolder) + await execa(CLICommand, pullCommandParameters, options) + + files = retrieveFiles(filesPath) + file = files.find((f: DirectoryTree) => { return f.path.includes('txt') }) + + const content: string = fs.readFileSync(file.path).toString('utf-8') + expect(content).to.equal(contentToPush) + }) }) }) }) From 140357e74c831d7b92577689ad17ec7a749986dc Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Wed, 26 Jun 2019 10:26:57 -0700 Subject: [PATCH 18/25] Only store layouts in separate folder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Continue storing templates in root so that we don’t introduce breaking changes. --- src/commands/templates/pull.ts | 22 ++++------------------ src/commands/templates/push.ts | 28 +++++++--------------------- 2 files changed, 11 insertions(+), 39 deletions(-) diff --git a/src/commands/templates/pull.ts b/src/commands/templates/pull.ts index 89a0690..f5eabc4 100644 --- a/src/commands/templates/pull.ts +++ b/src/commands/templates/pull.ts @@ -123,9 +123,6 @@ const processTemplates = (options: ProcessTemplatesOptions) => { // keep track of templates downloaded let totalDownloaded = 0 - // Create empty template and layout directories - createDirectories(outputDir) - // Iterate through each template and fetch content templates.forEach(template => { // Show warning if template doesn't have an alias @@ -144,7 +141,7 @@ const processTemplates = (options: ProcessTemplatesOptions) => { } client - .getTemplate(template.TemplateId) + .getTemplate(template.Alias) .then((response: Template) => { requestCount++ @@ -178,12 +175,9 @@ const processTemplates = (options: ProcessTemplatesOptions) => { * @return An object containing the HTML and Text body */ const saveTemplate = (outputDir: string, template: Template) => { - // Create the directory - const typePath = - template.TemplateType === 'Standard' ? 'templates' : 'layouts' - const path: string = untildify( - join(join(outputDir, typePath), template.Alias) - ) + outputDir = + template.TemplateType === 'Layout' ? join(outputDir, '_layouts') : outputDir + const path: string = untildify(join(outputDir, template.Alias)) ensureDirSync(path) @@ -208,11 +202,3 @@ const saveTemplate = (outputDir: string, template: Template) => { outputFileSync(join(path, 'meta.json'), JSON.stringify(meta, null, 2)) } - -/** - * Creates empty template and layout directories - */ -const createDirectories = (outputDir: string) => { - ensureDirSync(untildify(join(outputDir, 'templates'))) - ensureDirSync(untildify(join(outputDir, 'layouts'))) -} diff --git a/src/commands/templates/push.ts b/src/commands/templates/push.ts index 9423f62..31fc74b 100644 --- a/src/commands/templates/push.ts +++ b/src/commands/templates/push.ts @@ -66,20 +66,6 @@ const validateDirectory = ( return process.exit(1) } - // Check if path is missing templates and layouts folders - if ( - !existsSync(join(rootPath, 'templates')) && - !existsSync(join(rootPath, 'layouts')) - ) { - log( - 'The "templates" and "layouts" folder do not exist in the path provided', - { - error: true, - } - ) - return process.exit(1) - } - return push(serverToken, args) } @@ -199,16 +185,16 @@ const layoutUsedLabel = ( * Gather up templates on the file system */ const createManifest = (path: string): TemplateManifest[] => [ - ...parseDirectory('layouts', path), - ...parseDirectory('templates', path), + ...parseDirectory('Layout', path), + ...parseDirectory('Standard', path), ] /** - * Gathers and parses directory of templates or layouts + * Parses directory of templates or layouts */ -const parseDirectory = (type: string, rootPath: string) => { +const parseDirectory = (type: 'Layout' | 'Standard', rootPath: string) => { let manifest: TemplateManifest[] = [] - const path = join(rootPath, type) + const path = type === 'Layout' ? join(rootPath, '_layouts') : rootPath // Do not parse if directory does not exist if (!existsSync(path)) return manifest @@ -224,8 +210,8 @@ const parseDirectory = (type: string, rootPath: string) => { const htmlPath = join(path, join(dir, 'content.html')) const textPath = join(path, join(dir, 'content.txt')) let template: TemplateManifest = { - TemplateType: type === 'templates' ? 'Standard' : 'Layout', - ...(type === 'templates' && { LayoutTemplate: null }), + TemplateType: type, + ...(type === 'Standard' && { LayoutTemplate: null }), } // Check if meta file exists From e28e55db4a88fd3331f219825fb1289825384df4 Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Wed, 26 Jun 2019 10:27:33 -0700 Subject: [PATCH 19/25] v1.1.0 --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index fd328f5..eef694d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "postmark-cli", - "version": "2.0.0", + "version": "1.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index c2e1edc..78c150d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "postmark-cli", - "version": "2.0.0", + "version": "1.1.0", "description": "A CLI tool for managing templates, sending emails, and fetching servers on Postmark.", "main": "./dist/index.js", "dependencies": { From 613e79ab89c4c1e62ab018a3ba4894fc6420cd3a Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Wed, 26 Jun 2019 10:28:32 -0700 Subject: [PATCH 20/25] Format with Prettier --- test/integration/templates.pull.test.ts | 20 ++++++--- test/integration/templates.push.test.ts | 57 ++++++++++++++++++++----- 2 files changed, 60 insertions(+), 17 deletions(-) diff --git a/test/integration/templates.pull.test.ts b/test/integration/templates.pull.test.ts index 549f2fc..87293bd 100644 --- a/test/integration/templates.pull.test.ts +++ b/test/integration/templates.pull.test.ts @@ -1,14 +1,22 @@ -import {expect} from 'chai' +import { expect } from 'chai' import 'mocha' import execa from 'execa' import * as fs from 'fs-extra' -import {join} from 'path' +import { join } from 'path' const dirTree = require('directory-tree') -import {serverToken, CLICommand, TestDataFolder, createTemplateData, deleteTemplateData} from './shared' +import { + serverToken, + CLICommand, + TestDataFolder, + createTemplateData, + deleteTemplateData, +} from './shared' describe('Templates command', () => { - const options: execa.CommonOptions = { env: {POSTMARK_SERVER_TOKEN: serverToken} } + const options: execa.CommonOptions = { + env: { POSTMARK_SERVER_TOKEN: serverToken }, + } const dataFolder: string = TestDataFolder const commandParameters: string[] = ['templates', 'pull', dataFolder] @@ -17,7 +25,7 @@ describe('Templates command', () => { return createTemplateData() }) - after(async () => { + after(async () => { await deleteTemplateData() }) @@ -32,7 +40,7 @@ describe('Templates command', () => { } it('console out', async () => { - const {stdout} = await execa(CLICommand, commandParameters, options) + const { stdout } = await execa(CLICommand, commandParameters, options) expect(stdout).to.include('All finished') }) diff --git a/test/integration/templates.push.test.ts b/test/integration/templates.push.test.ts index 019437c..79238ef 100644 --- a/test/integration/templates.push.test.ts +++ b/test/integration/templates.push.test.ts @@ -6,12 +6,25 @@ import { DirectoryTree } from 'directory-tree' import { join } from 'path' const dirTree = require('directory-tree') -import {serverToken, CLICommand, TestDataFolder, createTemplateData, deleteTemplateData} from './shared' +import { + serverToken, + CLICommand, + TestDataFolder, + createTemplateData, + deleteTemplateData, +} from './shared' describe('Templates command', () => { - const options: execa.CommonOptions = { env: { POSTMARK_SERVER_TOKEN: serverToken }} + const options: execa.CommonOptions = { + env: { POSTMARK_SERVER_TOKEN: serverToken }, + } const dataFolder: string = TestDataFolder - const pushCommandParameters: string[] = ['templates', 'push', dataFolder, '--force'] + const pushCommandParameters: string[] = [ + 'templates', + 'push', + dataFolder, + '--force', + ] const pullCommandParameters: string[] = ['templates', 'pull', dataFolder] before(async () => { @@ -19,7 +32,7 @@ describe('Templates command', () => { return createTemplateData() }) - after(async () => { + after(async () => { await deleteTemplateData() }) @@ -46,8 +59,16 @@ describe('Templates command', () => { return f.path.includes('txt') }) - fs.writeFileSync(file.path, `test data ${Date.now().toString()}`, 'utf-8') - const { stdout } = await execa(CLICommand, pushCommandParameters, options) + fs.writeFileSync( + file.path, + `test data ${Date.now().toString()}`, + 'utf-8' + ) + const { stdout } = await execa( + CLICommand, + pushCommandParameters, + options + ) expect(stdout).to.include('All finished!') }) @@ -65,7 +86,9 @@ describe('Templates command', () => { await execa(CLICommand, pullCommandParameters, options) files = retrieveFiles(filesPath) - file = files.find((f: DirectoryTree) => { return f.path.includes('txt') }) + file = files.find((f: DirectoryTree) => { + return f.path.includes('txt') + }) const content: string = fs.readFileSync(file.path).toString('utf-8') expect(content).to.equal(contentToPush) @@ -77,10 +100,20 @@ describe('Templates command', () => { it('console out', async () => { const files = retrieveFiles(filesPath) - const file: DirectoryTree = files.find((f: DirectoryTree) => { return f.path.includes('txt') }) + const file: DirectoryTree = files.find((f: DirectoryTree) => { + return f.path.includes('txt') + }) - fs.writeFileSync(file.path, `test data ${Date.now().toString()} {{{@content}}}`, 'utf-8') - const { stdout } = await execa(CLICommand, pushCommandParameters, options) + fs.writeFileSync( + file.path, + `test data ${Date.now().toString()} {{{@content}}}`, + 'utf-8' + ) + const { stdout } = await execa( + CLICommand, + pushCommandParameters, + options + ) expect(stdout).to.include('All finished!') }) @@ -98,7 +131,9 @@ describe('Templates command', () => { await execa(CLICommand, pullCommandParameters, options) files = retrieveFiles(filesPath) - file = files.find((f: DirectoryTree) => { return f.path.includes('txt') }) + file = files.find((f: DirectoryTree) => { + return f.path.includes('txt') + }) const content: string = fs.readFileSync(file.path).toString('utf-8') expect(content).to.equal(contentToPush) From 1c01bab073e47059a0dc24d6602f025ddf38617e Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Wed, 26 Jun 2019 11:19:49 -0700 Subject: [PATCH 21/25] Update tests to work with new folder structure --- test/integration/templates.pull.test.ts | 22 +++++++++++++--------- test/integration/templates.push.test.ts | 19 +++++++++++-------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/test/integration/templates.pull.test.ts b/test/integration/templates.pull.test.ts index 87293bd..9e0fe0e 100644 --- a/test/integration/templates.pull.test.ts +++ b/test/integration/templates.pull.test.ts @@ -34,8 +34,13 @@ describe('Templates command', () => { }) describe('Pull', () => { - function retrieveFiles(path: string) { - const folderTree = dirTree(path) + function retrieveFiles(path: string, excludeLayouts?: boolean) { + const folderTree = dirTree( + path, + excludeLayouts && { + exclude: /_layouts$/, + } + ) return folderTree.children[0].children } @@ -45,18 +50,17 @@ describe('Templates command', () => { }) describe('Templates', () => { - const filesPath = join(dataFolder, 'templates') - it('templates', async () => { await execa(CLICommand, commandParameters, options) - const folderTree = dirTree(filesPath) + const folderTree = dirTree(dataFolder, { + exclude: /_layouts$/, + }) expect(folderTree.children.length).to.be.gt(0) }) it('single template - file names', async () => { await execa(CLICommand, commandParameters, options) - const files = retrieveFiles(filesPath) - + const files = retrieveFiles(dataFolder, true) const names: string[] = files.map((f: any) => { return f.name }) @@ -66,7 +70,7 @@ describe('Templates command', () => { it('single template files - none empty', async () => { await execa(CLICommand, commandParameters, options) - const files = retrieveFiles(filesPath) + const files = retrieveFiles(dataFolder) let result = files.findIndex((f: any) => { return f.size <= 0 @@ -76,7 +80,7 @@ describe('Templates command', () => { }) describe('Layouts', () => { - const filesPath = join(dataFolder, 'layouts') + const filesPath = join(dataFolder, '_layouts') it('layouts', async () => { await execa(CLICommand, commandParameters, options) diff --git a/test/integration/templates.push.test.ts b/test/integration/templates.push.test.ts index 79238ef..42740a2 100644 --- a/test/integration/templates.push.test.ts +++ b/test/integration/templates.push.test.ts @@ -41,8 +41,13 @@ describe('Templates command', () => { }) describe('Push', () => { - function retrieveFiles(path: string) { - const folderTree = dirTree(path) + function retrieveFiles(path: string, excludeLayouts?: boolean) { + const folderTree = dirTree( + path, + excludeLayouts && { + exclude: /_layouts$/, + } + ) return folderTree.children[0].children } @@ -51,10 +56,8 @@ describe('Templates command', () => { }) describe('Templates', () => { - const filesPath = join(dataFolder, 'templates') - it('console out', async () => { - const files = retrieveFiles(filesPath) + const files = retrieveFiles(dataFolder, true) const file: DirectoryTree = files.find((f: DirectoryTree) => { return f.path.includes('txt') }) @@ -73,7 +76,7 @@ describe('Templates command', () => { }) it('file content', async () => { - let files = retrieveFiles(filesPath) + let files = retrieveFiles(dataFolder, true) let file: DirectoryTree = files.find((f: DirectoryTree) => { return f.path.includes('txt') }) @@ -85,7 +88,7 @@ describe('Templates command', () => { fs.removeSync(dataFolder) await execa(CLICommand, pullCommandParameters, options) - files = retrieveFiles(filesPath) + files = retrieveFiles(dataFolder, true) file = files.find((f: DirectoryTree) => { return f.path.includes('txt') }) @@ -96,7 +99,7 @@ describe('Templates command', () => { }) describe('Layouts', () => { - const filesPath = join(dataFolder, 'layouts') + const filesPath = join(dataFolder, '_layouts') it('console out', async () => { const files = retrieveFiles(filesPath) From 1c69bb7cce6ed3027c15beeadc983e790e3ca753 Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Wed, 26 Jun 2019 15:45:25 -0700 Subject: [PATCH 22/25] Pushing templates no longer determines template type based on directory structure - Read TemplateType field from meta.json - Ability to traverse any folder structure --- package-lock.json | 10 +++++ package.json | 2 + src/commands/templates/pull.ts | 3 +- src/commands/templates/push.ts | 74 +++++++++++++++------------------- src/types/Template.ts | 9 +++++ 5 files changed, 56 insertions(+), 42 deletions(-) diff --git a/package-lock.json b/package-lock.json index eef694d..971512b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -141,6 +141,11 @@ "@types/node": "*" } }, + "@types/traverse": { + "version": "0.6.32", + "resolved": "https://registry.npmjs.org/@types/traverse/-/traverse-0.6.32.tgz", + "integrity": "sha512-RBz2uRZVCXuMg93WD//aTS5B120QlT4lR/gL+935QtGsKHLS6sCtZBaKfWjIfk7ZXv/r8mtGbwjVIee6/3XTow==" + }, "@types/yargs": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-13.0.0.tgz", @@ -2081,6 +2086,11 @@ } } }, + "traverse": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", + "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" + }, "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 78c150d..f220129 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "description": "A CLI tool for managing templates, sending emails, and fetching servers on Postmark.", "main": "./dist/index.js", "dependencies": { + "@types/traverse": "^0.6.32", "chalk": "^2.4.2", "fs-extra": "^7.0.1", "inquirer": "^6.2.1", @@ -12,6 +13,7 @@ "postmark": "^2.2.7", "request": "^2.88.0", "table": "^5.2.0", + "traverse": "^0.6.6", "untildify": "^4.0.0", "yargonaut": "^1.1.4", "yargs": "^13.2.4" diff --git a/src/commands/templates/pull.ts b/src/commands/templates/pull.ts index f5eabc4..b024015 100644 --- a/src/commands/templates/pull.ts +++ b/src/commands/templates/pull.ts @@ -67,7 +67,7 @@ const overwritePrompt = (serverToken: string, outputdirectory: string) => { type: 'confirm', name: 'overwrite', default: false, - message: `Are you sure you want to overwrite the files in ${outputdirectory}?`, + message: `Overwrite the files in ${outputdirectory}?`, }, ]).then((answer: any) => { if (answer.overwrite) { @@ -195,6 +195,7 @@ const saveTemplate = (outputDir: string, template: Template) => { Name: template.Name, Alias: template.Alias, ...(template.Subject && { Subject: template.Subject }), + TemplateType: template.TemplateType, ...(template.TemplateType === 'Standard' && { LayoutTemplate: template.LayoutTemplate, }), diff --git a/src/commands/templates/push.ts b/src/commands/templates/push.ts index 31fc74b..345fa1d 100644 --- a/src/commands/templates/push.ts +++ b/src/commands/templates/push.ts @@ -1,18 +1,13 @@ import chalk from 'chalk' import ora from 'ora' -import { join } from 'path' +import { join, dirname } from 'path' import { find } from 'lodash' import { prompt } from 'inquirer' +import traverse from 'traverse' import { table, getBorderCharacters } from 'table' import untildify from 'untildify' -import { - readJsonSync, - readFileSync, - readdirSync, - existsSync, - statSync, -} from 'fs-extra' - +import { readJsonSync, readFileSync, existsSync } from 'fs-extra' +import dirTree from 'directory-tree' import { ServerClient } from 'postmark' import { TemplateManifest, @@ -20,6 +15,8 @@ import { TemplatePushReview, TemplatePushArguments, Templates, + MetaTraverse, + MetaFile, } from '../../types' import { pluralize, log, validateToken } from '../../utils' @@ -181,55 +178,50 @@ const layoutUsedLabel = ( return label } -/** - * Gather up templates on the file system - */ -const createManifest = (path: string): TemplateManifest[] => [ - ...parseDirectory('Layout', path), - ...parseDirectory('Standard', path), -] - /** * Parses directory of templates or layouts */ -const parseDirectory = (type: 'Layout' | 'Standard', rootPath: string) => { +const createManifest = (path: string): TemplateManifest[] => { let manifest: TemplateManifest[] = [] - const path = type === 'Layout' ? join(rootPath, '_layouts') : rootPath + // const path = type === 'Layout' ? join(rootPath, '_layouts') : rootPath // Do not parse if directory does not exist if (!existsSync(path)) return manifest - // Get top level directory names - const list = readdirSync(path).filter(f => - statSync(join(path, f)).isDirectory() - ) + // Get directory tree + const tree = dirTree(path) + + // Find meta files and flatten into collection + const list: MetaTraverse[] = traverse(tree).reduce((acc, file) => { + if (file.name === 'meta.json') acc.push(file) + return acc + }, []) + + // console.log(util.inspect(tree, false, null, true)) + // console.log(util.inspect(list, false, null, true)) // Parse each directory - list.forEach(dir => { - const metaPath = join(path, join(dir, 'meta.json')) - const htmlPath = join(path, join(dir, 'content.html')) - const textPath = join(path, join(dir, 'content.txt')) - let template: TemplateManifest = { - TemplateType: type, - ...(type === 'Standard' && { LayoutTemplate: null }), - } + list.forEach(file => { + const { path } = file + const rootPath = dirname(path) + const htmlPath = join(rootPath, 'content.html') + const textPath = join(rootPath, 'content.txt') // Check if meta file exists - if (existsSync(metaPath)) { - // Read HTML and Text content from files - template.HtmlBody = existsSync(htmlPath) + if (existsSync(path)) { + const metaFile: MetaFile = readJsonSync(path) + const htmlFile: string = existsSync(htmlPath) ? readFileSync(htmlPath, 'utf-8') : '' - template.TextBody = existsSync(textPath) + const textFile: string = existsSync(textPath) ? readFileSync(textPath, 'utf-8') : '' - // Ensure HTML body or Text content exists - if (template.HtmlBody !== '' || template.TextBody !== '') { - // Assign contents of meta.json to object - template = Object.assign(template, readJsonSync(metaPath)) - manifest.push(template) - } + manifest.push({ + HtmlBody: htmlFile, + TextBody: textFile, + ...metaFile, + }) } }) diff --git a/src/types/Template.ts b/src/types/Template.ts index 4f806b2..acc1d66 100644 --- a/src/types/Template.ts +++ b/src/types/Template.ts @@ -69,6 +69,15 @@ export interface TemplatePushArguments { export interface MetaFile { Name: string Alias: string + TemplateType: string Subject?: string LayoutTemplate?: string | null } + +export interface MetaTraverse { + path: string + name: string + size: number + extension: string + type: string +} From 43e684eb0d42047cc3f72164039eaea88176fb5a Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Wed, 26 Jun 2019 16:00:18 -0700 Subject: [PATCH 23/25] Clean up --- src/commands/templates/push.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/commands/templates/push.ts b/src/commands/templates/push.ts index 345fa1d..91abd12 100644 --- a/src/commands/templates/push.ts +++ b/src/commands/templates/push.ts @@ -183,7 +183,6 @@ const layoutUsedLabel = ( */ const createManifest = (path: string): TemplateManifest[] => { let manifest: TemplateManifest[] = [] - // const path = type === 'Layout' ? join(rootPath, '_layouts') : rootPath // Do not parse if directory does not exist if (!existsSync(path)) return manifest @@ -197,9 +196,6 @@ const createManifest = (path: string): TemplateManifest[] => { return acc }, []) - // console.log(util.inspect(tree, false, null, true)) - // console.log(util.inspect(list, false, null, true)) - // Parse each directory list.forEach(file => { const { path } = file From 9a6cfb585dc43c6b2001e1eeb4c383745ac8385e Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Wed, 26 Jun 2019 16:04:17 -0700 Subject: [PATCH 24/25] More clean up --- src/commands/templates/push.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/commands/templates/push.ts b/src/commands/templates/push.ts index 91abd12..573f0c0 100644 --- a/src/commands/templates/push.ts +++ b/src/commands/templates/push.ts @@ -187,21 +187,18 @@ const createManifest = (path: string): TemplateManifest[] => { // Do not parse if directory does not exist if (!existsSync(path)) return manifest - // Get directory tree - const tree = dirTree(path) - // Find meta files and flatten into collection - const list: MetaTraverse[] = traverse(tree).reduce((acc, file) => { + const list: MetaTraverse[] = traverse(dirTree(path)).reduce((acc, file) => { if (file.name === 'meta.json') acc.push(file) return acc }, []) // Parse each directory list.forEach(file => { - const { path } = file - const rootPath = dirname(path) - const htmlPath = join(rootPath, 'content.html') - const textPath = join(rootPath, 'content.txt') + const { path } = file // Path to meta file + const rootPath = dirname(path) // Folder path + const htmlPath = join(rootPath, 'content.html') // HTML path + const textPath = join(rootPath, 'content.txt') // Text path // Check if meta file exists if (existsSync(path)) { From bc57e61ed5e38d1fea6727068e6b472695cc632a Mon Sep 17 00:00:00 2001 From: Derek Rushforth Date: Thu, 27 Jun 2019 08:40:30 -0700 Subject: [PATCH 25/25] Code refactor --- src/commands/templates/push.ts | 129 ++++++++++++++++++++------------- src/types/Template.ts | 4 +- 2 files changed, 81 insertions(+), 52 deletions(-) diff --git a/src/commands/templates/push.ts b/src/commands/templates/push.ts index 573f0c0..c6a7eec 100644 --- a/src/commands/templates/push.ts +++ b/src/commands/templates/push.ts @@ -15,7 +15,7 @@ import { TemplatePushReview, TemplatePushArguments, Templates, - MetaTraverse, + MetaFileTraverse, MetaFile, } from '../../types' import { pluralize, log, validateToken } from '../../utils' @@ -83,25 +83,19 @@ const push = (serverToken: string, args: TemplatePushArguments) => { .then(response => { compareTemplates(response, manifest) + // Show which templates are changing spinner.stop() printReview(review) - // Push templates + // Push templates if force arg is present if (force) { spinner.text = 'Pushing templates to Postmark...' spinner.start() return pushTemplates(spinner, client, manifest) } - // User confirmation before pushing - prompt([ - { - type: 'confirm', - name: 'confirm', - default: false, - message: `Would you like to continue?`, - }, - ]).then((answer: any) => { + // Ask for user confirmation + confirmation().then(answer => { if (answer.confirm) { spinner.text = 'Pushing templates to Postmark...' spinner.start() @@ -123,6 +117,23 @@ const push = (serverToken: string, args: TemplatePushArguments) => { } } +/** + * 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)) + }) + /** * Compare templates on server against local */ @@ -179,48 +190,64 @@ const layoutUsedLabel = ( } /** - * Parses directory of templates or layouts + * Parses templates folder and files */ const createManifest = (path: string): TemplateManifest[] => { let manifest: TemplateManifest[] = [] - // Do not parse if directory does not exist + // Return empty array if path does not exist if (!existsSync(path)) return manifest // Find meta files and flatten into collection - const list: MetaTraverse[] = traverse(dirTree(path)).reduce((acc, file) => { - if (file.name === 'meta.json') acc.push(file) - return acc - }, []) + const list: MetaFileTraverse[] = FindMetaFiles(path) // Parse each directory list.forEach(file => { - const { path } = file // Path to meta file - const rootPath = dirname(path) // Folder path - const htmlPath = join(rootPath, 'content.html') // HTML path - const textPath = join(rootPath, 'content.txt') // Text path - - // Check if meta file exists - if (existsSync(path)) { - const metaFile: MetaFile = readJsonSync(path) - const htmlFile: string = existsSync(htmlPath) - ? readFileSync(htmlPath, 'utf-8') - : '' - const textFile: string = existsSync(textPath) - ? readFileSync(textPath, 'utf-8') - : '' - - manifest.push({ - HtmlBody: htmlFile, - TextBody: textFile, - ...metaFile, - }) - } + const item = createManifestItem(file) + if (item) manifest.push(item) }) return manifest } +/** + * Gathers the template's content and metadata based on the metadata file location + */ +const createManifestItem = (file: any): MetaFile | null => { + const { path } = file // Path to meta file + const rootPath = dirname(path) // Folder path + const htmlPath = join(rootPath, 'content.html') // HTML path + const textPath = join(rootPath, 'content.txt') // Text path + + // Check if meta file exists + if (existsSync(path)) { + const metaFile: MetaFile = readJsonSync(path) + const htmlFile: string = existsSync(htmlPath) + ? readFileSync(htmlPath, 'utf-8') + : '' + const textFile: string = existsSync(textPath) + ? readFileSync(textPath, 'utf-8') + : '' + + return { + HtmlBody: htmlFile, + TextBody: textFile, + ...metaFile, + } + } + + return null +} + +/** + * Searches for all metadata files and flattens into a collection + */ +const FindMetaFiles = (path: string): MetaFileTraverse[] => + traverse(dirTree(path)).reduce((acc, file) => { + if (file.name === 'meta.json') acc.push(file) + return acc + }, []) + /** * Show which templates will change after the publish */ @@ -277,9 +304,9 @@ const pushTemplates = ( client: any, templates: TemplateManifest[] ) => { - templates.forEach(template => { + templates.forEach(template => pushTemplate(spinner, client, template, templates.length) - }) + ) } /** @@ -290,9 +317,9 @@ const pushTemplate = ( client: any, template: TemplateManifest, total: number -) => { +): void => { if (template.New) { - client + return client .createTemplate(template) .then((response: object) => pushComplete(true, response, template, spinner, total) @@ -300,16 +327,16 @@ const pushTemplate = ( .catch((response: object) => pushComplete(false, response, template, spinner, total) ) - } else { - client - .editTemplate(template.Alias, template) - .then((response: object) => - pushComplete(true, response, template, spinner, total) - ) - .catch((response: object) => - pushComplete(false, response, template, spinner, total) - ) } + + return client + .editTemplate(template.Alias, template) + .then((response: object) => + pushComplete(true, response, template, spinner, total) + ) + .catch((response: object) => + pushComplete(false, response, template, spinner, total) + ) } /** diff --git a/src/types/Template.ts b/src/types/Template.ts index acc1d66..579bce4 100644 --- a/src/types/Template.ts +++ b/src/types/Template.ts @@ -72,9 +72,11 @@ export interface MetaFile { TemplateType: string Subject?: string LayoutTemplate?: string | null + HtmlBody?: string + TextBody?: string } -export interface MetaTraverse { +export interface MetaFileTraverse { path: string name: string size: number