blockwork
Version:
Looker block tool
250 lines (239 loc) • 8.77 kB
JavaScript
const bb = require('bluebird')
const download = bb.promisify(require('download-git-repo'))
const prompt = bb.promisifyAll(require('prompt'),{suffix:"p"})
const fs = require("fs")
const path = require("path")
const glob = require("glob")
const dot = require("dot")
const getCliArgs = require('minimist')
const {PromiseReadable} = require('promise-readable')
const jsonValidate = require("jsonschema").validate
const CSON = require("cursive")
const didYouMean = require("didyoumean")
var Promise = bb
const blockstoreIndex = CSON.parse(fs.readFileSync(path.join(__dirname,"directory.cson")),{encoding:'utf8'})
const blockstoreNames = Object.keys(blockstoreIndex)
const ignoreCheck = /(^|\n)(\.\/)?blockwork-tmp/
const ignoreSuggest= "blockwork-tmp"
const downloadPath = (name)=>"./blockwork-tmp/"+name.slug
const templateGlob = (name)=>"./blockwork-tmp/"+name.slug+"/*.dot"
const specPath = (name)=>"./blockwork-tmp/"+name.slug+"/blockwork.spec.json"
const configPath = (name)=>name.slug+".blockwork.config.cson"
const outputPath = "."
const globOptions = {}
didYouMean.threshold = null
dot.templateSettings = {
evaluate: /\<\<\!([\s\S]+?)\>\>/g,
interpolate: /\<\<\:([\s\S]+?)\>\>/g,
encode: /\<\<&([\s\S]+?)\>\>/g,
use: /\<\<#([\s\S]+?)\>\>/g,
define: /\<\<##\s*([\w\.$]+)\s*(\:|=)([\s\S]+?)#\>\>/g,
conditional: /\<\<\?(\?)?\s*([\s\S]*?)\s*\>\>/g,
iterate: /\<\<\*\s*(?:\>\>|([\s\S]+?)\s*\:\s*([\w$]+)\s*(?:\:\s*([\w$]+))?\s*\>\>)/g,
varname: 'x',
strip: false,
append: true,
selfcontained: false
}
main()
async function main(){
try{
var argOffset
if(process.argv[0].match(/blockwork/)){argOffset=1} //e.g. blockwork foo
if(process.argv[0].match(/node/)){argOffset=2} //e.g. node index.js foo
if(argOffset===undefined){
throw "Unexpected usage of script. Please report your use case to https://github.com/fabio-looker/blockwork/issues"
}
const cliArgs = getCliArgs(process.argv.slice(argOffset))
const command = cliArgs._[0]
const blockName = {
prop:toASCIICamelCase(cliArgs._[1]),
slug:camelToHyphen(toASCIICamelCase(cliArgs._[1]))
}
switch (command){
case "install":
await fetch(blockName)
await config(blockName)
await compile(blockName)
break;
case "fetch":
await fetch(blockName);
break;
case "config":
await config(blockName)
break;
case "compile":
await config(blockName,{silent:true})
await compile(blockName)
break;
default:
console.warn("Missing or unknown command. Try `blockwork install foo`")
break;
}
}catch(e){console.error(e)}
process.exit(0)
}
async function fetch(b){
var suggestion;
if(!fs.existsSync(".gitignore") || !fs.readFileSync(".gitignore").match(ignoreCheck)){
console.warn("\x1b[33mWarning:\x1b[0m You probably want '"+ignoreSuggest+"' in your .gitignore")
}
if(!b.slug){throw "Block name required."}
const block = blockstoreIndex[b.prop]
if(!block){
if(suggestion = didYouMean(b.prop, blockstoreNames)){
throw "No registered block with the name " + b.slug +
"\nMaybe you wanted "+camelToHyphen(suggestion)+"?"
}else{
throw "No registered block with the name " + b.slug
}
}
console.log("Fetching block...")
await download(block.repo || block, downloadPath(b))
return true
}
async function config(b,options={}){
var specString, spec;
try{
specString = fs.readFileSync(specPath(b),{encoding:'utf8'})
}catch(e){throw "This block does not have a blockwork spec. Please file an issue with the block maintainer."}
try{
spec = JSON.parse(specString)
}catch(e){throw "The block's spec is malformed. Please file an issue with the block maintainer."}
while(1){
let configString, config, configStatus, statusColor, cont, mode, validation;
try{
statusColor = '\x1b[33m' //Yellow
configStatus = 'missing'
configString = fs.readFileSync(configPath(b),{encoding:'utf8'})
statusColor = '\x1b[31m' //Red
configStatus = 'malformed'
config = CSON.parse(configString)
configStatus = 'invalid'
validation = jsonValidate(config, {schema:spec.schema})
if(!validation.valid){throw {message:validation.errors.join('/n')}}
statusColor = '\x1b[32m' //green
configStatus = 'ok'
}catch(e){console.error(e.message)}
console.info(statusColor + "Your block configuration is " + configStatus+"\x1b[0m")
try{
if(config){console.log(config)}
if(configStatus == 'ok'){
cont = options.silent || (await prompt.getp([{
description:"Continue with the above config? (Yes/no)",
type:"string",
pattern:/yes|no|y|n/i,
message:"Yes or no required."
}])).question.slice(0,1).toLowerCase()=='y'
if(cont){ break; }
}
prompt.start()
mode = (await prompt.getp({
description:"Specify config via (w)eb UI or (c)ommand line, or (a)bort?",
type:"string",
pattern:/a(bort)?|w(eb)?|c(li)?|c(ommand)?/i,
message:"Allowed values: a / w / c"
})).question.slice(0,1).toLowerCase()
}catch(e){throw "Quitting from prompt"}
if(mode=='a'){process.exit(0)}
if(mode=='c'){config = await prompt.getp(spec.schema)}
if(mode=='w'){config = await webUI(b,spec)}
fs.writeFileSync(configPath(b),JSON.stringify(config,undefined,2))
}
return true;
}
async function compile(b){
var config
try{
config = CSON.parse(fs.readFileSync(configPath(b),{encoding:'utf8'}))
}catch(e){
console.error("Unexpected error reading config file during compile step. This should have been caught in earlier validation step")
}
const tFiles=glob.sync(templateGlob(b),globOptions)
if(!tFiles.length){console.warn("Warning: No template files were found.")}
tFiles.forEach(function(t){
const tFile=fs.readFileSync(t,{encoding:'utf8'})
var template, output
try{
template = dot.template(tFile);
}catch(e){
console.error("Error compiling template file "+t)
console.error(e.message||e);
throw "Unable to compile block template files. Please file an issue with the block maintainer"
}
try{
console.log(config)
console.log(template)
output = template(config)
}catch(e){
console.error("Error applying template file "+t)
console.error(e.message||e)
throw "Please check your config or file an issue with the block maintainer"
}
writeFileSyncPlusPath(
path.join(outputPath,path.basename(t).replace(/\.dot$/i,''))
,output
)
})
return;
function writeFileSyncPlusPath(outpath, output){
outpath.split('/').slice(0,-1).reduce(function(accum,x,i){
accum.push(x); const path=accum.join('/')
//e.g. for a/b/c.txt => path: "a","a/b"
//e.g. for /a/b/c.txt => path: "", "/a", "/a/b"
if(path && !fs.existsSync(path)){fs.mkdirSync(path)}
return accum;
},[])
fs.writeFileSync(outpath,output);
}
}
var restify, server;
async function webUI(b,spec){
return new Promise(function(resolve,reject){
restify = restify || require('restify')
server = server || restify.createServer({
name: 'myapp',
version: '1.0.0'
})
server.get( "/", restify.plugins.serveStatic({
directory: path.join(__dirname,'./static'),
default:"index.html"
}))
server.get( "/schema.json",(req,res)=>res.send(spec))
/* I prefer CDNs
server.get("/modules/jsoneditor",restify.serveStatic("node_modules/json-editor/dist"))
server.get("/modules/jquery",restify.serveStatic("node_modules/jquery/dist"))
server.get("/modules/font-awesome",restify.serveStatic("node_modules/font-awesome/css"))
*/
//server.use(restify.plugins.acceptParser(server.acceptable))
server.use(restify.plugins.bodyParser())
server.post("/submit",(req,res)=>{
try{
var validation = jsonValidate(req.body, {schema:spec.schema})
if(!validation.valid){
res.send(400,validation.errors)
}else{
res.send(200,{message:"Config received. Please return to command line to confirm."})
console.log("Config received.")
resolve(req.body)
}
}catch(e){
console.error("Error while receiving config from web UI:",e.message)
res.send(500)
}
})
server.listen(30018, function() {
console.log('http://localhost:30018/')
})
})
}
function toASCIICamelCase(str) {
return str.trim()
.replace(/[^-_ a-z0-9]/ig,'')
.replace(/[-_ ]+/g,'-')
.replace(/-[a-z]/ig, match=>match.slice(1).toUpperCase())
}
function camelToHyphen(str){
return str.replace(/[A-Z]/,match=>"-"+match.toLowerCase())
}