Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/zcli-themes/src/commands/themes/migrate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Command } from '@oclif/core'
import * as path from 'path'
import migrate from '../../lib/migrate'

export default class Migrate extends Command {
static description = 'migrate theme to the latest version of the templating api'

static hidden = true

static args = [
{ name: 'themeDirectory', required: true, default: '.' }
]

static examples = [
'$ zcli themes:migrate ./copenhagen_theme'
]

static strict = false

async run () {
const { flags, argv: [themeDirectory] } = await this.parse(Migrate)
const themePath = path.resolve(themeDirectory)

await migrate(themePath, flags)
}
}
104 changes: 104 additions & 0 deletions packages/zcli-themes/src/lib/handleTemplateError.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import * as sinon from 'sinon'
import { expect } from '@oclif/test'
import * as validationErrorsToString from './validationErrorsToString'
import * as errors from '@oclif/core/lib/errors'
import handleTemplateError from './handleTemplateError'

describe('handleTemplateError', () => {
beforeEach(() => {
sinon.restore()
})

it('transforms template identifiers to template paths and calls error', () => {
const validationErrorsToStringStub = sinon.stub(validationErrorsToString, 'default').returns('formatted errors')
const errorStub = sinon.stub(errors, 'error')

const templateErrors = {
home_page: [
{
description: 'not possible to access `names`',
line: 1,
column: 45,
length: 5
}
],
article_page: [
{
description: "'articles' does not exist",
line: 21,
column: 16,
length: 11
}
]
}

handleTemplateError('theme/path', templateErrors)

expect(validationErrorsToStringStub.calledOnce).to.equal(true)
expect(validationErrorsToStringStub.firstCall.args[0]).to.equal('theme/path')

const transformedErrors = validationErrorsToStringStub.firstCall.args[1]
expect(transformedErrors).to.have.property('templates/home_page.hbs')
expect(transformedErrors).to.have.property('templates/article_page.hbs')
expect(transformedErrors['templates/home_page.hbs']).to.deep.equal([
{
description: 'not possible to access `names`',
line: 1,
column: 45,
length: 5
}
])

expect(errorStub.calledOnce).to.equal(true)
expect(errorStub.firstCall.args[0]).to.contain('InvalidTemplates')
expect(errorStub.firstCall.args[0]).to.contain('Template(s) with syntax error(s)')
expect(errorStub.firstCall.args[0]).to.contain('formatted errors')
})

it('handles empty template errors', () => {
const validationErrorsToStringStub = sinon.stub(validationErrorsToString, 'default').returns('')
const errorStub = sinon.stub(errors, 'error')

handleTemplateError('theme/path', {})

expect(validationErrorsToStringStub.calledOnce).to.equal(true)
expect(validationErrorsToStringStub.firstCall.args[1]).to.deep.equal({})
expect(errorStub.calledOnce).to.equal(true)
})

it('handles single template with multiple errors', () => {
const validationErrorsToStringStub = sinon.stub(validationErrorsToString, 'default').returns('multiple errors')
sinon.stub(errors, 'error')

const templateErrors = {
new_request_page: [
{
description: 'First error',
line: 1,
column: 1,
length: 5
},
{
description: 'Second error',
line: 10,
column: 5,
length: 3
},
{
description: 'Third error',
line: 20,
column: 10,
length: 7
}
]
}

handleTemplateError('/my/theme', templateErrors)

const transformedErrors = validationErrorsToStringStub.firstCall.args[1]
expect(transformedErrors['templates/new_request_page.hbs']).to.have.length(3)
expect(transformedErrors['templates/new_request_page.hbs'][0].description).to.equal('First error')
expect(transformedErrors['templates/new_request_page.hbs'][1].description).to.equal('Second error')
expect(transformedErrors['templates/new_request_page.hbs'][2].description).to.equal('Third error')
})
})
19 changes: 19 additions & 0 deletions packages/zcli-themes/src/lib/handleTemplateError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { ValidationError, ValidationErrors } from '../types'
import validationErrorsToString from './validationErrorsToString'
import * as chalk from 'chalk'
import { error } from '@oclif/core/lib/errors'

export default function handleTemplateError (themePath: string, templateErrors: ValidationErrors) {
const validationErrors: ValidationErrors = {}
for (const [template, errors] of Object.entries(templateErrors)) {
// the theming endpoints return the template identifier as the 'key' instead of
// the template path. We must fix this so we can reuse `validationErrorsToString`
// and align with the job import error handling
validationErrors[`templates/${template}.hbs`] = errors as ValidationError[]
}

const title = `${chalk.bold('InvalidTemplates')} - Template(s) with syntax error(s)`
const details = validationErrorsToString(themePath, validationErrors)

error(`${title}\n${details}`)
}
Comment on lines +1 to +19
Copy link

Copilot AI Jan 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The handleTemplateError function lacks unit tests. This is an extracted function used by both preview and migrate commands, and verifying its behavior in isolation would improve maintainability. Tests should verify that it correctly transforms template identifiers to file paths and properly formats error messages.

Copilot uses AI. Check for mistakes.
12 changes: 8 additions & 4 deletions packages/zcli-themes/src/lib/handleThemeApiError.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import type { AxiosError, AxiosResponse } from 'axios'
import * as chalk from 'chalk'
import { error } from '@oclif/core/lib/errors'
import parseAxiosError from './parseAxiosError'

export default function handleThemeApiError (e: AxiosError): never {
const { response, message } = e
const { message, response } = parseAxiosError(e)

if (response) {
const { errors } = (response as AxiosResponse).data
for (const { code, title } of errors) {
error(`${chalk.bold(code)} - ${title}`)
const data = (response as AxiosResponse).data

if (data && typeof data === 'object' && 'errors' in data && Array.isArray(data.errors)) {
for (const { code, title } of data.errors) {
error(`${chalk.bold(code)} - ${title}`)
}
}
} else if (message) {
error(message)
Expand Down
Loading
Loading