├── _config.yml ├── .eslintignore ├── index.js ├── plugin ├── deploy-general.js ├── frontmatter.js ├── spa-gen-page.js ├── cleaner.js ├── static-handler.js ├── taxonomy.js ├── data-handler.js ├── filename-handler.js ├── list-handler.js ├── api-writer.js ├── url-handler.js ├── store.js ├── nuxt-generator.js └── file-loader.js ├── .npmignore ├── config ├── .eslintrc └── release.sh ├── .gitignore ├── .eslintrc ├── src ├── plugin-list.js ├── watcher.js ├── item.js ├── server.js ├── config.js ├── Vuetalisk.js ├── helper.js └── commander.js ├── utils ├── stats.js ├── remove-empty-directory.js ├── config-loader.js ├── config-writer.js ├── command-loader.js └── server.js ├── debug.js ├── example └── vuetalisk.config.js ├── .vuetalisk └── vuetalisk.config.js ├── defaults ├── vuetalisk.config.new.js ├── _config.js └── vuetalisk.config.js ├── bin ├── vuetalisk-clean.js ├── vuetalisk ├── vuetalisk-dev.js └── vuetalisk-build.js ├── LICENSE ├── cmd-helper.js ├── README.md ├── package.json └── loader.js /_config.yml: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | config/*.js 2 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./src/Vuetalisk') 2 | -------------------------------------------------------------------------------- /plugin/deploy-general.js: -------------------------------------------------------------------------------- 1 | class DeployGeneral () { 2 | 3 | 4 | } 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .* 2 | *.log 3 | *.swp 4 | *.yml 5 | coverage 6 | docs/_book 7 | config 8 | example 9 | dist 10 | dist/*.map 11 | lib 12 | test 13 | -------------------------------------------------------------------------------- /config/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "process": true 4 | }, 5 | "extends": "vue", 6 | "rules": { 7 | "no-multiple-empty-lines": [2, {"max": 2}], 8 | "no-console": 0 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .config.js 3 | lib 4 | dist 5 | dist/*.gz 6 | dist/index.html 7 | dist/static 8 | docs/_book 9 | test/e2e/report 10 | test/e2e/screenshots 11 | node_modules 12 | .DS_Store 13 | *.log 14 | *.swp 15 | *~ 16 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "plugin:vue-libs/recommended" 5 | ], 6 | "rules": { 7 | "object-curly-spacing": ["error", "always"], 8 | "no-multiple-empty-lines": ["error", { "max": 2, "maxBOF": 1 }], 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/plugin-list.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'api-writer': true, 3 | 'cleaner': true, 4 | 'data-handler': true, 5 | 'file-loader': true, 6 | 'filename-handler': true, 7 | 'frontmatter': true, 8 | 'list-handler': true, 9 | 'static-handler': true, 10 | 'store': true, 11 | 'url-handler': true 12 | } 13 | -------------------------------------------------------------------------------- /utils/stats.js: -------------------------------------------------------------------------------- 1 | function stat (filepath) { 2 | try { 3 | return {stat: fs.statSync(filepath)} 4 | } catch (e) { 5 | return {error: true, stat: {mtime: Date.now()}} 6 | } 7 | } 8 | 9 | stat.all = function (h) { 10 | return { 11 | nuxtDist: checkStat(h.pathNuxtDist), 12 | page: checkStat(h.pathTarget('index.html')), 13 | api: checkStat(join(h.root, '.site.json')) 14 | } 15 | } 16 | 17 | module.exports = stat 18 | -------------------------------------------------------------------------------- /config/release.sh: -------------------------------------------------------------------------------- 1 | set -e 2 | echo "Enter release version: " 3 | read VERSION 4 | 5 | read -p "Releasing $VERSION - are you sure? (y/n)" -n 1 -r 6 | echo # (optional) move to a new line 7 | if [[ $REPLY =~ ^[Yy]$ ]] 8 | then 9 | echo "Releasing $VERSION ..." 10 | # npm test 11 | # VERSION=$VERSION npm run build 12 | 13 | # commit 14 | git add -A 15 | git commit -m "[build] $VERSION" 16 | npm version $VERSION --message "[release] $VERSION" 17 | 18 | # publish 19 | git push origin refs/tags/v$VERSION 20 | git push 21 | npm publish 22 | fi 23 | -------------------------------------------------------------------------------- /debug.js: -------------------------------------------------------------------------------- 1 | const debugFunc = require('debug') 2 | 3 | function setw (str, n = 20, fill = ' ') { 4 | return (str + fill.toString().repeat(n)).slice(0, n) 5 | 6 | } 7 | 8 | function debug (name) { 9 | const width =25 10 | const head = 'Vuetal' 11 | let prefix = setw(head + ':' + name + ':', width) 12 | const _debug = debugFunc(prefix) 13 | for (const type of ['error', 'warn', 'info', 'log', 'debug']) { 14 | const dname = setw([head, type, name, ''].join(':'), width) 15 | _debug[type] = debugFunc(dname) 16 | } 17 | _debug.ERROR = (err, ...args) => { 18 | if (args && args.length > 0) _debug.error (...args) 19 | _debug.error (err) 20 | process.exit(1) 21 | } 22 | return _debug 23 | } 24 | 25 | 26 | module.exports = debug 27 | -------------------------------------------------------------------------------- /plugin/frontmatter.js: -------------------------------------------------------------------------------- 1 | const {debug} = require('../debug')('FrontMatter') 2 | let frontmatter 3 | 4 | class FrontMatter { 5 | constructor () { 6 | this.name = 'FrontMatter' 7 | } 8 | 9 | async register (vuetal) { 10 | frontmatter = require('gray-matter') 11 | } 12 | 13 | async processItem ({item, h}) { 14 | if (item.type != 'page') return 15 | try { 16 | const matter = frontmatter(item.data) 17 | item.matter = matter.data 18 | item.data = matter.content 19 | item.excert = matter.excert 20 | h.item.manipulateMatter(item) 21 | } catch (e) { 22 | error(e) 23 | // FIXME: Do Nothing ? 24 | // TODO: TOML, JSON ? 25 | } 26 | return item 27 | } 28 | } 29 | 30 | debug('front-matter loaded') 31 | module.exports = FrontMatter 32 | FrontMatter.install = () => new FrontMatter 33 | -------------------------------------------------------------------------------- /utils/remove-empty-directory.js: -------------------------------------------------------------------------------- 1 | const {join} = require('path') 2 | 3 | async function removeEmptyDirectory(dir, exclues = []) { 4 | const excludesMap = new Map(excludes.map(v => [v, true])) 5 | await _removeEmtpyDirectoryIter(dir, excludesMap) 6 | } 7 | 8 | async function _removeEmtpyDirectoryIter(dir, exclude) { 9 | const files = fs.readdirSync(dir) 10 | let nfiles = 0 11 | for (const file of files) { 12 | nfiles++ 13 | if (excludes.get(file)) continue 14 | const fullpath = path.join(dir, file) 15 | const stat = fs.statSync(fullpath) 16 | if(!stat.isDirectory()) continue 17 | const empty = await this.removeEmptyDirectory(fullpath) 18 | .catch(ERROR) 19 | if (empty) { 20 | fs.rmdirSync(fullpath) 21 | nfiles-- 22 | } 23 | } 24 | if (nfiles > 0) return false 25 | return true 26 | } 27 | 28 | module.exports = removeEmptyDirectory 29 | -------------------------------------------------------------------------------- /src/watcher.js: -------------------------------------------------------------------------------- 1 | // Watch site source and run vuetal 2 | const _ = require('lodash') 3 | const chokidar = require('chokidar') 4 | const path = require('path') 5 | const {buildApi, buildPage, cmd} = require('../bin/vuetalisk-build.js') 6 | const {debug, log, ERROR} = require('../debug')('watcher') 7 | 8 | async function watcher(options) { 9 | const vuetalConf = cmd.vuetalConf().init() 10 | const root = options.root || vuetalConf.root 11 | const sitedir = path.join(root, vuetalConf.config.get('','source_dir')) 12 | 13 | const watcher = chokidar.watch(sitedir, {ignoreInitial: true}) 14 | .on('all', _.debounce((event, path) => { 15 | console.log(event, path) 16 | 17 | if (options.dev) debug('Build Api with dev mode') 18 | buildApi(options) 19 | .then(() => console.log('vuetal well done')) 20 | .catch(err => { console.error(err) }) 21 | }), 2500) 22 | } 23 | 24 | module.exports = watcher 25 | -------------------------------------------------------------------------------- /example/vuetalisk.config.js: -------------------------------------------------------------------------------- 1 | const Vuetalisk = require('vuetalisk') 2 | 3 | const _config = require('vuetalisk/config-loader').find() 4 | 5 | /** 6 | * Configuration function 7 | */ 8 | const config = () => 9 | (new Vuetalisk) 10 | .setRoot('.') 11 | .configure(_config) 12 | 13 | /** 14 | * Init function. `config()` should be invoked in the begining 15 | */ 16 | const init = () => 17 | config() 18 | .useStore('store', '.store.json') 19 | 20 | /** 21 | * Build API. scan collections, process items/lists, Write API, remove deleted files 22 | */ 23 | const buildApi = () => 24 | init() 25 | .use('file-loader') 26 | .use('frontmatter') 27 | .use('filename-handler') 28 | .use('url-handler') 29 | .use('data-handler') 30 | .use('list-handler') 31 | .use('api-writer') 32 | .use('static-handler') 33 | .use('cleaner') 34 | 35 | /** 36 | * Build Vue/Nuxt, Pages. 37 | */ 38 | const buildPage = () => 39 | init() 40 | .use('nuxt-generator') 41 | 42 | module.exports = {config, init, buildApi, buildPage, _config} 43 | -------------------------------------------------------------------------------- /.vuetalisk/vuetalisk.config.js: -------------------------------------------------------------------------------- 1 | const Vuetalisk = require('vuetalisk') 2 | 3 | const _config = require('vuetalisk/utils/config-loader').find() 4 | 5 | /** 6 | * Configuration function 7 | */ 8 | const config = () => 9 | (new Vuetalisk) 10 | .setRoot('.') 11 | .configure(_config) 12 | 13 | /** 14 | * Init function. `config()` should be invoked in the begining 15 | */ 16 | const init = () => 17 | config() 18 | .useStore('store', '.store.json') 19 | 20 | /** 21 | * Build API. scan collections, process items/lists, Write API, remove deleted files 22 | */ 23 | const buildApi = () => 24 | init() 25 | .use('file-loader') 26 | .use('frontmatter') 27 | .use('filename-handler') 28 | .use('url-handler') 29 | .use('data-handler') 30 | .use('list-handler') 31 | .use('api-writer') 32 | .use('static-handler') 33 | .use('cleaner') 34 | 35 | /** 36 | * Build Vue/Nuxt, Pages. 37 | */ 38 | const buildPage = () => 39 | init() 40 | .use('nuxt-generator') 41 | 42 | module.exports = {config, init, buildApi, buildPage, _config} 43 | -------------------------------------------------------------------------------- /defaults/vuetalisk.config.new.js: -------------------------------------------------------------------------------- 1 | const Vuetalisk = require('vuetalisk') 2 | 3 | const _config = require('vuetalisk/utils/config-loader').find() 4 | 5 | /** 6 | * Configuration function 7 | */ 8 | const config = () => 9 | (new Vuetalisk) 10 | .setRoot('.') 11 | .configure(_config) 12 | 13 | /** 14 | * Init function. `config()` should be invoked in the begining 15 | */ 16 | const init = () => 17 | config() 18 | .useStore('store', '.store.json') 19 | 20 | /** 21 | * Build API. scan collections, process items/lists, Write API, remove deleted files 22 | */ 23 | const buildApi = () => 24 | init() 25 | .use('file-loader') 26 | .use('frontmatter') 27 | .use('filename-handler') 28 | .use('url-handler') 29 | .use('data-handler') 30 | .use('list-handler') 31 | .use('api-writer') 32 | .use('static-handler') 33 | .use('cleaner') 34 | 35 | /** 36 | * Build Vue/Nuxt, Pages. 37 | */ 38 | const buildPage = () => 39 | init() 40 | .use('nuxt-generator') 41 | 42 | module.exports = {config, init, buildApi, buildPage, _config} 43 | -------------------------------------------------------------------------------- /bin/vuetalisk-clean.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const cmd = require('../cmd-helper')('clean') 4 | const fs = require('fs-extra') 5 | 6 | const program = cmd.base() 7 | program 8 | .description('Clean API, assets, pages, database, vue/nuxt build\n Simply remove a dist directory and a db file') 9 | .parse(process.argv) 10 | 11 | const {debug, log, ERROR} = cmd.run() 12 | let vuetalConf = cmd.vuetalConf() 13 | 14 | action() 15 | .catch(ERROR) 16 | 17 | async function action () { 18 | const tal = vuetalConf.init() 19 | 20 | log('Remove dist', tal.helper.pathTarget()) 21 | await fs.remove(tal.helper.pathTarget()).catch(ERROR) 22 | 23 | log('Remove database') 24 | await tal.store.delete().catch(ERROR) 25 | 26 | cmd.timeEnd() 27 | } 28 | 29 | // not used yet 30 | async function cleanApi () { 31 | const tal = vuetalConf.init() 32 | log('Remove api point') 33 | await fs.remove(tal.helper.pathApi()).catch(ERROR) 34 | log('Remove database') 35 | await tal.store.delete().catch(ERROR) 36 | } 37 | 38 | async function cleanPage () { 39 | // TODO 40 | } 41 | 42 | module.exports = {action} 43 | -------------------------------------------------------------------------------- /defaults/_config.js: -------------------------------------------------------------------------------- 1 | const configDefault = { 2 | this_is_default: true, 3 | source_dir: 'site', 4 | target_dir: 'dist', 5 | api_point: 'api', 6 | basename: '/', 7 | permalink: '/:year/:month/:day/:slug', 8 | extensions: ['.md', '.markdown', '.json', '.html'], 9 | excludes: ['.git', '.gitignore'], 10 | taxonomy: { 11 | category: ['category', 'categories'], 12 | tag: ['tag', 'tags'] 13 | }, 14 | build: { 15 | protocal: 'http', 16 | host: '127.0.0.1', 17 | port: 3001 18 | }, 19 | collections: { 20 | pages: { 21 | type: 'page', 22 | path: '.', 23 | permalink: '/:path', 24 | list: '/pages/list', 25 | sort: ['dir', 'order'], 26 | }, 27 | data: { 28 | type: 'data', 29 | path: '_data', 30 | permalink: '/:path', 31 | list: '/data/list', 32 | extensions: ['.js', '.json', '.yml', '.yaml', '.tml', '.toml'], 33 | }, 34 | static: { 35 | type: 'file', 36 | extensions: '*', 37 | path: '_static', 38 | permalink: '/:path' 39 | } 40 | } 41 | } 42 | 43 | module.exports = configDefault 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmd-helper.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | class Cmd { 4 | constructor (name, program) { 5 | this.name = name 6 | this.program = program || require('commander') 7 | this.start = Date.now() 8 | this.debug = {log: v => {}} // do nothing yet 9 | this.vuetalConf 10 | } 11 | 12 | base () { 13 | return this.program 14 | .option('-q, --quiet', 'Quiet, do not write anyting on startard ouput') 15 | 16 | } 17 | run () { 18 | const program = this.program 19 | if (program.quiet) { 20 | } else { 21 | if (!process.env.DEBUG) 22 | process.env.DEBUG = 'vuetal:*,-vuetal:debug:*' 23 | } 24 | this.debug = require('./debug')('cmd:' + this.name) 25 | return this.debug 26 | } 27 | 28 | vuetalConf (root, confPath) { 29 | if (!root) root = process.cwd() 30 | if (!confPath) return require('./utils/config-loader').loadVuetalConfig(root) 31 | return require(path.join(root, confPath)) 32 | } 33 | 34 | timeEnd () { 35 | this.debug.log('Done in %fs', (Date.now() - this.start)/1000) 36 | 37 | } 38 | } 39 | 40 | module.exports = function (name, program) { 41 | return new Cmd(name, program) 42 | } 43 | -------------------------------------------------------------------------------- /plugin/spa-gen-page.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs-extra') 3 | const {DEBUG, ERROR} = require('./error.js') 4 | const Item = require('./item.js') 5 | 6 | class SpaGenPage { 7 | constructor () { 8 | this.name = 'SpaGenPage' 9 | } 10 | 11 | async register (vuetalisk) { 12 | } 13 | 14 | async processPostInstall ({checkpoint, h}) { 15 | const pages = h.properList({ 16 | isPage: true 17 | }) 18 | 19 | let indexhtml 20 | // const indexpath = path.join(h.root, h.conf('', 'target_dir'), 'index.html') 21 | const target = h.pathTarget() 22 | const indexpath = path.join(target, 'index.html') 23 | 24 | try { 25 | indexhtml = fs.readFileSync(indexpath) 26 | } catch (err) { 27 | ERROR(err) 28 | } 29 | 30 | if (!indexhtml) ERROR(`index.html doesn't exists. Build vue first`) 31 | const plist = [] 32 | for (const item of pages) { 33 | const outpath = path.join(target, item.url, 'index.html') 34 | const promise = fs.outputFile(outpath, indexhtml).catch(ERROR) 35 | plist.push(promise) 36 | } 37 | 38 | await Promise.all(plist) 39 | } 40 | } 41 | 42 | module.exports = SpaGenPage 43 | -------------------------------------------------------------------------------- /bin/vuetalisk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const commands = { 4 | clean: 'Clean API, assets, pages, database, vue/nuxt build', 5 | build: `Build site. Default is '-ap' means 'build all`, 6 | dev: 'Vuetalisk + Nuxt/Vue dev mode' 7 | } 8 | 9 | // Execute commander if it's not module mode 10 | if (!module.parent) commander() 11 | 12 | /** 13 | * Main Commander 14 | */ 15 | function commander () { 16 | const {debug} = require('../debug')('cmd:main') 17 | debug('start') 18 | 19 | const {join} = require('path') 20 | const cwd = process.cwd() 21 | 22 | debug('Load vuetal') 23 | const vuetalConf = require('../utils/config-loader').loadVuetalConfig() 24 | 25 | debug('Load config') 26 | const helper = vuetalConf.config().helper 27 | const customCommands = helper.conf('', 'build.commands') || {} 28 | debug('Load commander') 29 | const commander = require('../src/commander') 30 | const package = require(`${__dirname}/../package.json`) 31 | 32 | commander 33 | .version(package.version) 34 | .usage(' [options]') 35 | .customCommands(commands, customCommands, 'vuetalisk', ['bin', 'build'], '.') 36 | .parse(process.argv) 37 | debug('done') 38 | } 39 | 40 | module.exports = {commands} 41 | -------------------------------------------------------------------------------- /defaults/vuetalisk.config.js: -------------------------------------------------------------------------------- 1 | const Vuetalisk = require('vuetalisk') 2 | const NuxtGenerator = require('nuxt-generator') 3 | 4 | const _config = require('vuetalisk/utils/config-loader').find() 5 | 6 | /** 7 | * Configuration function 8 | */ 9 | const config = vuetal => vuetal 10 | .setRoot('.') 11 | .configure(_config) 12 | 13 | /** 14 | * Init function. `config()` should be invoked in the begining 15 | */ 16 | const init = vuetal => vuetal 17 | .useStore('store', '.store.json') 18 | 19 | /** 20 | * Build API. scan collections, process items/lists, Write API, remove deleted files 21 | */ 22 | const buildApi = vuetal => vuetal 23 | .use('file-loader') 24 | .use('frontmatter') 25 | .use('filename-handler') 26 | .use('url-handler') 27 | .use('data-handler') 28 | .use('list-handler') 29 | .use('api-writer') 30 | .use('static-handler') 31 | .use('cleaner') 32 | 33 | /** 34 | * Build Vue/Nuxt, Pages. 35 | */ 36 | const buildPage = vuetal => vuetal 37 | .use(NuxtGenerator) 38 | 39 | const dev = vuetal => vuetal 40 | .use(NuxtGenerator) 41 | 42 | const clean = vuetal => vuetal 43 | .use(NuxtGenerator) 44 | 45 | const deploy = vuetal => vuetal 46 | .use(NuxtGenerator) 47 | 48 | module.exports = {config, init, buildApi, buildPage} 49 | -------------------------------------------------------------------------------- /plugin/cleaner.js: -------------------------------------------------------------------------------- 1 | const {debug, log, ERROR} = require('../debug')('cleaner') 2 | const removeEmptyDirectory = require('vuetalisk/utils/removeEmptyDirectory') 3 | 4 | const path = require('path') 5 | const fs = require('fs-extra') 6 | 7 | /** 8 | * Managge deleted file in DB, Dist 9 | * Remove data field from pages. 10 | * Set all 'updated' field to false 11 | */ 12 | 13 | class Cleaner { 14 | constructor () { 15 | this.name = 'Cleaner' 16 | } 17 | 18 | async register (vuetalisk) { 19 | } 20 | 21 | async processInstall ({checkpoint, h}) { 22 | debug('processInstall ', new Date()) 23 | for (const item of await h.updatedList()) { 24 | if (true || item.type === 'page') { 25 | item.data = undefined 26 | } 27 | // await h.set(item) 28 | } 29 | 30 | // DELETE only handle api files of page 31 | // handling of file collection is on their own 32 | for (const item of await h.deletedList()) { 33 | await fs.remove(h.pathItemApi(item)) 34 | .then(() => h.remove(item)) 35 | .catch(ERROR) 36 | } 37 | 38 | // Clean empty directory 39 | removeEmptyDirectory(h.pathTarget()) 40 | .catch(ERROR) 41 | 42 | debug('processInstall:Done', new Date()) 43 | } 44 | } 45 | 46 | module.exports = Cleaner 47 | Cleaner.install = () => new Cleaner 48 | -------------------------------------------------------------------------------- /plugin/static-handler.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs-extra') 3 | const Item = require('../src/item.js') 4 | const {ERROR, debug, log} = require('../debug')('static-handler') 5 | 6 | class StaticHandler { 7 | constructor () { 8 | this.name = 'StaticHandler' 9 | } 10 | 11 | async register (vuetalisk) { 12 | } 13 | 14 | async processInstall ({checkpoint, h}) { 15 | debug('processInstall ', new Date) 16 | const files = await h.find({ 17 | isStatic: true, 18 | installed: false 19 | }).catch(ERROR) 20 | const plist = [] 21 | for (const item of files) { 22 | const src = h.pathItemSrc(item) 23 | const target = h.pathItemTarget(item) 24 | if (item.updated && !item.deleted) { 25 | const promise = fs.copy(src, target) 26 | // .then(() => { item.updated = false; filesTable.update(item) }) 27 | .catch(ERROR) 28 | plist.push(promise) 29 | } 30 | if (item.deleted) { 31 | const promise = fs.remove(target) 32 | .then(() => h.remove(item)) 33 | .catch(ERROR) 34 | plist.push(promise) 35 | } 36 | } 37 | await Promise.all(plist).catch(ERROR) 38 | debug('processInstall:Done', new Date) 39 | } 40 | } 41 | 42 | module.exports = StaticHandler 43 | StaticHandler.install = () => new StaticHandler 44 | -------------------------------------------------------------------------------- /bin/vuetalisk-dev.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const cmd = require('../cmd-helper')('dev') 4 | const path = require('path') 5 | const {buildApi, buildPage} = require('../bin/vuetalisk-build.js') 6 | 7 | const program = cmd.base() 8 | program 9 | .description(`Vuetalisk dev mode, Nuxt dev mode is optional`) 10 | .option('-n, --nuxt', 'Run Nuxt dev with Veutalisk dev') 11 | .option('-c, --clean', 'Clean data base, remove dist before build') 12 | .parse(process.argv) 13 | 14 | const {debug, log, ERROR} = cmd.run() 15 | let vuetalConf = cmd.vuetalConf() 16 | 17 | const options = { 18 | dev: true, 19 | nuxt: program.nuxt, 20 | clean: program.clean 21 | } 22 | 23 | action(options).catch(ERROR) 24 | 25 | async function action (opts) { 26 | process.argv = process.argv.slice(0,2) 27 | if (opts.clean) { 28 | debug('clean dist') 29 | await require('./vuetalisk-clean').action() 30 | } 31 | 32 | debug('build api') 33 | await buildApi(opts) 34 | .then(() => console.log('vuetal build is well done')) 35 | .catch(err => { console.error(err) }) 36 | 37 | debug('web-server') 38 | const helper = vuetalConf.init().helper 39 | require('../utils/server')(helper.pathTarget()).listen() 40 | 41 | debug('watcher') 42 | require('../utils/watcher.js')({dev: opts.dev}) 43 | 44 | if (opts.nuxt) { 45 | debug('nuxt dev') 46 | require('nuxt/bin/nuxt-dev') 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /plugin/taxonomy.js: -------------------------------------------------------------------------------- 1 | class Taxonomy { 2 | constructor () { 3 | 4 | } 5 | 6 | async register (vuetalisk) { 7 | this.config = vuetalisk.config.get() 8 | this.pages = vuetalisk.store.getCollection('pages') 9 | this.taxoList = this.config.taxonomy 10 | this.taxoMap = {} 11 | for (const taxoname in taxoList) { 12 | const aliases = taxoList[taxoname] 13 | for (const alias of aliases) { 14 | taxoMap[alias] = taxoname 15 | } 16 | taxoMap[taxoname] = taxoname 17 | } 18 | this.taxos = vuetalisk.store.getCollection('taxonomy') 19 | if (this.taxos) { 20 | vuetalisk.store.addCollection('taxonomy', { 21 | 22 | }) 23 | } 24 | } 25 | 26 | async processInstall () { 27 | const pages = this.pages.find() 28 | if (!obj.data) return false 29 | if (!obj.data.data) return false 30 | for (const taxoname in taxoMap) { 31 | 32 | } 33 | return obj.data && 34 | obj.data.data && 35 | 36 | 37 | }) 38 | 39 | 40 | } 41 | 42 | async api () { 43 | const res = { 44 | apiBase: this.vuetaliskConfig.api.metaPoint + '/collection' 45 | api: [] 46 | } 47 | const taxonomy = await this.model.find('taxonomy') 48 | for (const collection in taxonomy) { 49 | res.api.push({ 50 | name: collection, 51 | data: taxonomy[collection] 52 | }) 53 | } 54 | return res 55 | } 56 | 57 | } 58 | 59 | 60 | -------------------------------------------------------------------------------- /bin/vuetalisk-build.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const cmd = require('../cmd-helper')('build') 4 | const path = require('path') 5 | let vuetalConf = cmd.vuetalConf() 6 | const {debug, log, ERROR} = cmd.run() 7 | 8 | if (!module.parent) { 9 | commander() 10 | } 11 | 12 | /** 13 | * Command function 14 | */ 15 | async function commander () { 16 | const program = cmd.base() 17 | program 18 | .description(`Build site. Default is build only api`) 19 | .option('-f, --force', 'Force to build Nuxt/Vue libray') 20 | .option('-p, --page', 'Build pages. Nuxt/Vue can be compiled in advance') 21 | .option('-a, --all', 'Build all') 22 | .option('-c, --clean', 'Clean data base, remove dist before build') 23 | .parse(process.argv) 24 | 25 | 26 | action(program).catch(ERROR) 27 | } 28 | 29 | /** 30 | * Action 31 | */ 32 | async function action (program) { 33 | const opts = {} 34 | if (program.force) opts.forceBuild = true 35 | 36 | program.api = true 37 | if (program.all) { 38 | program.page = true 39 | } else if (program.page) { 40 | program.api = false 41 | } 42 | 43 | if (program.clean) { 44 | await require('./vuetalisk-clean').action() 45 | } 46 | 47 | if (program.api) await buildApi(opts) 48 | if (program.page) await buildPage(opts) 49 | 50 | cmd.timeEnd() 51 | } 52 | 53 | /** 54 | * Build API 55 | */ 56 | async function buildApi (opts) { 57 | await vuetalConf.buildApi().run(opts).catch(ERROR) 58 | } 59 | 60 | /** 61 | * Build Page 62 | */ 63 | async function buildPage (opts) { 64 | await vuetalConf.buildPage().run(opts).catch(ERROR) 65 | } 66 | 67 | module.exports = {cmd, buildApi, buildPage} 68 | 69 | -------------------------------------------------------------------------------- /plugin/data-handler.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Write data api 3 | * ym, toml, js will be transfromed to json 4 | * require works in js 5 | */ 6 | const path = require('path') 7 | const fs = require('fs-extra') 8 | const {log, debug, ERROR} = require('../debug')('data-handler') 9 | 10 | class DataHandler { 11 | constructor () { 12 | this.name = 'DataHandler' 13 | } 14 | 15 | async register () { 16 | } 17 | 18 | async processItem ({item, h}) { 19 | if (item.src.match(/menu/)) debug(item) 20 | if (item.type !== 'data') return 21 | const src = h.pathItemSrc(item) // TODO CHECK 22 | const ext = path.extname(item.src) 23 | let data 24 | switch (ext) { 25 | case '.json': 26 | item.data = JSON.parse(fs.readFileSync(src).toString()) 27 | break 28 | case '.js': 29 | data = require(src) 30 | if (data.constructor === Function) 31 | data = data(h) 32 | item.data = data 33 | break 34 | case '.yml': 35 | case '.yaml': 36 | item.data = this.yamlLoader(fs.readFileSync(src)) 37 | debug(item.data) 38 | break 39 | case '.tml': 40 | case '.toml': 41 | item.data = this.tomlLoader(fs.readFileSync(src)) 42 | break 43 | default: 44 | ERROR('Unsupproted data format', h.pathItemSrc(item)) 45 | } 46 | item.ext = '.json' 47 | } 48 | 49 | yamlLoader (raw) { 50 | if (!this._yamlParser) this._yamlParser = require('js-yaml') 51 | return this._yamlParser.safeLoad(raw) 52 | } 53 | 54 | tomlLoader (raw) { 55 | if (!this._tomlParser) this._tomlParser = require('toml') 56 | return this._tomlParser.parse(raw) 57 | } 58 | } 59 | 60 | module.exports = DataHandler 61 | DataHandler.install = () => new DataHandler 62 | -------------------------------------------------------------------------------- /plugin/filename-handler.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | const dateTitleFormat = /^(\d+-\d+-\d+)-(.+)\.([^.]+)$/ 4 | const orderTitleFormat = /^(\d+)\.(.+)\.([^]+)$/ 5 | 6 | class FilenameHandler { 7 | constructor () { 8 | this.name = 'FilenameHandler' 9 | } 10 | async register (vuetalisk) { 11 | } 12 | 13 | async processItem ({item, h}) { 14 | if (item.type === 'list') return 15 | 16 | const collection = item.collection 17 | let name = path.basename(item.path) 18 | let cleanPath = item.path 19 | // remove '/index' 20 | if (name.match(/^index(\.[^.]+)?$/)) { 21 | let dir = path.dirname(item.path) 22 | let ext = path.extname(name) 23 | name = path.basename(dir + ext) 24 | cleanPath = dir + ext 25 | } 26 | item.cleanPath = cleanPath 27 | 28 | let match, date, title, ext, order 29 | match = name.match(dateTitleFormat) 30 | if (match) { 31 | date = match[1] 32 | title = match[2] 33 | ext = match[3] 34 | } 35 | if (!match) { 36 | match = name.match(orderTitleFormat) 37 | if (match) { 38 | order = match[1] 39 | title = match[2] 40 | ext = match[3] 41 | } 42 | } 43 | // Most general way 44 | if (!match) { 45 | ext = path.extname(name) 46 | title = name.slice(0, -ext.length) 47 | } 48 | if (date && !item.date) { 49 | item.date = h.date(date) 50 | } 51 | if (title) { 52 | if (!item.slug) item.slug = title 53 | if (!item.title) item.title = title 54 | if (!item.slug) item.slug = title 55 | } 56 | if (ext && !item.ext) item.ext = ext 57 | if (order && !item.order) item.order = order 58 | } 59 | } 60 | 61 | 62 | module.exports = FilenameHandler 63 | FilenameHandler.install = () => new FilenameHandler 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vuetalisk 2 | [![Gitter chat](https://badges.gitter.im/qgp9/Vuetalisk.png)](https://gitter.im/qgp9/Vuetalisk) 3 | [![npm version](https://badge.fury.io/js/vuetalisk.svg)](https://badge.fury.io/js/vuetalisk) 4 | 5 | Vuetalisk is a Static Site Generator based on `Static API` for Vue with Jekyll like structure and markdown posts. 6 | 7 | **IMPORTANT This is still very early version** 8 | 9 |

10 | 11 | * Live DEMO : https://vuetal-nuxt-demo.netlify.com/ 12 | * Documents : https://github.com/qgp9/Vuetalisk/wiki 13 | * Getting Started : https://github.com/qgp9/Vuetalisk/wiki/Guide-Getting-Started 14 | 15 | A basic idea is that Vuetalisk just writes a static JSON API from jekyll/hexo like markdown pages, 16 | then Vue can fetch and `vuetify` them. 17 | 18 | # Features 19 | * Customisable plugin based archtecture inspired by `MetalSmith`. 20 | * Static API first. Any application which support ajax can be used as a frontend besides Vue 21 | * Helper for Nuxt/SSR for SEO. 22 | * [WIP] Helper for Vue/SPA both of history/hash routing 23 | * Jekyll/Hexo like directory structure and markdown page/posts 24 | 25 | # Install 26 | 27 | We have [vuetalisk-nuxt starter template](https://github.com/qgp9/vuetalisk-nuxt)! Let's go with this. 28 | 29 | ``` 30 | vue init qgp9/vuetalisk-nuxt my-project 31 | cd my-project 32 | npm install # or yarn install 33 | ``` 34 | 35 | Since Vuetalisk provides build command, good to install it globally or just `npx` 36 | ``` 37 | npm install -g vuetalisk 38 | vuetalisk -h 39 | vuetalisk build --all 40 | 41 | # or by npx 42 | 43 | npx vuetalisk -h 44 | npx vuetalisk build --all 45 | ``` 46 | 47 | # Basic Idea 48 | ![basic idea](http://i.imgur.com/VxE4bG4.png) 49 | 50 | # Structure 51 | ![strecture](http://i.imgur.com/AwG5x1W.png) 52 | 53 | -------------------------------------------------------------------------------- /utils/config-loader.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs-extra') 3 | 4 | class ConfigLoader { 5 | constructor (options) { 6 | this.setRoot(options.root) 7 | this.findOrder = [ 8 | 'vuetalisk.config.js', 9 | '_config.js', 10 | '_config.json', 11 | '_config.yml', 12 | '_config.yarm', 13 | '_config.toml', 14 | '_config.tml' 15 | ] 16 | if (options.ignoreVuetalConf) this.findOrder.slice(1) 17 | } 18 | 19 | setRoot (root) { 20 | this.root = root || path.resolve() 21 | } 22 | 23 | findConfig () { 24 | const files = new Map(fs.readdirSync(this.root).map(v => [v, true])) 25 | for (const name of this.findOrder ){ 26 | if (files.get(name)) { 27 | this.configFile = name 28 | break; 29 | } 30 | } 31 | if (!this.configFile) { 32 | throw Error(`Vuetalisk config files doesn't exists. It should be one of \n` + this.findOrder.join(' ')) 33 | } 34 | return this.configFile 35 | } 36 | 37 | loadVuetalConfig () { 38 | let confPath = path.join(this.root, 'vuetalisk.config.js') 39 | if (fs.existsSync(confPath)){ 40 | return require(confPath) 41 | } 42 | confPath = path.join(this.root, '.vuetalisk', 'vuetalisk.config.js') 43 | console.log(confPath) 44 | if (!fs.existsSync(confPath)){ 45 | fs.copySync(path.join(__dirname, '..', 'defaults', 'vuetalisk.config.js'), confPath) 46 | } 47 | return require(confPath) 48 | } 49 | } 50 | 51 | function find(root) { 52 | const finder = new ConfigLoader({ignoreVuetalConf: true, root}) 53 | return finder.findConfig() 54 | } 55 | 56 | function loadVuetalConfig(root) { 57 | const finder = new ConfigLoader({root}) 58 | return finder.loadVuetalConfig() 59 | } 60 | 61 | module.exports = {find, loadVuetalConfig, ConfigLoader} 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuetalisk", 3 | "description": "Static Site Generator for Vue", 4 | "version": "0.0.6", 5 | "main": "./index.js", 6 | "scripts": { 7 | "build": "node config/build.js", 8 | "clean": "rm -rf dist/*.js* && rm ./*.log", 9 | "lint": "eslint src test config", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/qgp9/Vuetalisk.git" 15 | }, 16 | "keywords": [ 17 | "vue", 18 | "Static", 19 | "Site", 20 | "Generator" 21 | ], 22 | "author": "qgp9 ", 23 | "license": "MIT", 24 | "bugs": { 25 | "url": "https://github.com/qgp9/Vuetalisk/issues" 26 | }, 27 | "homepage": "https://github.com/qgp9/Vuetalisk#readme", 28 | "bin": { 29 | "vuetalisk": "bin/vuetalisk" 30 | }, 31 | "files": [ 32 | "*.js", 33 | "src", 34 | "bin", 35 | "plugin", 36 | "defaults" 37 | ], 38 | "dependencies": { 39 | "chokidar": "^1.7.0", 40 | "commander": "^2.11.0", 41 | "debug": "^2.6.8", 42 | "express": "^4.15.3", 43 | "fs-extra": "^4.0.0", 44 | "gray-matter": "^3.0.2", 45 | "js-yaml": "^3.9.0", 46 | "lodash": "^4.17.4", 47 | "lokijs": "^1.5.0", 48 | "night-train": "^0.0.2", 49 | "serve-static": "^1.12.3", 50 | "toml": "^2.3.2" 51 | }, 52 | "devDependencies": { 53 | "babel-core": "^6.22.1", 54 | "babel-eslint": "^7.1.0", 55 | "babel-loader": "^6.2.10", 56 | "babel-plugin-istanbul": "^3.1.2", 57 | "babel-polyfill": "6.22.0", 58 | "babel-preset-es2015": "^6.22.0", 59 | "babel-preset-power-assert": "^1.0.0", 60 | "buble": "^0.14.0", 61 | "eslint": "^3.14.1", 62 | "rollup": "^0.36.4", 63 | "rollup-plugin-buble": "^0.14.0", 64 | "rollup-plugin-replace": "^1.1.1", 65 | "uglify-js": "^2.7.5" 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /loader.js: -------------------------------------------------------------------------------- 1 | const modules = { 2 | 'debug': './debug', 3 | 'vuetalisk': './bin/vuetalisk', 4 | 'vuetalisk-build': './bin/vuetalisk-build.js', 5 | 'vuetalisk-clean': './bin/vuetalisk-clean.js', 6 | 'vuetalisk-dev': './bin/vuetalisk-dev.js', 7 | 'api-writer': './plugin/api-writer.js', 8 | 'cleaner': './plugin/cleaner.js', 9 | 'data-handler': './plugin/data-handler.js', 10 | 'file-loader': './plugin/file-loader.js', 11 | 'filename-handler': './plugin/filename-handler.js', 12 | 'frontmatter': './plugin/frontmatter.js', 13 | 'list-handler': './plugin/list-handler.js', 14 | 'nuxt-generator': './plugin/nuxt-generator.js', 15 | 'spa-gen-page': './plugin/spa-gen-page.js', 16 | 'static-handler': './plugin/static-handler.js', 17 | 'store': './plugin/store.js', 18 | 'taxonomy': './plugin/taxonomy.js', 19 | 'url-handler': './plugin/url-handler.js', 20 | 'Vuetalisk': './src/Vuetalisk.js', 21 | 'commander': './src/commander.js', 22 | 'config': './src/config.js', 23 | 'helper': './src/helper.js', 24 | 'item': './src/item.js', 25 | 'plugin-list': './src/plugin-list.js', 26 | 'server': './src/server.js', 27 | 'watcher': './src/watcher.js', 28 | 'command-loader': './utils/command-loader.js', 29 | 'config-loader': './utils/config-loader.js', 30 | 'config-writer': './utils/config-writer.js', 31 | 'remove-empty-directory': './utils/remove-empty-directory.js', 32 | 'server': './utils/server.js', 33 | 'stats': './utils/stats.js', 34 | } 35 | 36 | function loader (name) { 37 | return require(modules[name]) 38 | } 39 | 40 | module.exports = loader 41 | -------------------------------------------------------------------------------- /utils/config-writer.js: -------------------------------------------------------------------------------- 1 | const {debug, ERROR} = require('../debug')('config-writer') 2 | //const nameMatcher = require('.name-matcher') 3 | const {extname, join} = require('path') 4 | 5 | async function configWriter (config, path, omits) { 6 | const fs = require('fs-extra') 7 | const _config = omits ? omitDeep(config, omits) : config 8 | const ext = extname(path) 9 | switch (ext) { 10 | case '.json': 11 | await fs.outputJson(path, _config).catch(ERROR) 12 | break 13 | case '.js': 14 | const js = 'module.exports=' + JSON.stringify(_config, null, 2) 15 | await fs.outputFile(path, js).catch(ERROR) 16 | break 17 | default: 18 | ERROR('Not supported ext ', path) 19 | } 20 | return _config 21 | } 22 | 23 | 24 | function omitDeep (config, omits, depth = 100) { 25 | const _ = require('lodash') 26 | const _config = _.cloneDeep(config) 27 | if (omits) { 28 | const omitMap = new Map(omits.map(v => [v, true])) 29 | _omitDeepIter(_config, omitMap, 0, depth) 30 | } 31 | return _config 32 | } 33 | 34 | 35 | function _omitDeepIter(node, omits, depth, maxdepth) { 36 | if (depth > maxdepth) return 37 | if (Array.isArray(node)) return 38 | if (typeof node !== 'object') return 39 | Object.keys(node, key => { 40 | for (const omit of omits) { 41 | if (key === omit) { 42 | delete node[key] 43 | break 44 | } else { 45 | _omitDeepIter(node[key], omits, depth + 1, maxdepth) 46 | } 47 | } 48 | }) 49 | } 50 | 51 | configWriter.omitList = [ 52 | 'source_dir', 53 | 'target_dir', 54 | 'extensions', 55 | 'excludes', 56 | 'internal', 57 | 'path', 58 | 'secrets' 59 | ] 60 | 61 | configWriter.writeHidden = function (root, config, omits) { 62 | if (!omits) omits = configWriter.omitList 63 | const hidden = join(root, '.vuetalisk') 64 | configWriter(config, join(hidden, 'config.js')) 65 | const _config = configWriter(config, join(hidden, 'site.json'), omits) 66 | } 67 | 68 | module.exports = configWriter 69 | -------------------------------------------------------------------------------- /plugin/list-handler.js: -------------------------------------------------------------------------------- 1 | const {log, debug, ERROR} = require('../debug')('ListHandler') 2 | const _ = require('lodash') 3 | const path = require('path') 4 | 5 | class ListHandler { 6 | constructor () { 7 | this.name = 'ListHandler' 8 | } 9 | 10 | async register (vuetal) { 11 | } 12 | 13 | async processInstall ({checkpoint, h}) { 14 | log('generate lists for collections') 15 | for (const collection in h.collections) { 16 | const {type, list, pagenation, archive, archive_by} = 17 | h.confs(collection, ['type', 'list', 'pagenation', 'achive', 'archive_by']) 18 | if (list) { 19 | const itemList = await h.properList({collection}) 20 | // @note Save lists of collection with compact items 21 | // Actual writing will be handled by ApiWriter 22 | const compactList = [] 23 | for (const item of itemList){ 24 | compactList.push(h.item.genOutput(_.omit(item, [ 25 | 'data', 26 | 'matter', 27 | 'slug' 28 | ]))) 29 | } 30 | const api_point = '/' + h.conf(collection, 'api_point') 31 | const api = path.join(api_point, (type === 'page' ? 'page' : ''), list + '.json') 32 | debug(api) 33 | let item = { 34 | name: collection, 35 | url: list, 36 | api: api, 37 | src: list, 38 | type: 'list', 39 | pagenation, 40 | archive, 41 | archiveBy: archive_by, 42 | data: compactList, 43 | updatedAt: h.date(checkpoint), 44 | lastChecked: h.date(checkpoint), 45 | updated: true, 46 | deleted: false, 47 | isApi: true, 48 | isPage: type === 'page', 49 | isStatic: false, 50 | installed: false 51 | } 52 | await h.set(item).catch(ERROR) 53 | } 54 | } 55 | h.collectionListUpdated = true 56 | debug('done') 57 | } 58 | } 59 | module.exports = ListHandler 60 | ListHandler.install = () => new ListHandler 61 | debug('ListHandler loaded') 62 | -------------------------------------------------------------------------------- /plugin/api-writer.js: -------------------------------------------------------------------------------- 1 | const load = require('../loader') 2 | const {debug, log, ERROR} = load('debug')('api-writer') 3 | const path = require('path') 4 | 5 | 6 | class ApiWriter { 7 | constructor () { 8 | this.name = 'ApiWriter' 9 | } 10 | 11 | async register (vuetalisk) { 12 | } 13 | 14 | // TODO write page, list, delete deleted 15 | async processInstall ({checkpoint, h, options = {ApiWriter: {}}}) { 16 | debug('processInstall ', new Date) 17 | const fs = require('fs-extra') 18 | 19 | const opts = options.ApiWriter 20 | 21 | // items whatever has isApi = true 22 | const items = await h.updatedList({ 23 | isApi: true, 24 | installed: false, 25 | }).catch(ERROR) 26 | 27 | // const list = await h.find({type:'list'}) 28 | 29 | let plist = [] 30 | for (const item of items) { 31 | if (item.src.match(/menu/)) debug(item) 32 | if (!item.api) continue 33 | const promise = fs.outputJson( 34 | h.pathTarget(item.collection, item.api), 35 | h.item.genOutput(item) 36 | ).catch(ERROR) 37 | plist.push(promise) 38 | } 39 | await Promise.all(plist).catch(ERROR) 40 | 41 | await this.write_siteinfo(h, opts).catch(ERROR) 42 | debug('processInstall:Done', new Date) 43 | } 44 | 45 | async write_siteinfo (h, options) { 46 | 47 | const omitList = [ 48 | 'source_dir', 49 | 'target_dir', 50 | 'extensions', 51 | 'excludes', 52 | 'internal', 53 | 'path', 54 | 'secrets' 55 | ] 56 | 57 | if (options.dev) { 58 | debug('Bulid siteinfo.json with dev mode') 59 | } else { 60 | debug('Build siteinfo.json') 61 | omitList.push('build') 62 | } 63 | 64 | const configWriter = require('../utils/config-writer') 65 | const siteinfo = await configWriter.hidden(h.root, h.config.config, omitList) 66 | .catch(ERROR) 67 | if (!options.skipWriteHidden) { 68 | configWriter(siteinfo, h.pathApi('','site.json')) 69 | .catch(ERROR) 70 | } 71 | } 72 | } 73 | 74 | module.exports = ApiWriter 75 | ApiWriter.install = () => new ApiWriter 76 | -------------------------------------------------------------------------------- /plugin/url-handler.js: -------------------------------------------------------------------------------- 1 | const {debug, ERROR} = require('../debug')('url-handler') 2 | const {join} = require('path') 3 | 4 | class UrlHandler { 5 | constructor () { 6 | this.name = 'UrlHandler' 7 | } 8 | async register (vuetalisk) { 9 | } 10 | 11 | async processItem ({item, h}) { 12 | // Only for page type 13 | if (item.type === 'list') return 14 | let permalink = item.permalink 15 | const collname = item.collection 16 | if (!permalink) { 17 | const template = h.config.get(collname, 'permalink') 18 | const date = new Date(item.date) 19 | let year = '' 20 | let month = '' 21 | let day = '' 22 | if (date) { 23 | year += date.getFullYear() 24 | month += ('0' + (date.getMonth() + 1)).slice(-2) 25 | day += ('0' + date.getDate()).slice(-2) 26 | } 27 | const paths = item.path.split('/') 28 | let cleanPath = item.cleanPath 29 | if (item.type === 'page' || item.type === 'data') 30 | cleanPath = cleanPath.replace(/\..+?$/, '') 31 | permalink = template 32 | .replace(':collection', collname) 33 | .replace(':slug', item.slug || '') 34 | .replace(':title', item.slug || '') 35 | .replace(':path', cleanPath) // filename-handler 36 | .replace(':year', year || '') 37 | .replace(':month', month || '') 38 | .replace(':day', day || '') 39 | .replace(':dir1', paths[0] || '') 40 | .replace(':dir2', paths[1] || '') 41 | .replace(':dir3', paths[2] || '') 42 | item.permalink = permalink 43 | } 44 | // @note url doesn't include base name 45 | let url = '/' + permalink 46 | url = url.replace(/\/+/g, '/') 47 | item.url = url 48 | 49 | const api_point = '/' + h.conf(collname, 'api_point') 50 | // Use api url for data 51 | switch (item.type) { 52 | case 'page': 53 | if (url === '/') url += 'index' 54 | item.api = join(api_point, 'page', url + '.json') 55 | break 56 | case 'data': 57 | item.api = join(api_point, collname, url + '.json') 58 | item.url = join('/', collname, url) 59 | break 60 | default: 61 | item.url = url 62 | } 63 | } 64 | } 65 | 66 | module.exports = UrlHandler 67 | UrlHandler.install = () => new UrlHandler 68 | -------------------------------------------------------------------------------- /src/item.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash') 2 | 3 | const itemFormat = { 4 | matter: { 5 | // All user front matter 6 | }, 7 | // UNIQUE 8 | src: '', // collection name + sub path. UNIQUE 9 | url: '', // url except api/. UNIQUE 10 | // all date/time should be linux time in milliseconds. 11 | date: 0, 12 | title: '', 13 | collection: '', 14 | slug: '', 15 | order: 0, 16 | excert: '', 17 | fiigure: '', 18 | data: '', // content or list or object 19 | updatedAt: 0, // time. actual mtime 20 | type: null, // page, file, list, data 21 | // only for build 22 | deleted: false, 23 | updated: false, // update or not in build time 24 | lastChecked: 0, // time 25 | path: '', // local path from collection path. 26 | cleanPath: '', // 'index' is removed from path 27 | // for LIST 28 | name: '', // collection name or any name of list 29 | pagenation: 0, // number of item in page 30 | archive: '', // archive url 31 | archiveBy: '', // Year or Month or day. 32 | } 33 | 34 | const itemApiFormat = { 35 | matter: { 36 | // All user front matter 37 | }, 38 | // UNIQUE 39 | url: '', // url except api/. UNIQUE 40 | // all date/time should be linux time in milliseconds. 41 | // from matter 42 | date: 0, 43 | title: '', 44 | slug: '', 45 | excert: '', 46 | description: '', 47 | fiigure: '', 48 | order: 0, 49 | weight: 0, 50 | ext: '', 51 | // 52 | collection: '', 53 | data: '', // content or list or object 54 | updatedAt: 0, // time. actual mtime 55 | type: null, // page, file, list, data 56 | // for LIST 57 | name: '', 58 | size: 0, // list size 59 | // pagenation 60 | size_all: 0, // size of all list 61 | page: 0, // current page 62 | page_all: 0, // number of all page 63 | // archive 64 | } 65 | 66 | const outItemFields = Object.keys(itemApiFormat) 67 | 68 | class Item { 69 | static manipulateMatter (item) { 70 | const matter = item.matter 71 | if (!matter) return 72 | for (const field of ['title', 'date', 'slug', 'excert', 'figure', 'description', 'order', 'weight']) { 73 | if (matter[field]) item[field] = matter[field] 74 | } 75 | item.date = new Date(item.date).getTime() 76 | } 77 | 78 | static genOutput(item) { 79 | return _.pick(item, outItemFields) 80 | } 81 | } 82 | 83 | 84 | module.exports = Item 85 | -------------------------------------------------------------------------------- /utils/command-loader.js: -------------------------------------------------------------------------------- 1 | const {join, resolve, extname} = require('path') 2 | /** 3 | * Add series of command, custom command 4 | * the arg "-abc" is equivalent to "-a -b -c". 5 | * This also normalizes equal sign and splits "--abc=def" into "--abc def". 6 | * 7 | * @param {Object} commands 8 | * @param {Object} customCommands 9 | * @param {String} prefix 10 | * @param {Array} possibleDirs 11 | * @param {String} customRoot 12 | * @return {Command} the new command 13 | * @api public 14 | */ 15 | 16 | function commandLoader(commands, customCommands, prefix, possibleDirs, customRoot, pick) { 17 | if (!possibleDirs) possibleDirs = ['bin', 'dir'] 18 | if (!customRoot) customRoot = process.cwd() 19 | let comms = commands 20 | let custs = customCommands 21 | if (pick) { 22 | comms = comms[pick] ? {[pick]: comms[pick]} : {} 23 | custs = custs[pick] ? {[pick]: custs[pick]} : {} 24 | } 25 | 26 | // manipulate options of commands 27 | for (const name in comms) { 28 | let opts = comms[name] 29 | if (typeof opts === 'string') { 30 | comms[name] = opts = {desc: opts} 31 | } 32 | } 33 | 34 | // Load custom commands 35 | for (const name in custs) { 36 | let opts = custs[name] 37 | // disable command 38 | if (!opts) { 39 | delete comms[name] 40 | continue 41 | } 42 | // if opts is string, it's description 43 | if (typeof opts === 'string') { 44 | custs[name] = opts = {desc: opts} 45 | } 46 | // check path of custom command 47 | const bin = opts.path 48 | if (!bin) { 49 | const subname = prefix + '-' + name + '.js' 50 | for (const dir of possibleDirs) { 51 | var absolute = resolve(customRoot, dir, subname) 52 | if (fs.existsSync(absolute)){ 53 | bin = absolute 54 | break 55 | } 56 | } 57 | if (!bin) throw Error(`can't find custom command ${name}`) 58 | } else { 59 | bin = path.resolve(customRoot, ...bin.split('/')) 60 | if (!fs.existsSync(bin)) throw Error(`${path} doesn't exists`) 61 | } 62 | opts.path = bin 63 | if (comms[name]) opts.desc = '[overriden] ' + (opts.desc || comms[name].desc) 64 | else opts.desc = '[custom] ' + opts.desc 65 | comms[name] = custs[name] 66 | } 67 | 68 | return commands 69 | } 70 | 71 | commandLoader.find = function (command, h) { 72 | const name = command 73 | .replace(/^vuetalisk-/, '') 74 | .replace(/\.js$/, '') 75 | const commands = require('../bin/vuetalisk').commands 76 | const customs = h.conf('', 'build.commands') 77 | const res = commandLoader(commands, customs, null, h.root, name) 78 | return res[name] 79 | } 80 | -------------------------------------------------------------------------------- /plugin/store.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const {debug, log, ERROR} = require('../debug')('Store') 4 | const DB = require('lokijs') 5 | const fs = require('fs') 6 | 7 | class Store { 8 | constructor (dbfile) { 9 | this.dbfile = dbfile || 'store.json' 10 | this.db = new DB(this.dbfile) 11 | this.loaded = false 12 | } 13 | 14 | async itemTable () { 15 | let col = this.db.getCollection('table') 16 | if (col) return col 17 | else return this.db.addCollection('table', { 18 | unique: ['src', 'url'], 19 | indices: ['collection', 'type', 'updated', 'deleted'] 20 | }) 21 | } 22 | 23 | async cacheTable () { 24 | let col = this.db.getCollection('cache') 25 | if (col) return col 26 | else return this.db.addCollection('cache', {unique: ['name']}) 27 | } 28 | 29 | async findItem (query) { 30 | return await this._itemTable.find(query) 31 | } 32 | 33 | async getItem (src) { 34 | if (src.constructor == String) { 35 | return await this._itemTable.by('src', src) 36 | } else { 37 | if (src.src) { 38 | return await this._itemTable.by('src', src.src) 39 | } else if (src.url) { 40 | return await this._itemTable.by('url', src.url) 41 | } 42 | } 43 | ERROR('src should be a string or object with src or url', src) 44 | } 45 | 46 | async setItem (item) { 47 | if (item.$loki) return this._itemTable.update(item) 48 | let oitem = this._itemTable.by('src', item.src) 49 | if (oitem) { 50 | item.$loki = oitem.$loki 51 | item.meta = oitem.meta 52 | this._itemTable.update(item) 53 | // this._itemTable.remove(oitem) 54 | return item 55 | } 56 | return this._itemTable.insert(item) 57 | } 58 | 59 | 60 | async removeItem (item) { 61 | this._itemTable.remove(item) 62 | } 63 | 64 | async getCache (name) { 65 | return this._cacheTable.by('name', name) 66 | } 67 | 68 | async setCache (item) { 69 | let oitem = this._cacheTable.by('name', item.name) 70 | if (oitem) { 71 | if (oitem === item) { 72 | this._cacheTable.update(item) 73 | return item 74 | } else { 75 | this._cacheTable.remove(oitem) 76 | return this._cacheTable.insert(item) 77 | } 78 | } else { 79 | return this._cacheTable.insert(item) 80 | } 81 | } 82 | 83 | async removeCache (item) { 84 | return this._cacheTable.remove(item) 85 | } 86 | 87 | async load(options = {}) { 88 | if (this.loaded) return 89 | await new Promise((resolve, reject) => { 90 | this.db.loadDatabase(options, err => { 91 | if (err) reject(err) 92 | else resolve() 93 | }) 94 | }).catch(ERROR) 95 | this.loaded = true 96 | this._itemTable = await this.itemTable() 97 | this._cacheTable = await this.cacheTable() 98 | } 99 | 100 | async save () { 101 | await new Promise((resolve, reject) => { 102 | this.db.saveDatabase(err => { 103 | if (err) reject(err) 104 | else resolve() 105 | }) 106 | }) 107 | } 108 | 109 | async delete () { 110 | if (!fs.existsSync(this.dbfile)) return 111 | await new Promise((resolve, reject) => { 112 | this.db.deleteDatabase(err => { 113 | if (err) reject(err) 114 | else resolve() 115 | }) 116 | }) 117 | } 118 | } 119 | debug('Store loaded') 120 | 121 | module.exports = Store 122 | Store.install = dbFile => new Store(dbFile) 123 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | const {debug, log, ERROR} = require('../debug')('server') 2 | const path = require('path') 3 | const fs = require('fs') 4 | 5 | class Server { 6 | constructor (root, port, host) { 7 | this.root = root 8 | debug('root is',root) 9 | this.host = host || 10 | process.env.VUETAL_API_HOST|| 11 | process.env.HOST || 12 | process.env.npm_package_config_nuxt_host || 13 | '127.0.0.1' 14 | this.port = port || 15 | process.env.VUETAL_API_PORT || 16 | parseInt( 17 | process.env.PORT || 18 | process.env.npm_package_config_nuxt_port || 19 | 3000 20 | ) + 1 21 | this.server = undefined 22 | this.startTime = Date.now() 23 | 24 | process.env.VUETAL_API_PROTOCOL = 'http' 25 | process.env.VUETAL_API_HOST = this.host 26 | process.env.VUETAL_API_PORT = this.port 27 | 28 | } 29 | async listen() { 30 | // process.env.DEBUG = '*' 31 | debug('Load Exporess') 32 | const express = require ('express') 33 | const serveStatic = require('serve-static') 34 | const app = express() 35 | app.use(serveStatic(this.root, { 36 | setHeaders: (res, path) => { 37 | res.setHeader('Access-Control-Allow-Origin', '*') 38 | } 39 | })) 40 | this.server = require('http').createServer(app); 41 | this.server.listen(this.port, this.host) 42 | console.log('server start') 43 | this.startTime = Date.now() 44 | return this.server 45 | } 46 | 47 | async listenSimple() { 48 | // process.env.DEBUG = '*' 49 | debug('Load server') 50 | const http = require('http') 51 | //const target = h.pathTarget() 52 | const target = this.root 53 | this.server = http.createServer(function (req, res) { 54 | const url = decodeURIComponent(req.url) 55 | const file = path.join(target, url) 56 | debug('get',file) 57 | fs.readFile(file, function (error, content) { 58 | if (error) { 59 | if(error.code == 'ENOENT'){ 60 | res.writeHead(404); 61 | res.end('404', 'utf-8'); 62 | } 63 | else { 64 | res.writeHead(500); 65 | res.end(error.code); 66 | res.end(); 67 | } 68 | 69 | } else { 70 | debug('send') 71 | res.writeHead(200, { 72 | 'Content-Type': 'application/json', 73 | 'Access-Control-Allow-Origin': '*' 74 | }); 75 | res.end(content, 'utf-8'); 76 | } 77 | }) 78 | }) 79 | this.server.listen(this.port, this.host) 80 | debug('start simple static server') 81 | this.startTime = Date.now() 82 | return this.server 83 | } 84 | 85 | async close () { 86 | if (this.server) { 87 | await new Promise((resolve, reject) => { 88 | const timer = setInterval(() => { 89 | this.server.close(); 90 | if (!this.server || !this.server.address()) { 91 | clearInterval(timer) 92 | resolve() 93 | } 94 | }, 50) 95 | }) 96 | } 97 | } 98 | 99 | /** 100 | * Wait certain time from server start. If server access is too fast, server could be not ready yet. 101 | * 102 | * @param {number=200} time wait time in ms. default is 200ms 103 | */ 104 | async wait (time=200) { 105 | if (!this.startTime) return 106 | const remain = time - (Date.now() - this.startTime) 107 | if (remain > 0) { 108 | debug(`wait for ${remain} among ${time}`) 109 | await new Promise(resolve => setTimeout(() => resolve(), remain)) 110 | } 111 | } 112 | } 113 | 114 | module.exports = function (port, host) { 115 | return new Server(port, host) 116 | } 117 | -------------------------------------------------------------------------------- /utils/server.js: -------------------------------------------------------------------------------- 1 | const {debug, log, ERROR} = require('../debug')('server') 2 | const path = require('path') 3 | const fs = require('fs') 4 | 5 | class Server { 6 | constructor (root, port, host) { 7 | this.root = root 8 | debug('root is',root) 9 | this.host = host || 10 | process.env.VUETAL_API_HOST|| 11 | process.env.HOST || 12 | process.env.npm_package_config_nuxt_host || 13 | '127.0.0.1' 14 | this.port = port || 15 | process.env.VUETAL_API_PORT || 16 | parseInt( 17 | process.env.PORT || 18 | process.env.npm_package_config_nuxt_port || 19 | 3000 20 | ) + 1 21 | this.server = undefined 22 | this.startTime = Date.now() 23 | 24 | process.env.VUETAL_API_PROTOCOL = 'http' 25 | process.env.VUETAL_API_HOST = this.host 26 | process.env.VUETAL_API_PORT = this.port 27 | 28 | } 29 | async listen() { 30 | // process.env.DEBUG = '*' 31 | debug('Load Exporess') 32 | const express = require ('express') 33 | const serveStatic = require('serve-static') 34 | const app = express() 35 | app.use(serveStatic(this.root, { 36 | setHeaders: (res, path) => { 37 | res.setHeader('Access-Control-Allow-Origin', '*') 38 | } 39 | })) 40 | this.server = require('http').createServer(app); 41 | this.server.listen(this.port, this.host) 42 | console.log('server start') 43 | this.startTime = Date.now() 44 | return this.server 45 | } 46 | 47 | async listenSimple() { 48 | // process.env.DEBUG = '*' 49 | debug('Load server') 50 | const http = require('http') 51 | //const target = h.pathTarget() 52 | const target = this.root 53 | this.server = http.createServer(function (req, res) { 54 | const url = decodeURIComponent(req.url) 55 | const file = path.join(target, url) 56 | debug('get',file) 57 | fs.readFile(file, function (error, content) { 58 | if (error) { 59 | if(error.code == 'ENOENT'){ 60 | res.writeHead(404); 61 | res.end('404', 'utf-8'); 62 | } 63 | else { 64 | res.writeHead(500); 65 | res.end(error.code); 66 | res.end(); 67 | } 68 | 69 | } else { 70 | debug('send') 71 | res.writeHead(200, { 72 | 'Content-Type': 'application/json', 73 | 'Access-Control-Allow-Origin': '*' 74 | }); 75 | res.end(content, 'utf-8'); 76 | } 77 | }) 78 | }) 79 | this.server.listen(this.port, this.host) 80 | debug('start simple static server') 81 | this.startTime = Date.now() 82 | return this.server 83 | } 84 | 85 | async close () { 86 | if (this.server) { 87 | await new Promise((resolve, reject) => { 88 | const timer = setInterval(() => { 89 | this.server.close(); 90 | if (!this.server || !this.server.address()) { 91 | clearInterval(timer) 92 | resolve() 93 | } 94 | }, 50) 95 | }) 96 | } 97 | } 98 | 99 | /** 100 | * Wait certain time from server start. If server access is too fast, server could be not ready yet. 101 | * 102 | * @param {number=200} time wait time in ms. default is 200ms 103 | */ 104 | async wait (time=200) { 105 | if (!this.startTime) return 106 | const remain = time - (Date.now() - this.startTime) 107 | if (remain > 0) { 108 | debug(`wait for ${remain} among ${time}`) 109 | await new Promise(resolve => setTimeout(() => resolve(), remain)) 110 | } 111 | } 112 | } 113 | 114 | module.exports = function (port, host) { 115 | return new Server(port, host) 116 | } 117 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const {debug, log} = require('../debug')('Config') 2 | const _ = require('lodash') 3 | const assert = require('assert') 4 | const fs = require('fs') 5 | const path = require('path') 6 | 7 | class Config { 8 | constructor () { 9 | this.config = {} 10 | } 11 | 12 | addFile (_path) { 13 | debug('config added', _path) 14 | const ext = path.extname(_path) 15 | if (ext === '.js') { 16 | this._merge(require(_path)) 17 | return 18 | } 19 | let data 20 | try { 21 | data = fs.readFileSync(_path) 22 | } catch(e) { 23 | throw Error(e) 24 | } 25 | this.addString(data, ext) 26 | } 27 | 28 | addObj (obj) { 29 | this._merge(obj) 30 | } 31 | 32 | addString (data, ext, _path) { 33 | if (!data) return 34 | let config 35 | try { 36 | if(ext === '.yaml' || ext === '.yml') { 37 | const yaml = require('js-yaml') 38 | config = yaml.safeLoad(data) 39 | } else if (ext === '.toml' || ext === '.tml') { 40 | const toml = require('toml') 41 | config = toml.parse(data) 42 | } else if (ext === '.json') { 43 | config = JSON.parse(data) 44 | } 45 | } catch (e) { 46 | console.error(`Errors while parsing config`) 47 | if (_path) console.error(`in file ${_path}`) 48 | throw Error(e) 49 | } 50 | this._merge(config) 51 | } 52 | 53 | _merge (obj) { 54 | this.config = _.merge(this.config, obj) 55 | } 56 | 57 | get(collname, option) { 58 | assert(arguments.length === 2) 59 | let res 60 | if (collname && collname !== 'global') { 61 | res = _.get(this.config.collections[collname], option) 62 | } 63 | if (!res) res = _.get(this.config, option) 64 | return res 65 | } 66 | 67 | gets(collname, args) { 68 | assert(Array.isArray(args)) 69 | let res = {} 70 | for (const arg of args) { 71 | res[arg] = this.get(collname, arg) 72 | } 73 | return res 74 | } 75 | 76 | getGlobal (option) { 77 | return _.get(this.config, option) 78 | } 79 | 80 | _arrayToMap (list) { 81 | if (!list) return list 82 | if (_.isPlainObject(list)) return list 83 | if (!Array.isArray(list)) list = [list] 84 | let map = {} 85 | for (const item of list) { 86 | map[item] = true 87 | } 88 | return map 89 | } 90 | 91 | _normalize () { 92 | const collections = this.config.collections 93 | for (const name in collections){ 94 | const collection = collections[name] 95 | // delete collection if it's false 96 | if (!collection) delete collections[name] 97 | else { 98 | // type check 99 | const type = collection.type 100 | if (!type) { 101 | ERROR(`"type" is missed in collection ${name}`) 102 | } 103 | if (!collection.type.match(/^(page|file|data)$/)){ 104 | ERROR(`Type "${type}" is not allowed in collection "${name}"`) 105 | } 106 | // check path 107 | const path = collection.path 108 | if (!path) { 109 | ERROR(`"path" is missed in collection "${name}"`) 110 | } 111 | // set name for array iteration 112 | if (!collection.name) collection.name = name 113 | } 114 | } 115 | 116 | // Convert several arrays to object map for easy access 117 | for (const field of ['excludes', 'extensions', 'includes']){ 118 | if (this.config[field]) { 119 | this.config[field] = this._arrayToMap(this.config[field]) 120 | } 121 | for (const collname in collections) { 122 | const coll = collections[collname] 123 | if (coll[field]) { 124 | coll[field] = this._arrayToMap(coll[field]) 125 | } 126 | } 127 | } 128 | for (const name in collections){ 129 | const collection = collections[name] 130 | if (!collection.name) collection.name = name 131 | } 132 | } 133 | } 134 | 135 | debug('config loaded') 136 | 137 | module.exports = Config 138 | -------------------------------------------------------------------------------- /plugin/nuxt-generator.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const fs = require('fs-extra') 3 | 4 | const {debug, log, ERROR} = require('../debug')('NuxtGenerator') 5 | 6 | var resolve = path.resolve 7 | //process.env.DEBUG = 'nuxt:*,Vuetal:*' 8 | 9 | 10 | class NuxtGenerator { 11 | constructor () { 12 | this.name = 'NuxtGenerator' 13 | } 14 | 15 | async register (qgp) { 16 | } 17 | 18 | 19 | async processPostInstall ({checkpoint, h, options}) { 20 | if (!options) options = {} 21 | 22 | const server = require('../src/server')(h.pathTarget()) 23 | server.listen() 24 | 25 | // Check nuxt dist ./.nuxt/dist 26 | let distStats = undefined 27 | try { 28 | distStats = fs.statSync(path.join(h.root, '.nuxt', 'dist')) 29 | } catch (e) { 30 | log(`nuxt dist doesn't exists. let's build now`) 31 | } 32 | const distMtime = distStats ? distStats.mtimeMs : Date.now() 33 | const nuxtConfig = require(path.join(h.root,'nuxt.config.js')) 34 | const nuxtSrcDir = path.join(h.root, nuxtConfig.srcDir) 35 | 36 | let isChanged = true 37 | if (!options.forceBuild && distStats) { 38 | isChanged = await scanDir(nuxtSrcDir, distMtime) 39 | } 40 | 41 | let pages 42 | let nuxt 43 | if (isChanged) { 44 | if (options.forceBuild) { 45 | log('Force Build Mode. Build nuxt lib now.') 46 | } else { 47 | log('Nuxt source is changed. Build nuxt lib now. %s', isChanged) 48 | } 49 | debug('Load Nuxt') 50 | const {Builder, Nuxt, nuxtOpts} = loadNuxt(h.root) 51 | nuxt = new Nuxt(nuxtOpts) 52 | debug('Build Nuxt Lib') 53 | await new Builder(nuxt).build().catch(ERROR) 54 | 55 | } 56 | 57 | log('Copy nuxt dist to Vuetal dist') 58 | const target = h.pathTarget() 59 | const targetNuxt = path.join(target, '_nuxt') 60 | const sourceNuxt = path.join('.nuxt', 'dist') 61 | await fs.remove(targetNuxt).catch(ERROR) 62 | await fs.copy(sourceNuxt, targetNuxt).catch(ERROR) 63 | 64 | // find pages to render 65 | pages = await h.properList({isPage: true}) 66 | 67 | if (!pages || pages.length == 0) { 68 | log('Nothing to update') 69 | return 70 | } else { 71 | log('%d pages to update', pages.length) 72 | } 73 | 74 | await server.wait(200) // 200ms *FROM* server start. 75 | 76 | log(`Start Renering`) 77 | const start = Date.now() 78 | const plist = [] 79 | for (const item of pages) { 80 | const url = item.url 81 | const outpath = path.join(target, url, 'index.html') 82 | let doRender = true 83 | if(!isChanged) { 84 | const updatedAt = item.updatedAt 85 | doRender = false 86 | if (!doRender && !fs.existsSync(outpath)) doRender = true 87 | if (!doRender) { 88 | const stat = fs.statSync(outpath) 89 | const mtime = stat.mtimeMs 90 | if (mtime < distMtime || mtime < updatedAt) doRender = true 91 | } 92 | } 93 | 94 | // doRender = true 95 | if (doRender) { 96 | if (!nuxt) { 97 | log('Load Nuxt') 98 | const {Nuxt, nuxtOpts} = loadNuxt(h.root) 99 | nuxt = new Nuxt(nuxtOpts) 100 | } 101 | 102 | const res = await nuxt.renderRoute(url) 103 | .catch(ERROR) 104 | if (res.error) { 105 | ERROR(res.error) 106 | } 107 | await fs.outputFile(outpath, res.html).catch(ERROR) 108 | } 109 | } 110 | debug('nuxt time: ', Date.now() - start) 111 | if (server) server.close() 112 | if (nuxt) nuxt.close() 113 | } 114 | } 115 | 116 | async function scanDir (fullpath, time) { 117 | const stats = fs.statSync(fullpath) 118 | const mtime = stats.mtimeMs 119 | if (mtime > time) return fullpath 120 | if (stats.isDirectory()) { 121 | const files = fs.readdirSync(fullpath) 122 | for (const file of files) { 123 | const res = await scanDir(path.join(fullpath, file), time) 124 | if (res) return res 125 | } 126 | } 127 | return false 128 | } 129 | 130 | function loadNuxt (root) { 131 | var nuxtOpts = {} 132 | var nuxtConfigFile = path.resolve(root, 'nuxt.config.js') 133 | if (fs.existsSync(nuxtConfigFile)) { 134 | nuxtOpts = require(nuxtConfigFile) 135 | } 136 | if (typeof nuxtOpts.rootDir !== 'string') { 137 | nuxtOpts.rootDir = root 138 | } 139 | nuxtOpts.dev = false // Force production mode (no webpack middleware called) 140 | const {Builder, Nuxt} = require('nuxt') 141 | return {Builder, Nuxt, nuxtOpts} 142 | } 143 | 144 | module.exports = NuxtGenerator 145 | -------------------------------------------------------------------------------- /plugin/file-loader.js: -------------------------------------------------------------------------------- 1 | const npath = require('path') 2 | const fs = require('fs-extra') 3 | const Item = require('../src/item.js') 4 | const {debug, log, error, ERROR} = require('../debug')('file-loader') 5 | 6 | class FileLoader { 7 | constructor () { 8 | this.name = 'FileLoader' 9 | } 10 | /** 11 | * A register wagon for night-train 12 | * @private 13 | */ 14 | async register (vuetalisk) { 15 | } 16 | 17 | /** 18 | * A wagon for night-train 19 | * @private 20 | */ 21 | async processCollection ({store, checkpoint, h}) { 22 | debug('processCollection ', new Date) 23 | this.checkpoint = checkpoint 24 | 25 | const plist = [] 26 | for (const collname in h.collections) { 27 | const fullpath = h.pathSrc(collname) 28 | log('Start iteration of processCollection.', collname, fullpath) 29 | const promise = this._processCollectionIter(collname, fullpath, h) 30 | .catch(ERROR) 31 | plist.push(promise) 32 | } 33 | await Promise.all(plist).catch(ERROR) 34 | 35 | log('check deleted') 36 | for (const item of await h.find({lastChecked: {$lt: checkpoint}})) { 37 | debug('deleted', item.src) 38 | item.deleted = true 39 | } 40 | 41 | debug('processCollection:Done', new Date) 42 | } 43 | 44 | /* A wagon for night-train 45 | * @private 46 | */ 47 | async processItem ({item, h}) { 48 | if (item.type != 'page') return 49 | const data = fs.readFileSync(h.pathItemSrc(item), 'utf8') 50 | if (data) item.data = data 51 | } 52 | 53 | /* Iterator to scan directory 54 | * @private 55 | */ 56 | async _processCollectionIter (collname, path, h) { 57 | let files 58 | try { 59 | files = fs.readdirSync(path) 60 | } catch (e) { 61 | if (e.code === 'ENOENT' ) { 62 | log(`******* Directory doesn't exists.`, e.path) 63 | debug(`******* Directory doesn't exists.`, e.path) 64 | error(`******* Directory doesn't exists.`, e.path) 65 | return 66 | } else { 67 | ERROR(e) 68 | } 69 | } 70 | for (const filename of files) { 71 | // Skip special 72 | if (filename[0] === '_') continue 73 | // Skip excludes 74 | if ((h.conf(collname, 'excludes') || {})[filename]) continue 75 | // 76 | const fullpath = npath.join(path, filename) 77 | const stat = fs.statSync(fullpath) 78 | if (stat.isDirectory()) { 79 | // scan directory reculsively 80 | await this._processCollectionIter(collname, fullpath, h).catch(ERROR) 81 | } else { 82 | // Extension of file 83 | const ext = npath.extname(filename) 84 | const exts = h.conf(collname, 'extensions') 85 | if (!exts['*'] && !exts[ext]) continue 86 | // path 87 | // basePath is path from collection path 88 | // example, with default config 89 | // "_post/2017-07-25-hello-world.md" will become 90 | // basePath = "2017-07-25-hello-world.md" 91 | // src = "/posts/2017-07-25-hello-world.md" 92 | let basePath = npath.relative(h.pathSrc(collname), fullpath) 93 | let src = npath.join('/', collname, basePath) 94 | let type = h.conf(collname, 'type') 95 | // Check Database 96 | let item = await h.get(src).catch(ERROR) 97 | if (item) { 98 | item.lastChecked = h.checkpoint 99 | item.installed= false 100 | if (item.updatedAt < stat.mtimeMs) { 101 | item.updatedAt = h.date(stat.mtimeMs) 102 | item.updated = true 103 | item.deleted = false 104 | } else { 105 | item.updated = false 106 | item.deleted = false 107 | } 108 | // await h.set(item).catch(ERROR) 109 | } else { 110 | let item = { 111 | src: src, 112 | path: basePath, 113 | type: type, 114 | collection: collname, 115 | lastChecked: h.checkpoint, 116 | updatedAt: h.date(stat.mtimeMs), 117 | updated: true, 118 | deleted: false, 119 | installed: false 120 | } 121 | switch (type) { 122 | case 'page': 123 | item.url = '_temp_' + src 124 | item.isApi = true 125 | item.isPage = true 126 | item.isStatic = false 127 | break; 128 | case 'data': 129 | item.url = '_temp_' + src 130 | item.isApi = true 131 | item.isPage = false 132 | item.isStatic = false 133 | break; 134 | case 'file': 135 | item.url = '/' + basePath 136 | item.isApi = false 137 | item.isPage = false 138 | item.isStatic = true 139 | break; 140 | default: 141 | ERROR('Wrong type of item', item) 142 | } 143 | h.set(item) 144 | } 145 | } 146 | } 147 | } 148 | } 149 | 150 | module.exports = FileLoader 151 | FileLoader.install = () => new FileLoader() 152 | -------------------------------------------------------------------------------- /src/Vuetalisk.js: -------------------------------------------------------------------------------- 1 | const {debug, warn, log, error, ERROR} = require('../debug')('Main') 2 | const nodepath = require('path') 3 | const pluginList = require('./plugin-list') 4 | let Helper 5 | 6 | function srcPath(...args) { return nodepath.resolve(__dirname, ...args) } 7 | function pluginPath(...args) { return nodepath.resolve(__dirname, '..', 'plugin', ...args) } 8 | function pkgPath(...args) { return nodepath.resolve(__dirname, '..', ...args) } 9 | 10 | class Vuetalisk { 11 | constructor(config){ 12 | const Config = require('./config') 13 | this.root = '.' 14 | this.config = new Config 15 | this.config.addObj(require('../defaults/_config.js')) 16 | this.trains = new (require('night-train'))([ 17 | 'processCollection', 18 | 'processItem', 19 | 'processInstall', 20 | 'processPostInstall' 21 | ]) 22 | this.dbLoaded = false 23 | this.registered = false 24 | this.helper = undefined 25 | Helper = require('./helper.js') 26 | } 27 | 28 | /** 29 | * Add configuration file. 30 | * * chinable 31 | * * dupllecated ivoking cause merging of config files 32 | * @param {string} path path of configuration file. Possible extensions are yml, yaml, tml, toml, js, json 33 | */ 34 | configure (config) { 35 | if (typeof config === 'object') { 36 | this.config.addObj(config) 37 | } else { 38 | this.config.addFile(nodepath.join(this.root, config)) 39 | } 40 | this.config._normalize() 41 | // @important this.helper is only for command. don't use this in plugin 42 | this.helper = new Helper(this) 43 | return this 44 | } 45 | 46 | /** 47 | * Set configuration of source directory 48 | * * Chainable 49 | * * Final value depend on an order of source and configure 50 | * @param {string} path path of source directory from current directory 51 | */ 52 | source (path) { 53 | // FIXME 54 | this.config.set('source_dir', path) 55 | return this 56 | } 57 | 58 | /** 59 | * Set root directory where _config.yml located 60 | * * Chainable 61 | * @param {string} path 62 | */ 63 | setRoot (path) { 64 | this.root = nodepath.resolve(path) 65 | return this 66 | } 67 | 68 | /** 69 | * Set backed db 70 | * * Chainable 71 | * @param {object} store 72 | */ 73 | useStore (store, ...args) { 74 | if (typeof store === 'string') { 75 | let Store 76 | if (pluginList[store]) Store = require(pluginPath(store)) 77 | else Store = require('vuetalisk-plugin-' + store) 78 | this.store = Store.install(...args) 79 | } else { 80 | this.store = store 81 | } 82 | return this 83 | } 84 | 85 | 86 | /** 87 | * Register plugin 88 | * * Chainable 89 | * @param {object} plugin 90 | */ 91 | use (plugin, ...args) { 92 | if (typeof plugin === 'string') { 93 | debug(plugin) 94 | let Plugin 95 | if (pluginList[plugin]) Plugin = require(pluginPath(plugin)) 96 | else Plugin = require('vuetalisk-plugin-' + plugin) 97 | this.trains.register(Plugin.install(...args)) 98 | } else { 99 | this.trains.register(plugin) 100 | } 101 | return this 102 | } 103 | 104 | /** 105 | * Helper function to run each 'processItem' train 106 | * @private 107 | */ 108 | async _processItems (h) { 109 | // const type = 'page' 110 | const items = await h.updatedList().catch(ERROR) 111 | const plist = [] 112 | for (const item of items) { 113 | const promise = this.trains.run('processItem', {h, item}) 114 | .catch(ERROR) 115 | plist.push(promise) 116 | } 117 | await Promise.all(plist) 118 | this.store.save() 119 | } 120 | 121 | /** 122 | * @private 123 | * init function which will be invoked in the begining of any run 124 | */ 125 | async init({options}) { 126 | debug('Init') 127 | await this.store.load().catch(ERROR) 128 | this.table = await this.store.itemTable().catch(ERROR) 129 | this.cache = await this.store.cacheTable().catch(ERROR) 130 | debug('Store loaded') 131 | 132 | // Finalize config 133 | this.config._normalize() 134 | debug('Config nomalized') 135 | 136 | // register plugin 137 | if (!this.registered) { 138 | await this.trains.runAsync('register', this) 139 | .then(() => { this.registered = true }) 140 | .catch(ERROR) 141 | } 142 | debug('Registration done') 143 | } 144 | 145 | /** 146 | * Run processCollection, processItem, processInstall 147 | */ 148 | async run (options) { 149 | if (!options) options = {} 150 | log('Run') 151 | const vuetalisk = this 152 | const checkpoint = this.checkpoint = Date.now() 153 | const h = new Helper(this) 154 | 155 | debug('Init') 156 | await this.init({options}).catch(ERROR) 157 | 158 | debug('processCollection') 159 | await this.trains.run('processCollection', {h, vuetalisk, checkpoint, options}) 160 | .catch(ERROR) 161 | 162 | debug('processItem') 163 | await this._processItems(h) 164 | .catch(ERROR) 165 | 166 | debug('processInstall') 167 | await this.trains.run('processInstall', {h, vuetalisk, checkpoint, options}) 168 | .catch(ERROR) 169 | 170 | debug('processPostInstall') 171 | await this.trains.run('processPostInstall', {h, vuetalisk, checkpoint, options}) 172 | .catch(ERROR) 173 | 174 | debug('SaveDB') 175 | await this.store.save().catch(ERROR) 176 | 177 | log('Well Done') 178 | } 179 | 180 | static require (module) { 181 | if (module === 'debug') return require(pkgPath(module)) 182 | return require(pluginPath(module)) 183 | } 184 | } 185 | 186 | debug('Vuetalisk loaded') 187 | 188 | module.exports = Vuetalisk 189 | -------------------------------------------------------------------------------- /src/helper.js: -------------------------------------------------------------------------------- 1 | const {log, debug, ERROR} = require('../debug')('Helper') 2 | const path = require('path') 3 | const _ = require('lodash') 4 | const Item = require('./item.js') 5 | 6 | class Helper { 7 | constructor (vuetal) { 8 | this.vuetal = vuetal 9 | this.colllectionListUpdated = false 10 | // Shortcut 11 | this.config = vuetal.config 12 | this.store = vuetal.store 13 | this.root = vuetal.root 14 | this.collections = vuetal.config.getGlobal('collections') 15 | this.checkpoint = vuetal.checkpoint 16 | this.item = Item 17 | this.types = ['page', 'file', 'list', 'data'] // TODO: data 18 | this.table = vuetal.table 19 | this.cache = vuetal.cache 20 | } 21 | 22 | 23 | /** 24 | * @namespace Helper 25 | */ 26 | 27 | /* A shortcut of `vuetalisk.config.gets(collname, ...args) to get configured options 28 | * @param {string} collname name of collection 29 | * @param {array} fields wanted fields 30 | * @return {Object} object keyed by fields with value 31 | */ 32 | confs (collname, fields) { 33 | return this.config.gets(collname, fields) 34 | } 35 | 36 | conf (collname, field) { 37 | return this.config.get(collname, field) 38 | } 39 | 40 | /** 41 | * Generate Object keyed by each collection name 42 | * @param {Function=Object} generator function to generate vaules for each collection. Simply "Object" will generate empty object ("{}") 43 | * @return {Object} 44 | */ 45 | genCollectionMap (generator = Object) { 46 | const map = {} 47 | for (const name in this.collections) { 48 | map[name] = generator() 49 | } 50 | return map 51 | } 52 | 53 | /** 54 | * return linux time in milliseconds. Standard foram in Vuetalisk 55 | * @param {number|string|Date} _date 56 | * @return {number} 57 | */ 58 | date (_date) { 59 | switch (_date.constructor) { 60 | case Number: 61 | return _date 62 | case String: 63 | return new Date(_date).getTime() 64 | case Date: 65 | return _date.getTime() 66 | default: 67 | ERROR('Strange date format') 68 | } 69 | } 70 | 71 | /** 72 | * return ISO string of date for JSON output 73 | * @param {number|string|Date} _date 74 | * @return {string} ISO String of date 75 | */ 76 | dateOut (_date) { 77 | switch (_date.constructor) { 78 | case Number: 79 | case String: 80 | return (new Date(_date)).toISOString() 81 | case Date: 82 | return _date.toISOString() 83 | default: 84 | ERROR('Strange date format') 85 | } 86 | } 87 | 88 | /** 89 | * @namespace PATH 90 | */ 91 | 92 | /** 93 | * Absolute source path of collection 94 | * @param {string} collname name of collection 95 | * @param {...string} others others will be attached by path.join 96 | * @return {string} Absolute path 97 | * @memberof PATH 98 | */ 99 | pathSrc (collname, ...others) { 100 | return path.join( 101 | this.root, 102 | this.config.get(collname, 'source_dir'), 103 | this.config.get(collname, 'path') || '', 104 | ...others.map(v => v.toString()) 105 | ) 106 | } 107 | 108 | /** 109 | * Absolute source path of item 110 | * @param {string} item 111 | * @return {string} Absolute path 112 | * @memberof PATH 113 | */ 114 | pathItemSrc (item) { 115 | return this.pathSrc(item.collection, item.path) 116 | } 117 | 118 | /** 119 | * Absolute target path of collection 120 | * @param {string} collname name of collection 121 | * @param {...string} others others will be attached by path.join 122 | * @return {string} 123 | * @memberof PATH 124 | */ 125 | pathTarget (collname, ...others) { 126 | return path.join( 127 | this.root, 128 | this.config.get(collname, 'target_dir'), 129 | ...others.map(v => v.toString()) 130 | ) 131 | } 132 | 133 | /** 134 | * Absolute target path of item 135 | * @param {string} item 136 | * @return {string} 137 | * @memberof PATH 138 | */ 139 | pathItemTarget (item) { 140 | return this.pathTarget(item.collection, item.url) 141 | } 142 | 143 | 144 | /** 145 | * Absolute api path of collname 146 | * @param {string} collname name of collection 147 | * @param {...string} others These are attached to api path by path.join 148 | * @return {string} local api path 149 | * @memberof PATH 150 | */ 151 | pathApi (collname, ...others) { 152 | return this.pathTarget(collname, 153 | this.config.get(collname, 'api_point'), 154 | ...others 155 | ) 156 | } 157 | /** 158 | * Absolute api path of an item 159 | * If url is '/', return 'api/url/index.json' 160 | * @param {Object} item 161 | * @return {string} local api path 162 | * @memberof PATH 163 | */ 164 | pathItemApi (item) { 165 | if (!item.api) return '' 166 | return this.pathApi(item.collection, item.api) 167 | } 168 | 169 | /** 170 | * @namespace Store 171 | * @note If a method gets both of collection and type as arguments, type has prority. 172 | */ 173 | 174 | 175 | /* Find items from table by collname, type 176 | * @param {string} collname name of collection 177 | * @param {string} type name of type 178 | * @param {Object} query not recommended 179 | */ 180 | async find (query = {}) { 181 | return await this.store.findItem(query) 182 | } 183 | 184 | /** 185 | * Get proper list of item for any collection. 186 | * proper means not deleted and will be written as API 187 | * @return {Object} query 188 | * @return {Array} list of items 189 | */ 190 | async properList (query = {}) { 191 | query.deleted = false 192 | return await this.store.findItem(query) 193 | } 194 | 195 | /** 196 | * Get list of upated item for any collection/type. 197 | * proper means not deleted and will be written as API 198 | * @return {Object} query 199 | * @return {Array} list of items 200 | */ 201 | async updatedList (query = {}) { 202 | query.updated = true 203 | return await this.store.findItem(query) 204 | } 205 | 206 | /** 207 | * Get list of deleted item for any collection/type. 208 | * proper means not deleted and will be written as API 209 | * @return {Object} query 210 | * @return {Array} list of items 211 | */ 212 | async deletedList (query = {}) { 213 | query.deleted = true 214 | return await this.store.findItem(query) 215 | } 216 | 217 | 218 | 219 | /** 220 | * @param {string} url 221 | */ 222 | async get (src) { 223 | return await this.store.getItem(src) 224 | } 225 | 226 | async set (item) { 227 | return await this.store.setItem(item) 228 | } 229 | /** 230 | * remove item from store 231 | * @param {object} item 232 | */ 233 | async remove (item) { 234 | await this.store.removeItem(item) 235 | } 236 | 237 | 238 | /** 239 | * @namespace Cache 240 | */ 241 | 242 | async cache (name) { 243 | return await this.store.getCache(name) 244 | } 245 | 246 | async setCache (item) { 247 | return await this.store.setCache(item) 248 | } 249 | 250 | async removeCache (item) { 251 | await this.store.removeCache(item) 252 | } 253 | 254 | /** 255 | * @namespace Etc 256 | */ 257 | } 258 | 259 | module.exports = Helper 260 | 261 | debug('Helper loaded') 262 | -------------------------------------------------------------------------------- /src/commander.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | var EventEmitter = require('events').EventEmitter; 6 | var spawn = require('child_process').spawn; 7 | var path = require('path'); 8 | var dirname = path.dirname; 9 | var basename = path.basename; 10 | var fs = require('fs'); 11 | 12 | /** 13 | * Expose the root command. 14 | */ 15 | 16 | exports = module.exports = new Command(); 17 | 18 | /** 19 | * Expose `Command`. 20 | */ 21 | 22 | exports.Command = Command; 23 | 24 | /** 25 | * Expose `Option`. 26 | */ 27 | 28 | exports.Option = Option; 29 | 30 | /** 31 | * Initialize a new `Option` with the given `flags` and `description`. 32 | * 33 | * @param {String} flags 34 | * @param {String} description 35 | * @api public 36 | */ 37 | 38 | function Option(flags, description) { 39 | this.flags = flags; 40 | this.required = ~flags.indexOf('<'); 41 | this.optional = ~flags.indexOf('['); 42 | this.bool = !~flags.indexOf('-no-'); 43 | flags = flags.split(/[ ,|]+/); 44 | if (flags.length > 1 && !/^[[<]/.test(flags[1])) this.short = flags.shift(); 45 | this.long = flags.shift(); 46 | this.description = description || ''; 47 | } 48 | 49 | /** 50 | * Return option name. 51 | * 52 | * @return {String} 53 | * @api private 54 | */ 55 | 56 | Option.prototype.name = function() { 57 | return this.long 58 | .replace('--', '') 59 | .replace('no-', ''); 60 | }; 61 | 62 | /** 63 | * Check if `arg` matches the short or long flag. 64 | * 65 | * @param {String} arg 66 | * @return {Boolean} 67 | * @api private 68 | */ 69 | 70 | Option.prototype.is = function(arg) { 71 | return arg == this.short || arg == this.long; 72 | }; 73 | 74 | /** 75 | * Initialize a new `Command`. 76 | * 77 | * @param {String} name 78 | * @api public 79 | */ 80 | 81 | function Command(name) { 82 | this.commands = []; 83 | this.options = []; 84 | this._execs = {}; 85 | this._allowUnknownOption = false; 86 | this._args = []; 87 | this._name = name || ''; 88 | } 89 | 90 | /** 91 | * Inherit from `EventEmitter.prototype`. 92 | */ 93 | 94 | Command.prototype.__proto__ = EventEmitter.prototype; 95 | 96 | /** 97 | * Add command `name`. 98 | * 99 | * The `.action()` callback is invoked when the 100 | * command `name` is specified via __ARGV__, 101 | * and the remaining arguments are applied to the 102 | * function for access. 103 | * 104 | * When the `name` is "*" an un-matched command 105 | * will be passed as the first arg, followed by 106 | * the rest of __ARGV__ remaining. 107 | * 108 | * Examples: 109 | * 110 | * program 111 | * .version('0.0.1') 112 | * .option('-C, --chdir ', 'change the working directory') 113 | * .option('-c, --config ', 'set config path. defaults to ./deploy.conf') 114 | * .option('-T, --no-tests', 'ignore test hook') 115 | * 116 | * program 117 | * .command('setup') 118 | * .description('run remote setup commands') 119 | * .action(function() { 120 | * console.log('setup'); 121 | * }); 122 | * 123 | * program 124 | * .command('exec ') 125 | * .description('run the given remote command') 126 | * .action(function(cmd) { 127 | * console.log('exec "%s"', cmd); 128 | * }); 129 | * 130 | * program 131 | * .command('teardown [otherDirs...]') 132 | * .description('run teardown commands') 133 | * .action(function(dir, otherDirs) { 134 | * console.log('dir "%s"', dir); 135 | * if (otherDirs) { 136 | * otherDirs.forEach(function (oDir) { 137 | * console.log('dir "%s"', oDir); 138 | * }); 139 | * } 140 | * }); 141 | * 142 | * program 143 | * .command('*') 144 | * .description('deploy the given env') 145 | * .action(function(env) { 146 | * console.log('deploying "%s"', env); 147 | * }); 148 | * 149 | * program.parse(process.argv); 150 | * 151 | * @param {String} name 152 | * @param {String} [desc] for git-style sub-commands 153 | * @return {Command} the new command 154 | * @api public 155 | */ 156 | 157 | Command.prototype.command = function(name, desc, opts) { 158 | opts = opts || {}; 159 | var args = name.split(/ +/); 160 | var cmd = new Command(args.shift()); 161 | 162 | if (desc) { 163 | cmd.description(desc); 164 | this.executables = true; 165 | this._execs[cmd._name] = opts.path || true; 166 | if (opts.isDefault) this.defaultExecutable = cmd._name; 167 | } 168 | 169 | cmd._noHelp = !!opts.noHelp; 170 | this.commands.push(cmd); 171 | cmd.parseExpectedArgs(args); 172 | cmd.parent = this; 173 | 174 | if (desc) return this; 175 | return cmd; 176 | }; 177 | 178 | /** 179 | * Add series of command, custom command 180 | * the arg "-abc" is equivalent to "-a -b -c". 181 | * This also normalizes equal sign and splits "--abc=def" into "--abc def". 182 | * 183 | * @param {Object} commands 184 | * @param {Object} customCommands 185 | * @param {String} prefix 186 | * @param {Array} possibleDirs 187 | * @param {String} customRoot 188 | * @return {Command} the new command 189 | * @api public 190 | */ 191 | 192 | Command.prototype.customCommands = function(commands, customCommands, prefix, possibleDirs, customRoot) { 193 | if (!possibleDirs) possibleDirs = ['bin'] 194 | if (!customRoot) customRoot = process.cwd() 195 | 196 | // manipulate options of commands 197 | for (var name in commands) { 198 | var opts = commands[name] 199 | if (typeof opts === 'string') commands[name] = opts = {desc: opts} 200 | } 201 | 202 | // Load custom commands 203 | for (var name in customCommands) { 204 | var opts = customCommands[name] 205 | // disable command 206 | if (!opts) { 207 | delete commands[name] 208 | continue 209 | } 210 | // if opts is string, it's description 211 | if (typeof opts === 'string') { 212 | customCommands[name] = opts = {desc: opts} 213 | } 214 | // check path of custom command 215 | var bin = opts.path 216 | if (!bin) { 217 | var subname = prefix + '-' + name 218 | var possibles = catesianProduct([possibleDirs, [subname + '.js', subname]]) 219 | for (var possible of possibles) { 220 | var absolute = path.resolve(customRoot, ...possible) 221 | if (fs.existsSync(absolute)){ 222 | bin = absolute 223 | break 224 | } 225 | } 226 | if (!bin) throw Error(`can't find custom command ${name}`) 227 | } else { 228 | console.log(customRoot, ...bin.split('/')) 229 | bin = path.resolve(customRoot, ...bin.split('/')) 230 | if (!fs.existsSync(bin)) throw Error(`${path} doesn't exists`) 231 | } 232 | opts.path = bin 233 | if (commands[name]) opts.desc = '[overrided] ' + (opts.desc || commands[name].desc) 234 | else opts.desc = '[custom] ' + opts.desc 235 | commands[name] = customCommands[name] 236 | } 237 | 238 | // add commands to commander 239 | let newCommand 240 | for (const name in commands) { 241 | const opts = commands[name] 242 | const desc = opts.desc 243 | newCommand = this.command(name, desc, opts) 244 | } 245 | return newCommand 246 | } 247 | /** 248 | * Define argument syntax for the top-level command. 249 | * 250 | * @api public 251 | */ 252 | 253 | Command.prototype.arguments = function (desc) { 254 | return this.parseExpectedArgs(desc.split(/ +/)); 255 | }; 256 | 257 | /** 258 | * Add an implicit `help [cmd]` subcommand 259 | * which invokes `--help` for the given command. 260 | * 261 | * @api private 262 | */ 263 | 264 | Command.prototype.addImplicitHelpCommand = function() { 265 | this.command('help [cmd]', 'display help for [cmd]'); 266 | }; 267 | 268 | /** 269 | * Parse expected `args`. 270 | * 271 | * For example `["[type]"]` becomes `[{ required: false, name: 'type' }]`. 272 | * 273 | * @param {Array} args 274 | * @return {Command} for chaining 275 | * @api public 276 | */ 277 | 278 | Command.prototype.parseExpectedArgs = function(args) { 279 | if (!args.length) return; 280 | var self = this; 281 | args.forEach(function(arg) { 282 | var argDetails = { 283 | required: false, 284 | name: '', 285 | variadic: false 286 | }; 287 | 288 | switch (arg[0]) { 289 | case '<': 290 | argDetails.required = true; 291 | argDetails.name = arg.slice(1, -1); 292 | break; 293 | case '[': 294 | argDetails.name = arg.slice(1, -1); 295 | break; 296 | } 297 | 298 | if (argDetails.name.length > 3 && argDetails.name.slice(-3) === '...') { 299 | argDetails.variadic = true; 300 | argDetails.name = argDetails.name.slice(0, -3); 301 | } 302 | if (argDetails.name) { 303 | self._args.push(argDetails); 304 | } 305 | }); 306 | return this; 307 | }; 308 | 309 | /** 310 | * Register callback `fn` for the command. 311 | * 312 | * Examples: 313 | * 314 | * program 315 | * .command('help') 316 | * .description('display verbose help') 317 | * .action(function() { 318 | * // output help here 319 | * }); 320 | * 321 | * @param {Function} fn 322 | * @return {Command} for chaining 323 | * @api public 324 | */ 325 | 326 | Command.prototype.action = function(fn) { 327 | var self = this; 328 | var listener = function(args, unknown) { 329 | // Parse any so-far unknown options 330 | args = args || []; 331 | unknown = unknown || []; 332 | 333 | var parsed = self.parseOptions(unknown); 334 | 335 | // Output help if necessary 336 | outputHelpIfNecessary(self, parsed.unknown); 337 | 338 | // If there are still any unknown options, then we simply 339 | // die, unless someone asked for help, in which case we give it 340 | // to them, and then we die. 341 | if (parsed.unknown.length > 0) { 342 | self.unknownOption(parsed.unknown[0]); 343 | } 344 | 345 | // Leftover arguments need to be pushed back. Fixes issue #56 346 | if (parsed.args.length) args = parsed.args.concat(args); 347 | 348 | self._args.forEach(function(arg, i) { 349 | if (arg.required && null == args[i]) { 350 | self.missingArgument(arg.name); 351 | } else if (arg.variadic) { 352 | if (i !== self._args.length - 1) { 353 | self.variadicArgNotLast(arg.name); 354 | } 355 | 356 | args[i] = args.splice(i); 357 | } 358 | }); 359 | 360 | // Always append ourselves to the end of the arguments, 361 | // to make sure we match the number of arguments the user 362 | // expects 363 | if (self._args.length) { 364 | args[self._args.length] = self; 365 | } else { 366 | args.push(self); 367 | } 368 | 369 | fn.apply(self, args); 370 | }; 371 | var parent = this.parent || this; 372 | var name = parent === this ? '*' : this._name; 373 | parent.on('command:' + name, listener); 374 | if (this._alias) parent.on('command:' + this._alias, listener); 375 | return this; 376 | }; 377 | 378 | /** 379 | * Define option with `flags`, `description` and optional 380 | * coercion `fn`. 381 | * 382 | * The `flags` string should contain both the short and long flags, 383 | * separated by comma, a pipe or space. The following are all valid 384 | * all will output this way when `--help` is used. 385 | * 386 | * "-p, --pepper" 387 | * "-p|--pepper" 388 | * "-p --pepper" 389 | * 390 | * Examples: 391 | * 392 | * // simple boolean defaulting to false 393 | * program.option('-p, --pepper', 'add pepper'); 394 | * 395 | * --pepper 396 | * program.pepper 397 | * // => Boolean 398 | * 399 | * // simple boolean defaulting to true 400 | * program.option('-C, --no-cheese', 'remove cheese'); 401 | * 402 | * program.cheese 403 | * // => true 404 | * 405 | * --no-cheese 406 | * program.cheese 407 | * // => false 408 | * 409 | * // required argument 410 | * program.option('-C, --chdir ', 'change the working directory'); 411 | * 412 | * --chdir /tmp 413 | * program.chdir 414 | * // => "/tmp" 415 | * 416 | * // optional argument 417 | * program.option('-c, --cheese [type]', 'add cheese [marble]'); 418 | * 419 | * @param {String} flags 420 | * @param {String} description 421 | * @param {Function|*} [fn] or default 422 | * @param {*} [defaultValue] 423 | * @return {Command} for chaining 424 | * @api public 425 | */ 426 | 427 | Command.prototype.option = function(flags, description, fn, defaultValue) { 428 | var self = this 429 | , option = new Option(flags, description) 430 | , oname = option.name() 431 | , name = camelcase(oname); 432 | 433 | // default as 3rd arg 434 | if (typeof fn != 'function') { 435 | if (fn instanceof RegExp) { 436 | var regex = fn; 437 | fn = function(val, def) { 438 | var m = regex.exec(val); 439 | return m ? m[0] : def; 440 | } 441 | } 442 | else { 443 | defaultValue = fn; 444 | fn = null; 445 | } 446 | } 447 | 448 | // preassign default value only for --no-*, [optional], or 449 | if (false == option.bool || option.optional || option.required) { 450 | // when --no-* we make sure default is true 451 | if (false == option.bool) defaultValue = true; 452 | // preassign only if we have a default 453 | if (undefined !== defaultValue) self[name] = defaultValue; 454 | } 455 | 456 | // register the option 457 | this.options.push(option); 458 | 459 | // when it's passed assign the value 460 | // and conditionally invoke the callback 461 | this.on('option:' + oname, function(val) { 462 | // coercion 463 | if (null !== val && fn) val = fn(val, undefined === self[name] 464 | ? defaultValue 465 | : self[name]); 466 | 467 | // unassigned or bool 468 | if ('boolean' == typeof self[name] || 'undefined' == typeof self[name]) { 469 | // if no value, bool true, and we have a default, then use it! 470 | if (null == val) { 471 | self[name] = option.bool 472 | ? defaultValue || true 473 | : false; 474 | } else { 475 | self[name] = val; 476 | } 477 | } else if (null !== val) { 478 | // reassign 479 | self[name] = val; 480 | } 481 | }); 482 | 483 | return this; 484 | }; 485 | 486 | /** 487 | * Allow unknown options on the command line. 488 | * 489 | * @param {Boolean} arg if `true` or omitted, no error will be thrown 490 | * for unknown options. 491 | * @api public 492 | */ 493 | Command.prototype.allowUnknownOption = function(arg) { 494 | this._allowUnknownOption = arguments.length === 0 || arg; 495 | return this; 496 | }; 497 | 498 | /** 499 | * Parse `argv`, settings options and invoking commands when defined. 500 | * 501 | * @param {Array} argv 502 | * @return {Command} for chaining 503 | * @api public 504 | */ 505 | 506 | Command.prototype.parse = function(argv) { 507 | // implicit help 508 | if (this.executables) this.addImplicitHelpCommand(); 509 | 510 | // store raw args 511 | this.rawArgs = argv; 512 | 513 | // guess name 514 | this._name = this._name || basename(argv[1], '.js'); 515 | 516 | // github-style sub-commands with no sub-command 517 | if (this.executables && argv.length < 3 && !this.defaultExecutable) { 518 | // this user needs help 519 | argv.push('--help'); 520 | } 521 | 522 | // process argv 523 | var parsed = this.parseOptions(this.normalize(argv.slice(2))); 524 | var args = this.args = parsed.args; 525 | 526 | var result = this.parseArgs(this.args, parsed.unknown); 527 | 528 | // executable sub-commands 529 | var name = result.args[0]; 530 | 531 | var aliasCommand = null; 532 | // check alias of sub commands 533 | if (name) { 534 | aliasCommand = this.commands.filter(function(command) { 535 | return command.alias() === name; 536 | })[0]; 537 | } 538 | 539 | var opts = {} 540 | if (this._execs[name] && typeof this._execs[name] != "function") { 541 | var sub = name === 'help' ? args[1] : name 542 | if (typeof this._execs[sub] === 'string') opts.path = this._execs[sub] 543 | return this.executeSubCommand(argv, args, parsed.unknown, opts); 544 | } else if (aliasCommand) { 545 | // is alias of a subCommand 546 | args[0] = aliasCommand._name; 547 | return this.executeSubCommand(argv, args, parsed.unknown); 548 | } else if (this.defaultExecutable) { 549 | // use the default subcommand 550 | args.unshift(this.defaultExecutable); 551 | return this.executeSubCommand(argv, args, parsed.unknown); 552 | } 553 | 554 | return result; 555 | }; 556 | 557 | /** 558 | * Execute a sub-command executable. 559 | * 560 | * @param {Array} argv 561 | * @param {Array} args 562 | * @param {Array} unknown 563 | * @param {Object} opts 564 | * @api private 565 | */ 566 | 567 | Command.prototype.executeSubCommand = function(argv, args, unknown, opts = {}) { 568 | args = args.concat(unknown); 569 | 570 | if (!args.length) this.help(); 571 | if ('help' == args[0] && 1 == args.length) this.help(); 572 | 573 | // --help 574 | if ('help' == args[0]) { 575 | args[0] = args[1]; 576 | args[1] = '--help'; 577 | } 578 | 579 | var localBin, bin 580 | 581 | if (opts.path) { 582 | localBin = opts.path 583 | } else { 584 | // executable 585 | var f = argv[1]; 586 | // name of the subcommand, link `pm-install` 587 | bin = basename(f, '.js') + '-' + args[0]; 588 | 589 | 590 | // In case of globally installed, get the base dir where executable 591 | // subcommand file should be located at 592 | var baseDir 593 | , link = fs.lstatSync(f).isSymbolicLink() ? fs.readlinkSync(f) : f; 594 | 595 | // when symbolink is relative path 596 | if (link !== f && link.charAt(0) !== '/') { 597 | link = path.join(dirname(f), link) 598 | } 599 | baseDir = dirname(link); 600 | 601 | // prefer local `./` to bin in the $PATH 602 | localBin = path.join(baseDir, bin); 603 | } 604 | 605 | // whether bin file is a js script with explicit `.js` extension 606 | var isExplicitJS = false; 607 | if (opts.path && localBin.endsWith('.js') && exists(localBin)) { 608 | bin = localBin 609 | isExplicitJS = true 610 | } else if (exists(localBin + '.js')) { 611 | bin = localBin + '.js'; 612 | isExplicitJS = true; 613 | } else if (exists(localBin)) { 614 | bin = localBin; 615 | } 616 | 617 | args = args.slice(1); 618 | 619 | var proc; 620 | if (process.platform !== 'win32') { 621 | if (isExplicitJS) { 622 | args.unshift(bin); 623 | // add executable arguments to spawn 624 | args = (process.execArgv || []).concat(args); 625 | 626 | proc = spawn(process.argv[0], args, { stdio: 'inherit', customFds: [0, 1, 2] }); 627 | } else { 628 | proc = spawn(bin, args, { stdio: 'inherit', customFds: [0, 1, 2] }); 629 | } 630 | } else { 631 | args.unshift(bin); 632 | proc = spawn(process.execPath, args, { stdio: 'inherit'}); 633 | } 634 | 635 | var signals = ['SIGUSR1', 'SIGUSR2', 'SIGTERM', 'SIGINT', 'SIGHUP']; 636 | signals.forEach(function(signal) { 637 | process.on(signal, function(){ 638 | if ((proc.killed === false) && (proc.exitCode === null)){ 639 | proc.kill(signal); 640 | } 641 | }); 642 | }); 643 | proc.on('close', process.exit.bind(process)); 644 | proc.on('error', function(err) { 645 | if (err.code == "ENOENT") { 646 | console.error('\n %s(1) does not exist, try --help\n', bin); 647 | } else if (err.code == "EACCES") { 648 | console.error('\n %s(1) not executable. try chmod or run with root\n', bin); 649 | } 650 | process.exit(1); 651 | }); 652 | 653 | // Store the reference to the child process 654 | this.runningCommand = proc; 655 | }; 656 | 657 | /** 658 | * Normalize `args`, splitting joined short flags. For example 659 | * the arg "-abc" is equivalent to "-a -b -c". 660 | * This also normalizes equal sign and splits "--abc=def" into "--abc def". 661 | * 662 | * @param {Array} args 663 | * @return {Array} 664 | * @api private 665 | */ 666 | 667 | Command.prototype.normalize = function(args) { 668 | var ret = [] 669 | , arg 670 | , lastOpt 671 | , index; 672 | 673 | for (var i = 0, len = args.length; i < len; ++i) { 674 | arg = args[i]; 675 | if (i > 0) { 676 | lastOpt = this.optionFor(args[i-1]); 677 | } 678 | 679 | if (arg === '--') { 680 | // Honor option terminator 681 | ret = ret.concat(args.slice(i)); 682 | break; 683 | } else if (lastOpt && lastOpt.required) { 684 | ret.push(arg); 685 | } else if (arg.length > 1 && '-' == arg[0] && '-' != arg[1]) { 686 | arg.slice(1).split('').forEach(function(c) { 687 | ret.push('-' + c); 688 | }); 689 | } else if (/^--/.test(arg) && ~(index = arg.indexOf('='))) { 690 | ret.push(arg.slice(0, index), arg.slice(index + 1)); 691 | } else { 692 | ret.push(arg); 693 | } 694 | } 695 | 696 | return ret; 697 | }; 698 | 699 | 700 | 701 | /** 702 | * Parse command `args`. 703 | * 704 | * When listener(s) are available those 705 | * callbacks are invoked, otherwise the "*" 706 | * event is emitted and those actions are invoked. 707 | * 708 | * @param {Array} args 709 | * @return {Command} for chaining 710 | * @api private 711 | */ 712 | 713 | Command.prototype.parseArgs = function(args, unknown) { 714 | var name; 715 | 716 | if (args.length) { 717 | name = args[0]; 718 | if (this.listeners('command:' + name).length) { 719 | this.emit('command:' + args.shift(), args, unknown); 720 | } else { 721 | this.emit('command:*', args); 722 | } 723 | } else { 724 | outputHelpIfNecessary(this, unknown); 725 | 726 | // If there were no args and we have unknown options, 727 | // then they are extraneous and we need to error. 728 | if (unknown.length > 0) { 729 | this.unknownOption(unknown[0]); 730 | } 731 | } 732 | 733 | return this; 734 | }; 735 | 736 | /** 737 | * Return an option matching `arg` if any. 738 | * 739 | * @param {String} arg 740 | * @return {Option} 741 | * @api private 742 | */ 743 | 744 | Command.prototype.optionFor = function(arg) { 745 | for (var i = 0, len = this.options.length; i < len; ++i) { 746 | if (this.options[i].is(arg)) { 747 | return this.options[i]; 748 | } 749 | } 750 | }; 751 | 752 | /** 753 | * Parse options from `argv` returning `argv` 754 | * void of these options. 755 | * 756 | * @param {Array} argv 757 | * @return {Array} 758 | * @api public 759 | */ 760 | 761 | Command.prototype.parseOptions = function(argv) { 762 | var args = [] 763 | , len = argv.length 764 | , literal 765 | , option 766 | , arg; 767 | 768 | var unknownOptions = []; 769 | 770 | // parse options 771 | for (var i = 0; i < len; ++i) { 772 | arg = argv[i]; 773 | 774 | // literal args after -- 775 | if (literal) { 776 | args.push(arg); 777 | continue; 778 | } 779 | 780 | if ('--' == arg) { 781 | literal = true; 782 | continue; 783 | } 784 | 785 | // find matching Option 786 | option = this.optionFor(arg); 787 | 788 | // option is defined 789 | if (option) { 790 | // requires arg 791 | if (option.required) { 792 | arg = argv[++i]; 793 | if (null == arg) return this.optionMissingArgument(option); 794 | this.emit('option:' + option.name(), arg); 795 | // optional arg 796 | } else if (option.optional) { 797 | arg = argv[i+1]; 798 | if (null == arg || ('-' == arg[0] && '-' != arg)) { 799 | arg = null; 800 | } else { 801 | ++i; 802 | } 803 | this.emit('option:' + option.name(), arg); 804 | // bool 805 | } else { 806 | this.emit('option:' + option.name()); 807 | } 808 | continue; 809 | } 810 | 811 | // looks like an option 812 | if (arg.length > 1 && '-' == arg[0]) { 813 | unknownOptions.push(arg); 814 | 815 | // If the next argument looks like it might be 816 | // an argument for this option, we pass it on. 817 | // If it isn't, then it'll simply be ignored 818 | if (argv[i+1] && '-' != argv[i+1][0]) { 819 | unknownOptions.push(argv[++i]); 820 | } 821 | continue; 822 | } 823 | 824 | // arg 825 | args.push(arg); 826 | } 827 | 828 | return { args: args, unknown: unknownOptions }; 829 | }; 830 | 831 | /** 832 | * Return an object containing options as key-value pairs 833 | * 834 | * @return {Object} 835 | * @api public 836 | */ 837 | Command.prototype.opts = function() { 838 | var result = {} 839 | , len = this.options.length; 840 | 841 | for (var i = 0 ; i < len; i++) { 842 | var key = camelcase(this.options[i].name()); 843 | result[key] = key === 'version' ? this._version : this[key]; 844 | } 845 | return result; 846 | }; 847 | 848 | /** 849 | * Argument `name` is missing. 850 | * 851 | * @param {String} name 852 | * @api private 853 | */ 854 | 855 | Command.prototype.missingArgument = function(name) { 856 | console.error(); 857 | console.error(" error: missing required argument `%s'", name); 858 | console.error(); 859 | process.exit(1); 860 | }; 861 | 862 | /** 863 | * `Option` is missing an argument, but received `flag` or nothing. 864 | * 865 | * @param {String} option 866 | * @param {String} flag 867 | * @api private 868 | */ 869 | 870 | Command.prototype.optionMissingArgument = function(option, flag) { 871 | console.error(); 872 | if (flag) { 873 | console.error(" error: option `%s' argument missing, got `%s'", option.flags, flag); 874 | } else { 875 | console.error(" error: option `%s' argument missing", option.flags); 876 | } 877 | console.error(); 878 | process.exit(1); 879 | }; 880 | 881 | /** 882 | * Unknown option `flag`. 883 | * 884 | * @param {String} flag 885 | * @api private 886 | */ 887 | 888 | Command.prototype.unknownOption = function(flag) { 889 | if (this._allowUnknownOption) return; 890 | console.error(); 891 | console.error(" error: unknown option `%s'", flag); 892 | console.error(); 893 | process.exit(1); 894 | }; 895 | 896 | /** 897 | * Variadic argument with `name` is not the last argument as required. 898 | * 899 | * @param {String} name 900 | * @api private 901 | */ 902 | 903 | Command.prototype.variadicArgNotLast = function(name) { 904 | console.error(); 905 | console.error(" error: variadic arguments must be last `%s'", name); 906 | console.error(); 907 | process.exit(1); 908 | }; 909 | 910 | /** 911 | * Set the program version to `str`. 912 | * 913 | * This method auto-registers the "-V, --version" flag 914 | * which will print the version number when passed. 915 | * 916 | * @param {String} str 917 | * @param {String} [flags] 918 | * @return {Command} for chaining 919 | * @api public 920 | */ 921 | 922 | Command.prototype.version = function(str, flags) { 923 | if (0 == arguments.length) return this._version; 924 | this._version = str; 925 | flags = flags || '-V, --version'; 926 | this.option(flags, 'output the version number'); 927 | this.on('option:version', function() { 928 | process.stdout.write(str + '\n'); 929 | process.exit(0); 930 | }); 931 | return this; 932 | }; 933 | 934 | /** 935 | * Set the description to `str`. 936 | * 937 | * @param {String} str 938 | * @return {String|Command} 939 | * @api public 940 | */ 941 | 942 | Command.prototype.description = function(str) { 943 | if (0 === arguments.length) return this._description; 944 | this._description = str; 945 | return this; 946 | }; 947 | 948 | /** 949 | * Set an alias for the command 950 | * 951 | * @param {String} alias 952 | * @return {String|Command} 953 | * @api public 954 | */ 955 | 956 | Command.prototype.alias = function(alias) { 957 | var command = this; 958 | if(this.commands.length !== 0) { 959 | command = this.commands[this.commands.length - 1] 960 | } 961 | 962 | if (arguments.length === 0) return command._alias; 963 | 964 | command._alias = alias; 965 | return this; 966 | }; 967 | 968 | /** 969 | * Set / get the command usage `str`. 970 | * 971 | * @param {String} str 972 | * @return {String|Command} 973 | * @api public 974 | */ 975 | 976 | Command.prototype.usage = function(str) { 977 | var args = this._args.map(function(arg) { 978 | return humanReadableArgName(arg); 979 | }); 980 | 981 | var usage = '[options]' 982 | + (this.commands.length ? ' [command]' : '') 983 | + (this._args.length ? ' ' + args.join(' ') : ''); 984 | 985 | if (0 == arguments.length) return this._usage || usage; 986 | this._usage = str; 987 | 988 | return this; 989 | }; 990 | 991 | /** 992 | * Get or set the name of the command 993 | * 994 | * @param {String} str 995 | * @return {String|Command} 996 | * @api public 997 | */ 998 | 999 | Command.prototype.name = function(str) { 1000 | if (0 === arguments.length) return this._name; 1001 | this._name = str; 1002 | return this; 1003 | }; 1004 | 1005 | /** 1006 | * Return the largest option length. 1007 | * 1008 | * @return {Number} 1009 | * @api private 1010 | */ 1011 | 1012 | Command.prototype.largestOptionLength = function() { 1013 | return this.options.reduce(function(max, option) { 1014 | return Math.max(max, option.flags.length); 1015 | }, 0); 1016 | }; 1017 | 1018 | /** 1019 | * Return help for options. 1020 | * 1021 | * @return {String} 1022 | * @api private 1023 | */ 1024 | 1025 | Command.prototype.optionHelp = function() { 1026 | var width = this.largestOptionLength(); 1027 | 1028 | // Append the help information 1029 | return this.options.map(function(option) { 1030 | return pad(option.flags, width) + ' ' + option.description; 1031 | }).concat([pad('-h, --help', width) + ' ' + 'output usage information']) 1032 | .join('\n'); 1033 | }; 1034 | 1035 | /** 1036 | * Return command help documentation. 1037 | * 1038 | * @return {String} 1039 | * @api private 1040 | */ 1041 | 1042 | Command.prototype.commandHelp = function() { 1043 | if (!this.commands.length) return ''; 1044 | 1045 | var commands = this.commands.filter(function(cmd) { 1046 | return !cmd._noHelp; 1047 | }).map(function(cmd) { 1048 | var args = cmd._args.map(function(arg) { 1049 | return humanReadableArgName(arg); 1050 | }).join(' '); 1051 | 1052 | return [ 1053 | cmd._name 1054 | + (cmd._alias ? '|' + cmd._alias : '') 1055 | + (cmd.options.length ? ' [options]' : '') 1056 | + ' ' + args 1057 | , cmd._description 1058 | ]; 1059 | }); 1060 | 1061 | var width = commands.reduce(function(max, command) { 1062 | return Math.max(max, command[0].length); 1063 | }, 0); 1064 | 1065 | return [ 1066 | '' 1067 | , ' Commands:' 1068 | , '' 1069 | , commands.map(function(cmd) { 1070 | var desc = cmd[1] ? ' ' + cmd[1] : ''; 1071 | return pad(cmd[0], width) + desc; 1072 | }).join('\n').replace(/^/gm, ' ') 1073 | , '' 1074 | ].join('\n'); 1075 | }; 1076 | 1077 | /** 1078 | * Return program help documentation. 1079 | * 1080 | * @return {String} 1081 | * @api private 1082 | */ 1083 | 1084 | Command.prototype.helpInformation = function() { 1085 | var desc = []; 1086 | if (this._description) { 1087 | desc = [ 1088 | ' ' + this._description 1089 | , '' 1090 | ]; 1091 | } 1092 | 1093 | var cmdName = this._name; 1094 | if (this._alias) { 1095 | cmdName = cmdName + '|' + this._alias; 1096 | } 1097 | var usage = [ 1098 | '' 1099 | ,' Usage: ' + cmdName + ' ' + this.usage() 1100 | , '' 1101 | ]; 1102 | 1103 | var cmds = []; 1104 | var commandHelp = this.commandHelp(); 1105 | if (commandHelp) cmds = [commandHelp]; 1106 | 1107 | var options = [ 1108 | '' 1109 | , ' Options:' 1110 | , '' 1111 | , '' + this.optionHelp().replace(/^/gm, ' ') 1112 | , '' 1113 | ]; 1114 | 1115 | return usage 1116 | .concat(desc) 1117 | .concat(options) 1118 | .concat(cmds) 1119 | .join('\n'); 1120 | }; 1121 | 1122 | /** 1123 | * Output help information for this command 1124 | * 1125 | * @api public 1126 | */ 1127 | 1128 | Command.prototype.outputHelp = function(cb) { 1129 | if (!cb) { 1130 | cb = function(passthru) { 1131 | return passthru; 1132 | } 1133 | } 1134 | process.stdout.write(cb(this.helpInformation())); 1135 | this.emit('--help'); 1136 | }; 1137 | 1138 | /** 1139 | * Output help information and exit. 1140 | * 1141 | * @api public 1142 | */ 1143 | 1144 | Command.prototype.help = function(cb) { 1145 | this.outputHelp(cb); 1146 | process.exit(); 1147 | }; 1148 | 1149 | /** 1150 | * Camel-case the given `flag` 1151 | * 1152 | * @param {String} flag 1153 | * @return {String} 1154 | * @api private 1155 | */ 1156 | 1157 | function camelcase(flag) { 1158 | return flag.split('-').reduce(function(str, word) { 1159 | return str + word[0].toUpperCase() + word.slice(1); 1160 | }); 1161 | } 1162 | 1163 | /** 1164 | * Pad `str` to `width`. 1165 | * 1166 | * @param {String} str 1167 | * @param {Number} width 1168 | * @return {String} 1169 | * @api private 1170 | */ 1171 | 1172 | function pad(str, width) { 1173 | var len = Math.max(0, width - str.length); 1174 | return str + Array(len + 1).join(' '); 1175 | } 1176 | 1177 | /** 1178 | * Output help information if necessary 1179 | * 1180 | * @param {Command} command to output help for 1181 | * @param {Array} array of options to search for -h or --help 1182 | * @api private 1183 | */ 1184 | 1185 | function outputHelpIfNecessary(cmd, options) { 1186 | options = options || []; 1187 | for (var i = 0; i < options.length; i++) { 1188 | if (options[i] == '--help' || options[i] == '-h') { 1189 | cmd.outputHelp(); 1190 | process.exit(0); 1191 | } 1192 | } 1193 | } 1194 | 1195 | /** 1196 | * Takes an argument an returns its human readable equivalent for help usage. 1197 | * 1198 | * @param {Object} arg 1199 | * @return {String} 1200 | * @api private 1201 | */ 1202 | 1203 | function humanReadableArgName(arg) { 1204 | var nameOutput = arg.name + (arg.variadic === true ? '...' : ''); 1205 | 1206 | return arg.required 1207 | ? '<' + nameOutput + '>' 1208 | : '[' + nameOutput + ']' 1209 | } 1210 | 1211 | // for versions before node v0.8 when there weren't `fs.existsSync` 1212 | function exists(file) { 1213 | try { 1214 | if (fs.statSync(file).isFile()) { 1215 | return true; 1216 | } 1217 | } catch (e) { 1218 | return false; 1219 | } 1220 | } 1221 | // [[1,2],[3,4]] => [[1,3], [1,4], [2,3], [2,4]] 1222 | function catesianProduct(list) { 1223 | return list.reverse().reduce((p,c) => c.reduce((q,w) => q.concat(p.map(v => v.concat(w))),[]), [[]]).map(v => v.reverse()) 1224 | } 1225 | --------------------------------------------------------------------------------