Skip to content

Commit 9caa96c

Browse files
authored
Merge pull request #337 from zendesk/luis/themes_migrate_assets
feat: write migrated assets to theme during themes:migrate
2 parents 642d837 + 3ac1b31 commit 9caa96c

7 files changed

Lines changed: 141 additions & 9 deletions

File tree

packages/zcli-themes/src/lib/migrate.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import * as getVariables from './getVariables'
66
import * as getAssets from './getAssets'
77
import * as rewriteManifest from './rewriteManifest'
88
import * as rewriteTemplates from './rewriteTemplates'
9+
import * as rewriteAssets from './rewriteAssets'
910
import * as axios from 'axios'
1011
import { request } from '@zendesk/zcli-core'
1112
import * as errors from '@oclif/core/lib/errors'
@@ -45,6 +46,7 @@ describe('migrate', () => {
4546
const getAssetsStub = sinon.stub(getAssets, 'default')
4647
const rewriteManifestStub = sinon.stub(rewriteManifest, 'default')
4748
const rewriteTemplatesStub = sinon.stub(rewriteTemplates, 'default')
49+
const rewriteAssetsStub = sinon.stub(rewriteAssets, 'default')
4850
const requestStub = sinon.stub(request, 'requestAPI')
4951

5052
getManifestStub.withArgs('theme/path').returns(manifest)
@@ -88,6 +90,9 @@ describe('migrate', () => {
8890
home_page: '<h1>Updated Home</h1>',
8991
'article_pages/product_updates': '<h1>Updated Product updates</h1>',
9092
'custom_pages/faq': '<h1>Updated FAQ</h1>'
93+
},
94+
assets: {
95+
'category_tree.js': Buffer.from('console.log("tree")').toString('base64')
9196
}
9297
}
9398
}) as axios.AxiosPromise
@@ -131,6 +136,12 @@ describe('migrate', () => {
131136
'custom_pages/faq': '<h1>Updated FAQ</h1>'
132137
})
133138
).to.equal(true)
139+
140+
expect(
141+
rewriteAssetsStub.calledWith('theme/path', {
142+
'category_tree.js': Buffer.from('console.log("tree")').toString('base64')
143+
})
144+
).to.equal(true)
134145
})
135146

136147
it('throws an error when validation fails with template errors', async () => {

packages/zcli-themes/src/lib/migrate.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { CliUx } from '@oclif/core'
1010
import type { AxiosError } from 'axios'
1111
import rewriteTemplates from './rewriteTemplates'
1212
import rewriteManifest from './rewriteManifest'
13+
import rewriteAssets from './rewriteAssets'
1314
import handleTemplateError from './handleTemplateError'
1415
import parseAxiosError from './parseAxiosError'
1516

@@ -50,6 +51,7 @@ export default async function migrate (themePath: string, flags: Flags): Promise
5051
})
5152
rewriteManifest(themePath, data.metadata.api_version)
5253
rewriteTemplates(themePath, data.templates)
54+
rewriteAssets(themePath, data.assets)
5355
CliUx.ux.action.stop('Ok')
5456
} catch (e) {
5557
CliUx.ux.action.stop(chalk.bold.red('!'))
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import * as sinon from 'sinon'
2+
import * as fs from 'fs'
3+
import { expect } from '@oclif/test'
4+
import rewriteAssets from './rewriteAssets'
5+
6+
describe('rewriteAssets', () => {
7+
beforeEach(() => {
8+
sinon.restore()
9+
})
10+
11+
it('decodes base64 content and writes assets to the correct file paths', () => {
12+
const mkdirSyncStub = sinon.stub(fs, 'mkdirSync')
13+
const writeFileSyncStub = sinon.stub(fs, 'writeFileSync')
14+
15+
const jsContent = Buffer.from('console.log("hello")').toString('base64')
16+
17+
rewriteAssets('theme/path', {
18+
'script.js': jsContent
19+
})
20+
21+
expect(mkdirSyncStub.calledOnce).to.equal(true)
22+
expect(mkdirSyncStub.firstCall.args[1]).to.deep.equal({ recursive: true })
23+
24+
expect(writeFileSyncStub.callCount).to.equal(1)
25+
expect(writeFileSyncStub.firstCall.args[0]).to.equal('theme/path/assets/script.js')
26+
expect(Buffer.compare(
27+
writeFileSyncStub.firstCall.args[1] as Buffer,
28+
Buffer.from('console.log("hello")')
29+
)).to.equal(0)
30+
})
31+
32+
it('handles multiple file types', () => {
33+
sinon.stub(fs, 'mkdirSync')
34+
const writeFileSyncStub = sinon.stub(fs, 'writeFileSync')
35+
36+
const jsContent = Buffer.from('var a = 1;').toString('base64')
37+
const pngContent = Buffer.from([0x89, 0x50, 0x4e, 0x47]).toString('base64')
38+
39+
rewriteAssets('theme/path', {
40+
'app.js': jsContent,
41+
'logo.png': pngContent
42+
})
43+
44+
expect(writeFileSyncStub.callCount).to.equal(2)
45+
expect(writeFileSyncStub.firstCall.args[0]).to.equal('theme/path/assets/app.js')
46+
expect(writeFileSyncStub.secondCall.args[0]).to.equal('theme/path/assets/logo.png')
47+
48+
expect(Buffer.compare(
49+
writeFileSyncStub.secondCall.args[1] as Buffer,
50+
Buffer.from([0x89, 0x50, 0x4e, 0x47])
51+
)).to.equal(0)
52+
})
53+
54+
it('handles empty assets object', () => {
55+
sinon.stub(fs, 'mkdirSync')
56+
const writeFileSyncStub = sinon.stub(fs, 'writeFileSync')
57+
58+
rewriteAssets('theme/path', {})
59+
60+
expect(writeFileSyncStub.callCount).to.equal(0)
61+
})
62+
63+
it('throws an error when file cannot be written', () => {
64+
sinon.stub(fs, 'mkdirSync')
65+
const writeFileSyncStub = sinon.stub(fs, 'writeFileSync')
66+
67+
writeFileSyncStub.throws(new Error('Permission denied'))
68+
69+
const jsContent = Buffer.from('first').toString('base64')
70+
71+
expect(() => {
72+
rewriteAssets('theme/path', { 'a.js': jsContent })
73+
}).to.throw('Failed to write asset file: theme/path/assets/a.js')
74+
})
75+
76+
it('throws an error when assets directory cannot be created', () => {
77+
sinon.stub(fs, 'mkdirSync').throws(new Error('Permission denied'))
78+
sinon.stub(fs, 'writeFileSync')
79+
80+
const jsContent = Buffer.from('hello').toString('base64')
81+
82+
expect(() => {
83+
rewriteAssets('theme/path', { 'a.js': jsContent })
84+
}).to.throw('Failed to create assets directory: theme/path/assets')
85+
})
86+
})
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { CLIError } from '@oclif/core/lib/errors'
2+
import * as fs from 'fs'
3+
import * as chalk from 'chalk'
4+
5+
export default function rewriteAssets (themePath: string, assets: Record<string, string>) {
6+
const assetsDir = `${themePath}/assets`
7+
8+
try {
9+
fs.mkdirSync(assetsDir, { recursive: true })
10+
} catch (error) {
11+
throw new CLIError(chalk.red(`Failed to create assets directory: ${assetsDir}`))
12+
}
13+
14+
for (const [filename, base64Content] of Object.entries(assets)) {
15+
const filePath = `${assetsDir}/${filename}`
16+
17+
try {
18+
fs.writeFileSync(filePath, Buffer.from(base64Content, 'base64'))
19+
} catch (error) {
20+
throw new CLIError(chalk.red(`Failed to write asset file: ${filePath}`))
21+
}
22+
}
23+
}

packages/zcli-themes/src/lib/rewriteTemplates.test.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -34,22 +34,18 @@ describe('rewriteTemplates', () => {
3434
])
3535
})
3636

37-
it('ignores write errors', () => {
37+
it('throws an error when file cannot be written', () => {
3838
const writeFileSyncStub = sinon.stub(fs, 'writeFileSync')
3939

40-
writeFileSyncStub.onFirstCall().throws(new Error('Permission denied'))
41-
writeFileSyncStub.onSecondCall().returns(undefined)
40+
writeFileSyncStub.throws(new Error('Permission denied'))
4241

4342
const templates = {
44-
home_page: '<h1>Updated Home</h1>',
45-
article_page: '<h1>Updated Article</h1>'
43+
home_page: '<h1>Updated Home</h1>'
4644
}
4745

4846
expect(() => {
4947
rewriteTemplates('theme/path', templates)
50-
}).to.not.throw()
51-
52-
expect(writeFileSyncStub.callCount).to.equal(2)
48+
}).to.throw('Failed to write template file: theme/path/templates/home_page.hbs')
5349
})
5450

5551
it('handles empty templates object', () => {

packages/zcli-themes/src/lib/rewriteTemplates.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { CLIError } from '@oclif/core/lib/errors'
12
import * as fs from 'fs'
3+
import * as chalk from 'chalk'
24

35
export default function rewriteTemplates (themePath: string, templates: Record<string, string>) {
46
for (const [identifier, content] of Object.entries(templates)) {
@@ -8,7 +10,7 @@ export default function rewriteTemplates (themePath: string, templates: Record<s
810
try {
911
fs.writeFileSync(filePath, content)
1012
} catch (error) {
11-
// Ignore errors if file doesn't exist or can't be written
13+
throw new CLIError(chalk.red(`Failed to write template file: ${filePath}`))
1214
}
1315
}
1416
}

packages/zcli-themes/tests/functional/migrate.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ describe('themes:migrate', function () {
1111
let fetchStub: sinon.SinonStub
1212
let manifestBackup: string
1313
let templateBackup: string
14+
const migratedAssetPath = path.join(baseThemePath, 'assets/category_tree.js')
1415

1516
beforeEach(() => {
1617
fetchStub = sinon.stub(global, 'fetch')
@@ -33,6 +34,10 @@ describe('themes:migrate', function () {
3334
path.join(baseThemePath, 'templates/document_head.hbs'),
3435
templateBackup
3536
)
37+
// Clean up migrated asset
38+
if (fs.existsSync(migratedAssetPath)) {
39+
fs.unlinkSync(migratedAssetPath)
40+
}
3641
})
3742

3843
describe('successful migration', () => {
@@ -55,6 +60,9 @@ describe('themes:migrate', function () {
5560
},
5661
templates: {
5762
document_head: '{{!chat (obsolete)}}'
63+
},
64+
assets: {
65+
'category_tree.js': Buffer.from('console.log("category_tree");\n').toString('base64')
5866
}
5967
})
6068
)
@@ -77,6 +85,10 @@ describe('themes:migrate', function () {
7785
'utf8'
7886
)
7987
expect(template).to.contain('{{!chat (obsolete)}}')
88+
89+
// Verify asset was written
90+
const asset = fs.readFileSync(migratedAssetPath, 'utf8')
91+
expect(asset).to.equal('console.log("category_tree");\n')
8092
})
8193
})
8294

0 commit comments

Comments
 (0)