-
Notifications
You must be signed in to change notification settings - Fork 22
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: adding support for zcli:theme preview
- Loading branch information
1 parent
7f1f4c5
commit 918bff3
Showing
23 changed files
with
1,153 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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')) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
@echo off | ||
|
||
node "%~dp0\run" %* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
{ | ||
"name": "@zendesk/zcli-theme", | ||
"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-theme": "./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": { | ||
"axios": "^0.27.2", | ||
"chalk": "^4.1.2", | ||
"cors": "^2.8.5", | ||
"express": "^4.17.1", | ||
"glob": "^9.3.2", | ||
"sass": "^1.60.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", | ||
"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-theme" | ||
}, | ||
"types": "lib/index.d.ts" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import { Command, Flags } from '@oclif/core' | ||
import * as path from 'path' | ||
import * as fs from 'fs' | ||
import * as express from 'express' | ||
import * as morgan from 'morgan' | ||
import * as chalk from 'chalk' | ||
import * as cors from 'cors' | ||
import getRuntimeContext from '../../lib/getRuntimeContext' | ||
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 Server extends Command { | ||
static description = 'preview a theme in development mode' | ||
|
||
static flags = { | ||
help: Flags.help({ char: 'h' }), | ||
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' }), | ||
subdomain: Flags.string({ description: 'Account subdomain or full URL (including protocol)' }), | ||
username: Flags.string({ description: 'Account username (email)' }), | ||
password: Flags.string({ description: 'Account password' }) | ||
} | ||
|
||
static args = [ | ||
{ name: 'themeDirectory', required: true, default: '.' } | ||
] | ||
|
||
static examples = [ | ||
'$ zcli theme:preview ./copenhagen_theme' | ||
] | ||
|
||
static strict = false | ||
|
||
async run () { | ||
const { flags } = await this.parse(Server) | ||
const { argv: [themeDirectory] } = await this.parse(Server) | ||
const themePath = path.resolve(themeDirectory) | ||
const context = await getRuntimeContext(themePath, flags) | ||
const { logs: tailLogs, port, host, origin } = context | ||
|
||
await preview(themePath, context) | ||
|
||
const app = express() | ||
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, context) | ||
const assets = getAssets(themePath, context) | ||
const compiled = zass(source, variables, assets) | ||
res.header('Content-Type', 'text/css') | ||
res.send(compiled) | ||
}) | ||
|
||
const server = app.listen(port, host, () => { | ||
console.log(chalk.bold.green('Ready', chalk.blueBright(`${origin}/hc/admin/local_preview/start`, '🚀'))) | ||
console.log(`You can exit preview mode in the UI or by visiting ${origin}/hc/admin/local_preview/stop`) | ||
tailLogs && this.log(chalk.bold('Tailing logs')) | ||
}) | ||
|
||
const monitoredPaths = [ | ||
`${themePath}/manifest.json`, | ||
`${themePath}/templates` | ||
] | ||
|
||
// Keep references of watchers for unwatching later | ||
const watchers = monitoredPaths.map(path => | ||
fs.watch(path, { recursive: true }, (eventType, filename) => { | ||
console.log(chalk.bold.gray('Change'), filename) | ||
preview(themePath, context) | ||
})) | ||
|
||
return { | ||
close: () => { | ||
// Stop watching file changes before terminating the server | ||
watchers.forEach(watcher => watcher.close()) | ||
server.close() | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export default {} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
import * as sinon from 'sinon' | ||
import * as fs from 'fs' | ||
import { expect } from '@oclif/test' | ||
import getAssets from './getAssets' | ||
|
||
const context = { | ||
bind: 'localhost', | ||
port: 1000, | ||
logs: true, | ||
host: 'localhost', | ||
subdomain: 'z3n', | ||
username: '[email protected]', | ||
password: '123456', | ||
origin: 'https://z3n.zendesk.com' | ||
} | ||
|
||
describe('getAssets', () => { | ||
beforeEach(() => { | ||
sinon.restore() | ||
}) | ||
|
||
it('returns an array of tupples containing the parsed path and url for each asset', () => { | ||
const readdirSyncStub = sinon.stub(fs, 'readdirSync') | ||
|
||
readdirSyncStub.returns(['.gitkeep', 'foo.png', 'bar.png'] as any) | ||
|
||
const assets = getAssets('theme/path', context) | ||
|
||
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 and error when an asset has illegal characters in its name', () => { | ||
const readdirSyncStub = sinon.stub(fs, 'readdirSync') | ||
|
||
readdirSyncStub.returns(['unsuported file name.png'] as any) | ||
|
||
expect(() => { | ||
getAssets('theme/path', context) | ||
}).to.throw('The asset "unsuported file name.png" has illegal characters in its name. Filenames should only have alpha-numerical characters, ., _, -, and +') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
import type { RuntimeContext } 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, context: RuntimeContext): [path.ParsedPath, string][] { | ||
const filenames = fs.readdirSync(`${themePath}/assets`) | ||
const assets: [path.ParsedPath, string][] = [] | ||
|
||
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://${context.host}:${context.port}/guide/assets/${filename}`]) | ||
} | ||
}) | ||
|
||
return assets | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
import * as sinon from 'sinon' | ||
import * as fs from 'fs' | ||
import { expect } from '@oclif/test' | ||
import getManifest from './getManifest' | ||
|
||
describe('getManifest', () => { | ||
beforeEach(() => { | ||
sinon.restore() | ||
}) | ||
|
||
it('returns the manifest.json file parsed as json', () => { | ||
const existsSyncStub = sinon.stub(fs, 'existsSync') | ||
const readFileSyncStub = sinon.stub(fs, 'readFileSync') | ||
|
||
const manifest = { | ||
name: 'Copenhagen theme', | ||
author: 'Jane Doe', | ||
version: '1.0.1', | ||
api_version: 1, | ||
settings: [] | ||
} | ||
|
||
existsSyncStub | ||
.withArgs('theme/path/manifest.json') | ||
.returns(true) | ||
|
||
readFileSyncStub | ||
.withArgs('theme/path/manifest.json') | ||
.returns(JSON.stringify(manifest)) | ||
|
||
expect(getManifest('theme/path')).to.deep.equal(manifest) | ||
}) | ||
|
||
it('throws and error when it can\'t find a manifest.json file', () => { | ||
const existsSyncStub = sinon.stub(fs, 'existsSync') | ||
|
||
existsSyncStub | ||
.withArgs('theme/path/manifest.json') | ||
.returns(false) | ||
|
||
expect(() => { | ||
getManifest('theme/path') | ||
}).to.throw('Couldn\'t find a manifest.json file at path: "theme/path/manifest.json"') | ||
}) | ||
|
||
it('throws and error when the manifest.json file is malformed', () => { | ||
const existsSyncStub = sinon.stub(fs, 'existsSync') | ||
const readFileSyncStub = sinon.stub(fs, 'readFileSync') | ||
|
||
existsSyncStub | ||
.withArgs('theme/path/manifest.json') | ||
.returns(true) | ||
|
||
readFileSyncStub | ||
.withArgs('theme/path/manifest.json') | ||
.returns('{"name": "Copenhagen theme",,, }') | ||
|
||
expect(() => { | ||
getManifest('theme/path') | ||
}).to.throw('manifest.json file was malformed at path: "theme/path/manifest.json"') | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import type { Manifest } from '../types' | ||
import { CLIError } from '@oclif/core/lib/errors' | ||
import * as fs from 'fs' | ||
import * as chalk from 'chalk' | ||
|
||
export default function getManifest (themePath: string): Manifest { | ||
const manifestFilePath = `${themePath}/manifest.json` | ||
|
||
if (fs.existsSync(manifestFilePath)) { | ||
const manifestFile = fs.readFileSync(manifestFilePath, 'utf8') | ||
try { | ||
return JSON.parse(manifestFile) | ||
} catch (error) { | ||
throw new CLIError(chalk.red(`manifest.json file was malformed at path: "${manifestFilePath}"`)) | ||
} | ||
} else { | ||
throw new CLIError(chalk.red(`Couldn't find a manifest.json file at path: "${manifestFilePath}"`)) | ||
} | ||
} |
Oops, something went wrong.