diff --git a/packages/zcli-themes/src/commands/themes/migrate.ts b/packages/zcli-themes/src/commands/themes/migrate.ts index 6777eca2..55a649df 100644 --- a/packages/zcli-themes/src/commands/themes/migrate.ts +++ b/packages/zcli-themes/src/commands/themes/migrate.ts @@ -18,9 +18,9 @@ export default class Migrate extends Command { static strict = false async run () { - const { flags, argv: [themeDirectory] } = await this.parse(Migrate) + const { argv: [themeDirectory] } = await this.parse(Migrate) const themePath = path.resolve(themeDirectory) - await migrate(themePath, flags) + await migrate(themePath) } } diff --git a/packages/zcli-themes/src/commands/themes/preview.ts b/packages/zcli-themes/src/commands/themes/preview.ts index aa3412bd..97dd6e3d 100644 --- a/packages/zcli-themes/src/commands/themes/preview.ts +++ b/packages/zcli-themes/src/commands/themes/preview.ts @@ -99,7 +99,7 @@ export default class Preview extends Command { `${themePath}/style.css` ] - const watcher = chokidar.watch(monitoredPaths).on('change', async (path) => { + const handleThemeChange = async (path: string) => { this.log(chalk.bold('Change'), path) try { await preview(themePath, flags) @@ -111,7 +111,12 @@ export default class Preview extends Command { } catch (e) { this.error(e as Error, { exit: false }) } - }) + } + + const watcher = chokidar.watch(monitoredPaths, { ignoreInitial: true }) + .on('add', handleThemeChange) + .on('change', handleThemeChange) + .on('unlink', handleThemeChange) return { close: () => { diff --git a/packages/zcli-themes/src/lib/getAssets.test.ts b/packages/zcli-themes/src/lib/getAssets.test.ts index 2de99962..7cfefe80 100644 --- a/packages/zcli-themes/src/lib/getAssets.test.ts +++ b/packages/zcli-themes/src/lib/getAssets.test.ts @@ -39,6 +39,30 @@ describe('getAssets', () => { ]) }) + it('returns basenames as urls when flags are not provided', () => { + const existsSyncStub = sinon.stub(fs, 'existsSync') + const readdirSyncStub = sinon.stub(fs, 'readdirSync') + + existsSyncStub + .withArgs('theme/path/assets') + .returns(true) + + readdirSyncStub.returns(['foo.png', 'bar.png'] as any) + + const assets = getAssets('theme/path') + + expect(assets).to.deep.equal([ + [ + { base: 'foo.png', dir: '', ext: '.png', name: 'foo', root: '' }, + 'foo.png' + ], + [ + { base: 'bar.png', dir: '', ext: '.png', name: 'bar', root: '' }, + 'bar.png' + ] + ]) + }) + it('throws an error when an asset has illegal characters in its name', () => { const existsSyncStub = sinon.stub(fs, 'existsSync') const readdirSyncStub = sinon.stub(fs, 'readdirSync') diff --git a/packages/zcli-themes/src/lib/getAssets.ts b/packages/zcli-themes/src/lib/getAssets.ts index 38d8a798..53fdef76 100644 --- a/packages/zcli-themes/src/lib/getAssets.ts +++ b/packages/zcli-themes/src/lib/getAssets.ts @@ -4,7 +4,7 @@ import * as fs from 'fs' import * as path from 'path' import { getLocalServerBaseUrl } from './getLocalServerBaseUrl' -export default function getAssets (themePath: string, flags: Flags): [path.ParsedPath, string][] { +export default function getAssets (themePath: string, flags?: Flags): [path.ParsedPath, string][] { const assetsPath = `${themePath}/assets` const filenames = fs.existsSync(assetsPath) ? fs.readdirSync(assetsPath) : [] const assets: [path.ParsedPath, string][] = [] @@ -18,7 +18,8 @@ export default function getAssets (themePath: string, flags: Flags): [path.Parse ) } if (!name.startsWith('.')) { - assets.push([parsedPath, `${getLocalServerBaseUrl(flags)}/guide/assets/${filename}`]) + const url = flags ? `${getLocalServerBaseUrl(flags)}/guide/assets/${filename}` : filename + assets.push([parsedPath, url]) } }) diff --git a/packages/zcli-themes/src/lib/getVariables.test.ts b/packages/zcli-themes/src/lib/getVariables.test.ts index 44479a8a..4409a7af 100644 --- a/packages/zcli-themes/src/lib/getVariables.test.ts +++ b/packages/zcli-themes/src/lib/getVariables.test.ts @@ -40,6 +40,23 @@ describe('getVariables', () => { ]) }) + it('uses the matched filename as the value when flags are not provided', () => { + const existsSyncStub = sinon.stub(fs, 'existsSync') + const readdirSyncStub = sinon.stub(fs, 'readdirSync') + + existsSyncStub + .withArgs('theme/path/settings') + .returns(true) + + readdirSyncStub.returns(['logo.png', 'favicon.png'] as any) + + expect(getVariables('theme/path', settings)).to.deep.equal([ + { identifier: 'color', type: 'color', value: '#999' }, + { identifier: 'logo', type: 'file', value: 'logo.png' }, + { identifier: 'favicon', type: 'file', value: 'favicon.png' } + ]) + }) + it('throws an error when it doesn\'t find an asset within the settings folder for a variable of type "file"', () => { const existsSyncStub = sinon.stub(fs, 'existsSync') const readdirSyncStub = sinon.stub(fs, 'readdirSync') diff --git a/packages/zcli-themes/src/lib/getVariables.ts b/packages/zcli-themes/src/lib/getVariables.ts index ba2f0ae8..48d6a117 100644 --- a/packages/zcli-themes/src/lib/getVariables.ts +++ b/packages/zcli-themes/src/lib/getVariables.ts @@ -4,7 +4,7 @@ import * as path from 'path' import { CLIError } from '@oclif/core/lib/errors' import { getLocalServerBaseUrl } from './getLocalServerBaseUrl' -export default function getVariables (themePath: string, settings: Setting[], flags: Flags): Variable[] { +export default function getVariables (themePath: string, settings: Setting[], flags?: Flags): Variable[] { const settingsPath = `${themePath}/settings` const filenames = fs.existsSync(settingsPath) ? fs.readdirSync(settingsPath) : [] @@ -18,7 +18,7 @@ export default function getVariables (themePath: string, settings: Setting[], fl `The setting "${variable.identifier}" of type "file" does not have a matching file within the "settings" folder` ) } - variable.value = file && `${getLocalServerBaseUrl(flags)}/guide/settings/${file}` + variable.value = flags ? `${getLocalServerBaseUrl(flags)}/guide/settings/${file}` : file } return variable }) diff --git a/packages/zcli-themes/src/lib/migrate.test.ts b/packages/zcli-themes/src/lib/migrate.test.ts index 0aac8f60..03a60f0a 100644 --- a/packages/zcli-themes/src/lib/migrate.test.ts +++ b/packages/zcli-themes/src/lib/migrate.test.ts @@ -27,13 +27,6 @@ const manifest = { ] } -const flags = { - bind: 'localhost', - port: 1000, - logs: true, - livereload: false -} - describe('migrate', () => { beforeEach(() => { sinon.restore() @@ -56,16 +49,16 @@ describe('migrate', () => { 'custom_pages/faq': '

FAQ

' }) - getVariablesStub.withArgs('theme/path', manifest.settings, flags).returns([ + getVariablesStub.withArgs('theme/path', manifest.settings).returns([ { identifier: 'color', type: 'color', value: '#999' }, { identifier: 'logo', type: 'file', - value: 'http://localhost:1000/guide/settings/logo.png' + value: 'logo.png' } ]) - getAssetsStub.withArgs('theme/path', flags).returns([ + getAssetsStub.withArgs('theme/path').returns([ [ { base: 'background.png', @@ -74,7 +67,7 @@ describe('migrate', () => { name: 'background', root: '' }, - 'http://localhost:1000/guide/assets/background.png' + 'background.png' ] ]) @@ -98,7 +91,7 @@ describe('migrate', () => { }) as axios.AxiosPromise ) - await migrate('theme/path', flags) + await migrate('theme/path') expect( requestStub.calledWith( @@ -114,12 +107,11 @@ describe('migrate', () => { 'article_pages/product_updates': '

Product updates

', 'custom_pages/faq': '

FAQ

', assets: { - 'background.png': - 'http://localhost:1000/guide/assets/background.png' + 'background.png': 'background.png' }, variables: { color: '#999', - logo: 'http://localhost:1000/guide/settings/logo.png' + logo: 'logo.png' }, metadata: { api_version: 1 } } @@ -178,7 +170,7 @@ describe('migrate', () => { const errorStub = sinon.stub(errors, 'error').callThrough() try { - await migrate('theme/path', flags) + await migrate('theme/path') } catch { const [call] = errorStub.getCalls() const [error] = call.args @@ -206,7 +198,7 @@ describe('migrate', () => { const errorStub = sinon.stub(errors, 'error').callThrough() try { - await migrate('theme/path', flags) + await migrate('theme/path') } catch { const [call] = errorStub.getCalls() const [error] = call.args @@ -230,7 +222,7 @@ describe('migrate', () => { const errorStub = sinon.stub(errors, 'error').callThrough() try { - await migrate('theme/path', flags) + await migrate('theme/path') } catch { const [call] = errorStub.getCalls() const [error] = call.args @@ -250,7 +242,7 @@ describe('migrate', () => { const errorStub = sinon.stub(errors, 'error').callThrough() try { - await migrate('theme/path', flags) + await migrate('theme/path') } catch { const [call] = errorStub.getCalls() const [error] = call.args diff --git a/packages/zcli-themes/src/lib/migrate.ts b/packages/zcli-themes/src/lib/migrate.ts index 0e91c54c..a73673d1 100644 --- a/packages/zcli-themes/src/lib/migrate.ts +++ b/packages/zcli-themes/src/lib/migrate.ts @@ -1,4 +1,4 @@ -import type { Flags, ValidationErrors } from '../types' +import type { ValidationErrors } from '../types' import getManifest from './getManifest' import getTemplates from './getTemplates' import getVariables from './getVariables' @@ -14,11 +14,11 @@ import rewriteAssets from './rewriteAssets' import handleTemplateError from './handleTemplateError' import parseAxiosError from './parseAxiosError' -export default async function migrate (themePath: string, flags: Flags): Promise { +export default async function migrate (themePath: string): Promise { const manifest = getManifest(themePath) const templates = getTemplates(themePath) - const variables = getVariables(themePath, manifest.settings, flags) - const assets = getAssets(themePath, flags) + const variables = getVariables(themePath, manifest.settings) + const assets = getAssets(themePath) const variablesPayload = variables.reduce((payload, variable) => ({ ...payload, diff --git a/packages/zcli-themes/src/lib/rewriteTemplates.test.ts b/packages/zcli-themes/src/lib/rewriteTemplates.test.ts index 1899b7cd..58b04d28 100644 --- a/packages/zcli-themes/src/lib/rewriteTemplates.test.ts +++ b/packages/zcli-themes/src/lib/rewriteTemplates.test.ts @@ -9,6 +9,7 @@ describe('rewriteTemplates', () => { }) it('writes templates to the correct file paths', () => { + sinon.stub(fs, 'mkdirSync') const writeFileSyncStub = sinon.stub(fs, 'writeFileSync') const templates = { @@ -35,6 +36,7 @@ describe('rewriteTemplates', () => { }) it('throws an error when file cannot be written', () => { + sinon.stub(fs, 'mkdirSync') const writeFileSyncStub = sinon.stub(fs, 'writeFileSync') writeFileSyncStub.throws(new Error('Permission denied')) @@ -58,7 +60,8 @@ describe('rewriteTemplates', () => { expect(writeFileSyncStub.callCount).to.equal(0) }) - it('handles nested template paths correctly', () => { + it('creates intermediate directories for nested template paths', () => { + const mkdirSyncStub = sinon.stub(fs, 'mkdirSync') const writeFileSyncStub = sinon.stub(fs, 'writeFileSync') const templates = { @@ -68,6 +71,15 @@ describe('rewriteTemplates', () => { rewriteTemplates('theme/path', templates) + expect(mkdirSyncStub.callCount).to.equal(2) + expect(mkdirSyncStub.firstCall.args).to.deep.equal([ + 'theme/path/templates/article_pages', + { recursive: true } + ]) + expect(mkdirSyncStub.secondCall.args).to.deep.equal([ + 'theme/path/templates/custom_pages/deep/nested', + { recursive: true } + ]) expect(writeFileSyncStub.callCount).to.equal(2) expect(writeFileSyncStub.firstCall.args).to.deep.equal([ 'theme/path/templates/article_pages/product_updates.hbs', diff --git a/packages/zcli-themes/src/lib/rewriteTemplates.ts b/packages/zcli-themes/src/lib/rewriteTemplates.ts index 8d88e4e0..a7a24d09 100644 --- a/packages/zcli-themes/src/lib/rewriteTemplates.ts +++ b/packages/zcli-themes/src/lib/rewriteTemplates.ts @@ -1,5 +1,6 @@ import { CLIError } from '@oclif/core/lib/errors' import * as fs from 'fs' +import * as path from 'path' import * as chalk from 'chalk' export default function rewriteTemplates (themePath: string, templates: Record) { @@ -8,6 +9,7 @@ export default function rewriteTemplates (themePath: string, templates: Record { expect(string).to.contain('templates/new_request_page.hbs') expect(string).to.contain("'post_form' does not exist") }) + + it('includes the :line:column suffix when line or column is 0', () => { + const validationErrors = { + 'templates/home_page.hbs': [ + { + description: 'error at start of file', + line: 0, + column: 0, + length: 1 + } + ] + } + + const string = validationErrorsToString('theme/path', validationErrors) + + expect(string).to.contain('theme/path/templates/home_page.hbs:0:0') + }) + + it('omits the suffix when line and column are not provided', () => { + const validationErrors = { + 'templates/home_page.hbs': [ + { + description: 'error without location' + } + ] + } + + const string = validationErrorsToString('theme/path', validationErrors) + + expect(string).to.contain('theme/path/templates/home_page.hbs\n') + expect(string).to.not.contain('home_page.hbs:') + }) }) diff --git a/packages/zcli-themes/src/lib/validationErrorsToString.ts b/packages/zcli-themes/src/lib/validationErrorsToString.ts index b30a138d..6e1668d7 100644 --- a/packages/zcli-themes/src/lib/validationErrorsToString.ts +++ b/packages/zcli-themes/src/lib/validationErrorsToString.ts @@ -6,7 +6,7 @@ export default function validationErrorsToString (themePath: string, validationE for (const [template, errors] of Object.entries(validationErrors)) { for (const { line, column, description } of errors) { - string += `\n${chalk.bold('Validation error')} ${themePath}/${template}${line && column ? `:${line}:${column}` : ''}\n ${description}\n` + string += `\n${chalk.bold('Validation error')} ${themePath}/${template}${line !== undefined && column !== undefined ? `:${line}:${column}` : ''}\n ${description}\n` } }