diff --git a/packages/zcli-themes/src/lib/migrate.test.ts b/packages/zcli-themes/src/lib/migrate.test.ts
index 8bd4d01a..0aac8f60 100644
--- a/packages/zcli-themes/src/lib/migrate.test.ts
+++ b/packages/zcli-themes/src/lib/migrate.test.ts
@@ -6,6 +6,7 @@ import * as getVariables from './getVariables'
import * as getAssets from './getAssets'
import * as rewriteManifest from './rewriteManifest'
import * as rewriteTemplates from './rewriteTemplates'
+import * as rewriteAssets from './rewriteAssets'
import * as axios from 'axios'
import { request } from '@zendesk/zcli-core'
import * as errors from '@oclif/core/lib/errors'
@@ -45,6 +46,7 @@ describe('migrate', () => {
const getAssetsStub = sinon.stub(getAssets, 'default')
const rewriteManifestStub = sinon.stub(rewriteManifest, 'default')
const rewriteTemplatesStub = sinon.stub(rewriteTemplates, 'default')
+ const rewriteAssetsStub = sinon.stub(rewriteAssets, 'default')
const requestStub = sinon.stub(request, 'requestAPI')
getManifestStub.withArgs('theme/path').returns(manifest)
@@ -88,6 +90,9 @@ describe('migrate', () => {
home_page: '
Updated Home
',
'article_pages/product_updates': 'Updated Product updates
',
'custom_pages/faq': 'Updated FAQ
'
+ },
+ assets: {
+ 'category_tree.js': Buffer.from('console.log("tree")').toString('base64')
}
}
}) as axios.AxiosPromise
@@ -131,6 +136,12 @@ describe('migrate', () => {
'custom_pages/faq': 'Updated FAQ
'
})
).to.equal(true)
+
+ expect(
+ rewriteAssetsStub.calledWith('theme/path', {
+ 'category_tree.js': Buffer.from('console.log("tree")').toString('base64')
+ })
+ ).to.equal(true)
})
it('throws an error when validation fails with template errors', async () => {
diff --git a/packages/zcli-themes/src/lib/migrate.ts b/packages/zcli-themes/src/lib/migrate.ts
index f8d9db2b..0e91c54c 100644
--- a/packages/zcli-themes/src/lib/migrate.ts
+++ b/packages/zcli-themes/src/lib/migrate.ts
@@ -10,6 +10,7 @@ import { CliUx } from '@oclif/core'
import type { AxiosError } from 'axios'
import rewriteTemplates from './rewriteTemplates'
import rewriteManifest from './rewriteManifest'
+import rewriteAssets from './rewriteAssets'
import handleTemplateError from './handleTemplateError'
import parseAxiosError from './parseAxiosError'
@@ -50,6 +51,7 @@ export default async function migrate (themePath: string, flags: Flags): Promise
})
rewriteManifest(themePath, data.metadata.api_version)
rewriteTemplates(themePath, data.templates)
+ rewriteAssets(themePath, data.assets)
CliUx.ux.action.stop('Ok')
} catch (e) {
CliUx.ux.action.stop(chalk.bold.red('!'))
diff --git a/packages/zcli-themes/src/lib/rewriteAssets.test.ts b/packages/zcli-themes/src/lib/rewriteAssets.test.ts
new file mode 100644
index 00000000..8ea00e39
--- /dev/null
+++ b/packages/zcli-themes/src/lib/rewriteAssets.test.ts
@@ -0,0 +1,86 @@
+import * as sinon from 'sinon'
+import * as fs from 'fs'
+import { expect } from '@oclif/test'
+import rewriteAssets from './rewriteAssets'
+
+describe('rewriteAssets', () => {
+ beforeEach(() => {
+ sinon.restore()
+ })
+
+ it('decodes base64 content and writes assets to the correct file paths', () => {
+ const mkdirSyncStub = sinon.stub(fs, 'mkdirSync')
+ const writeFileSyncStub = sinon.stub(fs, 'writeFileSync')
+
+ const jsContent = Buffer.from('console.log("hello")').toString('base64')
+
+ rewriteAssets('theme/path', {
+ 'script.js': jsContent
+ })
+
+ expect(mkdirSyncStub.calledOnce).to.equal(true)
+ expect(mkdirSyncStub.firstCall.args[1]).to.deep.equal({ recursive: true })
+
+ expect(writeFileSyncStub.callCount).to.equal(1)
+ expect(writeFileSyncStub.firstCall.args[0]).to.equal('theme/path/assets/script.js')
+ expect(Buffer.compare(
+ writeFileSyncStub.firstCall.args[1] as Buffer,
+ Buffer.from('console.log("hello")')
+ )).to.equal(0)
+ })
+
+ it('handles multiple file types', () => {
+ sinon.stub(fs, 'mkdirSync')
+ const writeFileSyncStub = sinon.stub(fs, 'writeFileSync')
+
+ const jsContent = Buffer.from('var a = 1;').toString('base64')
+ const pngContent = Buffer.from([0x89, 0x50, 0x4e, 0x47]).toString('base64')
+
+ rewriteAssets('theme/path', {
+ 'app.js': jsContent,
+ 'logo.png': pngContent
+ })
+
+ expect(writeFileSyncStub.callCount).to.equal(2)
+ expect(writeFileSyncStub.firstCall.args[0]).to.equal('theme/path/assets/app.js')
+ expect(writeFileSyncStub.secondCall.args[0]).to.equal('theme/path/assets/logo.png')
+
+ expect(Buffer.compare(
+ writeFileSyncStub.secondCall.args[1] as Buffer,
+ Buffer.from([0x89, 0x50, 0x4e, 0x47])
+ )).to.equal(0)
+ })
+
+ it('handles empty assets object', () => {
+ sinon.stub(fs, 'mkdirSync')
+ const writeFileSyncStub = sinon.stub(fs, 'writeFileSync')
+
+ rewriteAssets('theme/path', {})
+
+ expect(writeFileSyncStub.callCount).to.equal(0)
+ })
+
+ 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'))
+
+ const jsContent = Buffer.from('first').toString('base64')
+
+ expect(() => {
+ rewriteAssets('theme/path', { 'a.js': jsContent })
+ }).to.throw('Failed to write asset file: theme/path/assets/a.js')
+ })
+
+ it('throws an error when assets directory cannot be created', () => {
+ sinon.stub(fs, 'mkdirSync').throws(new Error('Permission denied'))
+ sinon.stub(fs, 'writeFileSync')
+
+ const jsContent = Buffer.from('hello').toString('base64')
+
+ expect(() => {
+ rewriteAssets('theme/path', { 'a.js': jsContent })
+ }).to.throw('Failed to create assets directory: theme/path/assets')
+ })
+})
diff --git a/packages/zcli-themes/src/lib/rewriteAssets.ts b/packages/zcli-themes/src/lib/rewriteAssets.ts
new file mode 100644
index 00000000..8fd3e628
--- /dev/null
+++ b/packages/zcli-themes/src/lib/rewriteAssets.ts
@@ -0,0 +1,23 @@
+import { CLIError } from '@oclif/core/lib/errors'
+import * as fs from 'fs'
+import * as chalk from 'chalk'
+
+export default function rewriteAssets (themePath: string, assets: Record) {
+ const assetsDir = `${themePath}/assets`
+
+ try {
+ fs.mkdirSync(assetsDir, { recursive: true })
+ } catch (error) {
+ throw new CLIError(chalk.red(`Failed to create assets directory: ${assetsDir}`))
+ }
+
+ for (const [filename, base64Content] of Object.entries(assets)) {
+ const filePath = `${assetsDir}/${filename}`
+
+ try {
+ fs.writeFileSync(filePath, Buffer.from(base64Content, 'base64'))
+ } catch (error) {
+ throw new CLIError(chalk.red(`Failed to write asset file: ${filePath}`))
+ }
+ }
+}
diff --git a/packages/zcli-themes/src/lib/rewriteTemplates.test.ts b/packages/zcli-themes/src/lib/rewriteTemplates.test.ts
index 982f95e2..1899b7cd 100644
--- a/packages/zcli-themes/src/lib/rewriteTemplates.test.ts
+++ b/packages/zcli-themes/src/lib/rewriteTemplates.test.ts
@@ -34,22 +34,18 @@ describe('rewriteTemplates', () => {
])
})
- it('ignores write errors', () => {
+ it('throws an error when file cannot be written', () => {
const writeFileSyncStub = sinon.stub(fs, 'writeFileSync')
- writeFileSyncStub.onFirstCall().throws(new Error('Permission denied'))
- writeFileSyncStub.onSecondCall().returns(undefined)
+ writeFileSyncStub.throws(new Error('Permission denied'))
const templates = {
- home_page: 'Updated Home
',
- article_page: 'Updated Article
'
+ home_page: 'Updated Home
'
}
expect(() => {
rewriteTemplates('theme/path', templates)
- }).to.not.throw()
-
- expect(writeFileSyncStub.callCount).to.equal(2)
+ }).to.throw('Failed to write template file: theme/path/templates/home_page.hbs')
})
it('handles empty templates object', () => {
diff --git a/packages/zcli-themes/src/lib/rewriteTemplates.ts b/packages/zcli-themes/src/lib/rewriteTemplates.ts
index 93099a6a..8d88e4e0 100644
--- a/packages/zcli-themes/src/lib/rewriteTemplates.ts
+++ b/packages/zcli-themes/src/lib/rewriteTemplates.ts
@@ -1,4 +1,6 @@
+import { CLIError } from '@oclif/core/lib/errors'
import * as fs from 'fs'
+import * as chalk from 'chalk'
export default function rewriteTemplates (themePath: string, templates: Record) {
for (const [identifier, content] of Object.entries(templates)) {
@@ -8,7 +10,7 @@ export default function rewriteTemplates (themePath: string, templates: Record {
fetchStub = sinon.stub(global, 'fetch')
@@ -33,6 +34,10 @@ describe('themes:migrate', function () {
path.join(baseThemePath, 'templates/document_head.hbs'),
templateBackup
)
+ // Clean up migrated asset
+ if (fs.existsSync(migratedAssetPath)) {
+ fs.unlinkSync(migratedAssetPath)
+ }
})
describe('successful migration', () => {
@@ -55,6 +60,9 @@ describe('themes:migrate', function () {
},
templates: {
document_head: '{{!chat (obsolete)}}'
+ },
+ assets: {
+ 'category_tree.js': Buffer.from('console.log("category_tree");\n').toString('base64')
}
})
)
@@ -77,6 +85,10 @@ describe('themes:migrate', function () {
'utf8'
)
expect(template).to.contain('{{!chat (obsolete)}}')
+
+ // Verify asset was written
+ const asset = fs.readFileSync(migratedAssetPath, 'utf8')
+ expect(asset).to.equal('console.log("category_tree");\n')
})
})