@ffc-sync/sync
Version:
shopify theme deployment utility
231 lines (199 loc) • 5.39 kB
JavaScript
const fs = require('fs-extra')
const exit = require('exit')
const wait = require('w2t')
const fetch = require('node-fetch')
const readdir = require('recursive-readdir')
const {
logger,
abs,
sanitize
} = require('@ffc-sync/util')
const log = logger('ffc-sync')
module.exports = function init (config) {
if (!config.id) {
log.error(`theme id is missing from config`)
exit()
}
if (!config.password) {
log.error(`theme API password is missing from config`)
exit()
}
if (!config.store) {
log.error(`store url is missing from config`)
exit()
}
/**
* filled on each sync request, emptied when successful
*/
let queue = []
function createProgressCallback (cb) {
return total => remaining => cb && cb(total, remaining)
}
function api (method, body) {
return fetch(`https://${config.store}/admin/themes/${config.id}/assets.json`, {
method,
headers: {
'X-Shopify-Access-Token': config.password,
'Content-Type': 'application/json',
'Accept': 'application/json'
},
body: JSON.stringify(body)
})
}
function test () {
return api('GET', undefined)
.then(res => {
if (res.status === 404) {
throw {
errors: `No theme found with id '${config.id}'`
}
}
return res.json()
})
.then(({ errors, ...rest }) => {
if (errors) {
throw {errors}
}
return rest
})
}
function upload ({ key, file }) {
try {
const encoded = Buffer.from(fs.readFileSync(file), 'utf-8').toString('base64')
const value = Buffer.from(fs.readFileSync(file), 'utf-8').toString()
const output = key.includes('.liquid') ? { key, value } : { key, attachment: encoded }
return api('PUT', {
asset: output
})
.then(res => res ? res.json() : {})
.then(({ errors, asset }) => {
if (errors) {
throw {
key,
errors
}
}
return {
key,
asset
}
})
} catch (e) {
console.error(e)
}
}
function remove ({ key }) {
return api('DELETE', {
asset: { key }
})
.then(res => res ? res.json() : {})
.then(({ errors, asset }) => {
if (errors) {
throw {
key,
errors
}
}
return {
key,
asset
}
})
}
function enqueue (action, cb) {
return new Promise((res, rej) => {
;(function push (p) {
if (!p) res()
wait(500, [
action(p)
])
.then(() => {
cb && cb(queue.length)
if (queue.length) return push(queue.pop())
res()
})
.catch(e => {
cb && cb(queue.length)
if (queue.length) return push(queue.pop())
rej(e)
})
})(queue.pop())
})
}
function sync (paths = [], cb) {
paths = [].concat(paths)
paths = paths.length ? paths : ['.']
paths = paths.map(p => abs(p))
const deploy = fs.lstatSync(paths[0]).isDirectory()
const ignored = config.ignore
return new Promise((res, rej) => {
if (deploy) {
readdir(abs(paths[0]), ignored, (e, files) => {
if (e) {
log.error(e.message || e)
exit()
}
queue = files.map(file => ({
key: sanitize(file),
file
})).filter(f => f.key)
queue.reduce((_, f) => {
if (_.indexOf(f.key) > -1) {
const dirs = files
.map(f => f.replace(process.cwd(), ''))
.reduce((_, f) => {
const frag = f.split('/')[1]
if (_.indexOf(frag) > -1) return _
return _.concat(frag)
}, [])
.filter(f => fs.lstatSync(f).isDirectory())
log.error(
[
`plz only specify one theme directory\n`
].concat(dirs.map(dir => (
` ${log.colors.gray('>')} ${dir}/\n`
))).join('')
)
exit()
}
return _.concat(f.key)
}, [])
if (!queue.length) res()
res(enqueue(
upload,
createProgressCallback(cb)(queue.length)
))
})
} else {
queue = paths.map(file => ({
key: sanitize(file),
file
})).filter(f => f.key)
if (!queue.length) {
log.info('syncing', `nothing was synced`)
res()
return
}
res(enqueue(
upload,
createProgressCallback(cb)(queue.length)
))
}
})
}
function unsync (paths = [], cb) {
queue = [].concat(paths).map(p => ({
key: sanitize(p)
})).filter(f => f.key)
return Promise.resolve(enqueue(
remove,
createProgressCallback(cb)(queue.length)
))
}
return {
sync,
unsync,
config,
test
}
}