Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for zcli theme preview #176

Merged
merged 16 commits into from
May 10, 2023
Merged
Show file tree
Hide file tree
Changes from 15 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
2 changes: 1 addition & 1 deletion .github/CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ includes:
- `└─── packages/` – the folder that contains individual packages which are published to the npm registry.<br>
&nbsp;&nbsp;&nbsp;&nbsp;`├── zcli/` – contains the main package and is published as `@zendesk/zcli`<br>
&nbsp;&nbsp;&nbsp;&nbsp;`├── zcli-apps/` - contains apps related commands as a npm plugin<br>
&nbsp;&nbsp;&nbsp;&nbsp;`├── zcli-themes/` - contains apps related commands as a npm plugin<br>
&nbsp;&nbsp;&nbsp;&nbsp;`├── zcli-themes/` - contains themes related commands as a npm plugin<br>

## Versioning Workflow

Expand Down
33 changes: 33 additions & 0 deletions docs/themes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
`zcli themes`
===========

zcli themes commands helps with managing Zendesk Help Center theming workflow.

* [`zcli themes:preview [THEMEDIRECTORY]`](#zcli-themespreview-themedirectory)

## Configuration

NOTE: preview requires login so make sure to first run `zcli login -i`

## `zcli themes:preview [THEMEDIRECTORY]`

starts local theme preview

```
USAGE
$ zcli themes:preview [THEMEDIRECTORY]

ARGUMENTS
THEMEDIRECTORY [default: .] theme path where manifest.json exists

OPTIONS
--bind=bind [default: localhost] Bind theme assets server to a specific host
--port=port [default: 4567] Port for the http server to use
--logs Tail logs
--no-livereload Disable live-reloading the preview when a change is made

EXAMPLES
$ zcli themes:preview ./copenhagen_theme
$ zcli themes:preview ./copenhagen_theme --port=9999
$ zcli themes:preview ./copenhagen_theme --no-livereload
```
2 changes: 1 addition & 1 deletion packages/zcli-apps/src/commands/apps/new.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export default class New extends Command {
fsExtra.copy(
path.join(process.cwd(), '/', 'app_scaffolds-master/packages/', flagScaffold),
path.join(process.cwd(), directoryName),
{ overwrite: true, errorOnExist: true }, async (err: Error) => {
{ overwrite: true, errorOnExist: true }, async (err) => {
await cleanDirectory(this.unzippedScaffoldPath)
if (err) {
const fsExtraError = err as FsExtraError
Expand Down
2 changes: 1 addition & 1 deletion packages/zcli-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"dist": "dist"
},
"devDependencies": {
"@types/fs-extra": "^9.0.1"
"@types/fs-extra": "9.0.13"
},
"optionalDependencies": {
"keytar": "^7.9.0"
Expand Down
5 changes: 5 additions & 0 deletions packages/zcli-themes/bin/run
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env node

const oclif = require('@oclif/core')

oclif.run().catch(require('@oclif/core/handle'))
3 changes: 3 additions & 0 deletions packages/zcli-themes/bin/run.cmd
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@echo off

node "%~dp0\run" %*
64 changes: 64 additions & 0 deletions packages/zcli-themes/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
{
"name": "@zendesk/zcli-themes",
"description": "zcli theme commands live here",
"version": "1.0.0-beta.32",
"author": "@zendesk/vikings",
"npmRegistry": "https://registry.npmjs.org",
"publishConfig": {
"access": "public"
},
"bin": {
"zcli-themes": "./bin/run"
},
"scripts": {
"build": "tsc",
"prepack": "tsc && ../../scripts/prepack.sh",
"postpack": "rm -f oclif.manifest.json npm-shrinkwrap.json && rm -rf ./dist && git checkout ./package.json",
"type:check": "tsc"
},
"dependencies": {
"@types/ws": "^8.5.4",
"axios": "^0.27.2",
"chalk": "^4.1.2",
"chokidar": "^3.5.3",
"cors": "^2.8.5",
"express": "^4.17.1",
"glob": "^9.3.2",
"sass": "^1.60.0",
"ws": "^8.13.0"
},
"devDependencies": {
"@oclif/test": "=2.1.0",
"@types/chai": "^4",
"@types/cors": "^2.8.6",
"@types/mocha": "^9.1.1",
"@types/sinon": "^10.0.13",
"chai": "^4",
"eslint": "^8.18.0",
"eslint-config-oclif": "^4.0.0",
"eslint-config-oclif-typescript": "^1.0.2",
"lerna": "^5.1.8",
"mocha": "^10.0.0",
"nock": "^13.2.8",
"sinon": "^14.0.0"
},
"files": [
"/bin",
"/dist",
"/oclif.manifest.json",
"/npm-shrinkwrap.json"
],
"keywords": [
"zcli",
"zendesk",
"cli",
"command"
],
"license": "MIT",
"main": "src/index.js",
"oclif": {
"commands": "./src/commands",
"bin": "zcli-themes"
},
"types": "lib/index.d.ts"
}
118 changes: 118 additions & 0 deletions packages/zcli-themes/src/commands/themes/preview.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { Command, Flags } from '@oclif/core'
import { CLIError } from '@oclif/core/lib/errors'
import * as path from 'path'
import * as fs from 'fs'
import * as express from 'express'
import * as http from 'http'
import * as WebSocket from 'ws'
import * as morgan from 'morgan'
import * as chalk from 'chalk'
import * as cors from 'cors'
import * as chokidar from 'chokidar'
import { Auth } from '@zendesk/zcli-core'
import preview from '../../lib/preview'
import getManifest from '../../lib/getManifest'
import getVariables from '../../lib/getVariables'
import getAssets from '../../lib/getAssets'
import zass from '../../lib/zass'

const logMiddleware = morgan((tokens, req, res) =>
`${chalk.green(tokens.method(req, res))} ${tokens.url(req, res)} ${chalk.bold(tokens.status(req, res))}`
)

export default class Preview extends Command {
static description = 'preview a theme in development mode'

static flags = {
bind: Flags.string({ default: 'localhost', description: 'Bind theme server to a specific host' }),
port: Flags.integer({ default: 4567, description: 'Port for the http server to use' }),
logs: Flags.boolean({ default: false, description: 'Tail logs' }),
livereload: Flags.boolean({ default: true, description: 'Enable or disable live-reloading the preview when a change is made', allowNo: true })
}

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

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

static strict = false

async run () {
const { flags, argv: [themeDirectory] } = await this.parse(Preview)
const themePath = path.resolve(themeDirectory)
const { logs: tailLogs, bind: host, port } = flags

if (!await preview(themePath, flags)) {
throw new CLIError('Unable to start preview')
}

const app = express()
const server = http.createServer(app)
const wss = new WebSocket.Server({ server, path: '/livereload' })

app.use(cors())
tailLogs && app.use(logMiddleware)

app.use('/guide/assets', express.static(`${themePath}/assets`))
app.use('/guide/settings', express.static(`${themePath}/settings`))

app.get('/guide/script.js', (req, res) => {
const script = path.resolve(`${themePath}/script.js`)
const source = fs.readFileSync(script, 'utf8')
res.header('Content-Type', 'text/javascript')
res.send(source)
})

app.get('/guide/style.css', (req, res) => {
const style = path.resolve(`${themePath}/style.css`)
const source = fs.readFileSync(style, 'utf8')
const manifest = getManifest(themePath)
const variables = getVariables(themePath, manifest.settings, flags)
const assets = getAssets(themePath, flags)
const compiled = zass(source, variables, assets)
res.header('Content-Type', 'text/css')
res.send(compiled)
})

server.listen(port, host, async () => {
// preview requires authentication so we're sure
// to have a logged in profile at this point
const { subdomain } = await new Auth().getLoggedInProfile()
console.log(chalk.bold.green('Ready', chalk.blueBright(`https://${subdomain}.zendesk.com/hc/admin/local_preview/start`, '🚀')))
console.log(`You can exit preview mode in the UI or by visiting https://${subdomain}.zendesk.com/hc/admin/local_preview/stop`)
tailLogs && this.log(chalk.bold('Tailing logs'))
})

const monitoredPaths = [
`${themePath}/assets`,
`${themePath}/settings`,
`${themePath}/templates`,
`${themePath}/manifest.json`,
`${themePath}/script.js`,
`${themePath}/style.css`
]

const watcher = chokidar.watch(monitoredPaths).on('change', async (path) => {
console.log(chalk.bold.gray('Change'), path)
if (await preview(themePath, flags)) {
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send('reload')
}
})
}
})

return {
close: () => {
// Stop watching file changes before terminating the server
watcher.close()
server.close()
wss.close()
}
}
}
}
1 change: 1 addition & 0 deletions packages/zcli-themes/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default {}
56 changes: 56 additions & 0 deletions packages/zcli-themes/src/lib/getAssets.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import * as sinon from 'sinon'
import * as fs from 'fs'
import { expect } from '@oclif/test'
import getAssets from './getAssets'

const flags = {
bind: 'localhost',
port: 1000,
logs: true,
livereload: true
}

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

it('returns an array of tuples containing the parsed path and url for each asset', () => {
const existsSyncStub = sinon.stub(fs, 'existsSync')
const readdirSyncStub = sinon.stub(fs, 'readdirSync')

existsSyncStub
.withArgs('theme/path/assets')
.returns(true)

readdirSyncStub.returns(['.gitkeep', 'foo.png', 'bar.png'] as any)

const assets = getAssets('theme/path', flags)

expect(assets).to.deep.equal([
[
{ base: 'foo.png', dir: '', ext: '.png', name: 'foo', root: '' },
'http://localhost:1000/guide/assets/foo.png'
],
[
{ base: 'bar.png', dir: '', ext: '.png', name: 'bar', root: '' },
'http://localhost:1000/guide/assets/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')

existsSyncStub
.withArgs('theme/path/assets')
.returns(true)

readdirSyncStub.returns(['unsuported file name.png'] as any)

expect(() => {
getAssets('theme/path', flags)
}).to.throw('The asset "unsuported file name.png" has illegal characters in its name. Filenames should only have alpha-numerical characters, ., _, -, and +')
})
})
26 changes: 26 additions & 0 deletions packages/zcli-themes/src/lib/getAssets.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { Flags } from '../types'
import { CLIError } from '@oclif/core/lib/errors'
import * as fs from 'fs'
import * as path from 'path'

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][] = []
const { bind: host, port } = flags

filenames.forEach(filename => {
const parsedPath = path.parse(filename)
const name = parsedPath.name.toLowerCase()
if (name.match(/[^a-z0-9-_+.]/)) {
throw new CLIError(
`The asset "${filename}" has illegal characters in its name. Filenames should only have alpha-numerical characters, ., _, -, and +`
)
}
if (!name.startsWith('.')) {
assets.push([parsedPath, `http://${host}:${port}/guide/assets/${filename}`])
}
})

return assets
}
Loading