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') }) })