├── .nvmrc ├── .npmignore ├── .eslintignore ├── .babelrc ├── bin ├── vuex.js ├── vuex-g.js ├── vuex-init.js ├── vuex-new.js ├── vuex-compile.js ├── vuex-server.js └── vuex-generate.js ├── .travis.yml ├── .gitignore ├── .mocharc ├── blueprints ├── component │ ├── files │ │ └── src │ │ │ ├── styles │ │ │ └── components │ │ │ │ └── __style__.css │ │ │ └── components │ │ │ └── __name__.vue │ └── index.js ├── route │ ├── index.js │ └── files │ │ └── src │ │ └── routes │ │ └── __name__ │ │ ├── components │ │ └── __name__View.vue │ │ ├── index.js │ │ └── modules │ │ └── __name__.js └── .eslintrc ├── templates └── .starterrc ├── src ├── config.js ├── prompts │ ├── setup.js │ └── initPrompt.js ├── cli │ ├── vuex-init.js │ ├── vuex-new.js │ ├── vuex-compile.js │ ├── vuex-server.js │ ├── vuex-generate.js │ └── index.js ├── models │ ├── task.js │ ├── sub-command.js │ ├── project-settings.js │ ├── file-info.js │ ├── logger.js │ └── blueprint.js ├── version.js ├── util │ ├── mixin.js │ ├── text-helper.js │ └── fs.js ├── tasks │ ├── webpack-compile.js │ ├── webpack-server.js │ ├── git-pull.js │ ├── create-and-step-into-directory.js │ └── generate-from-blueprint.js └── sub-commands │ ├── server.js │ ├── compile.js │ ├── init.js │ ├── generate.js │ └── new.js ├── tests ├── .eslintrc ├── index.js └── utils │ └── fs.spec.js ├── .eslintrc ├── README.md ├── LICENSE └── package.json /.nvmrc: -------------------------------------------------------------------------------- 1 | 6.3.0 -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | src/ -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | lib -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015" 4 | ] 5 | } -------------------------------------------------------------------------------- /bin/vuex.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/cli'); -------------------------------------------------------------------------------- /bin/vuex-g.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('./vuex-generate'); 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6.2" 4 | - "6.1" 5 | - "5.1" -------------------------------------------------------------------------------- /bin/vuex-init.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/cli/vuex-init'); 4 | -------------------------------------------------------------------------------- /bin/vuex-new.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/cli/vuex-new'); 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | npm-debug.log 4 | coverage/ 5 | tmp/ 6 | .vuexrc 7 | -------------------------------------------------------------------------------- /bin/vuex-compile.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/cli/vuex-compile'); -------------------------------------------------------------------------------- /bin/vuex-server.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/cli/vuex-server'); 4 | -------------------------------------------------------------------------------- /.mocharc: -------------------------------------------------------------------------------- 1 | --require ./tests/index.js 2 | --full-trace 3 | --recursive ./tests/**/*.spec.js 4 | -------------------------------------------------------------------------------- /bin/vuex-generate.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../lib/cli/vuex-generate'); 4 | -------------------------------------------------------------------------------- /blueprints/component/files/src/styles/components/__style__.css: -------------------------------------------------------------------------------- 1 | .v-<%= dashesEntityName%>{ 2 | 3 | } -------------------------------------------------------------------------------- /blueprints/route/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | description() { 3 | return '创建路由。'; 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /templates/.starterrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceBase":"src", 3 | "testBase":"tests", 4 | "smartPath":"containers", 5 | "dumbPath":"components", 6 | "fileCasing":"default" 7 | } 8 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | let config = { 4 | basePath: process.cwd(), 5 | pkgBasePath: path.dirname(module.id) 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /src/prompts/setup.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | export const setupPrompt = (promptType, prompt) => { 4 | prompt.message = chalk.green(`${promptType}: `); 5 | prompt.delimiter = ''; 6 | prompt.start(); 7 | }; 8 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "../.eslintrc", 3 | "env" : { 4 | "mocha" : true 5 | }, 6 | "globals" : { 7 | "expect" : false, 8 | "should" : false, 9 | "sinon" : false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /blueprints/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends" : "../.eslintrc", 3 | "env" : { 4 | "mocha" : true 5 | }, 6 | "globals" : { 7 | "expect" : false, 8 | "should" : false, 9 | "sinon" : false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/cli/vuex-init.js: -------------------------------------------------------------------------------- 1 | import commander from 'commander'; 2 | import Init from '../sub-commands/init'; 3 | 4 | const subCommand = new Init(); 5 | commander.on('--help', () => { 6 | subCommand.printUserHelp(); 7 | }); 8 | 9 | subCommand.run(); -------------------------------------------------------------------------------- /tests/index.js: -------------------------------------------------------------------------------- 1 | // --------------------------------------- 2 | // Test Environment Setup 3 | // --------------------------------------- 4 | import {expect} from "chai"; 5 | import sinon from "sinon"; 6 | 7 | global.expect = expect; 8 | global.sinon = sinon; 9 | -------------------------------------------------------------------------------- /src/models/task.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 任务基类 3 | */ 4 | class Task { 5 | constructor(environment) { 6 | this.logger = environment.logger; 7 | this.settings = environment.settings; 8 | } 9 | 10 | run() { 11 | throw new Error('任务必须实现 run()'); 12 | } 13 | } 14 | 15 | export default Task; 16 | -------------------------------------------------------------------------------- /blueprints/component/index.js: -------------------------------------------------------------------------------- 1 | var utils = require('../../lib/util/text-helper'); 2 | 3 | module.exports = { 4 | description() { 5 | return '创建组件'; 6 | }, 7 | fileMapTokens() { 8 | return { 9 | __style__: (options) => utils.normalizeCasing(options.entity.name, 'dashes') 10 | }; 11 | } 12 | }; -------------------------------------------------------------------------------- /blueprints/route/files/src/routes/__name__/components/__name__View.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /src/version.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { readJsonSync } from 'fs-extra'; 3 | import config from './config'; 4 | 5 | const { pkgBasePath } = config; 6 | 7 | export const version = () => { 8 | const pkgPath = path.join(pkgBasePath, '../package.json'); 9 | return readJsonSync(pkgPath).version; 10 | }; 11 | -------------------------------------------------------------------------------- /src/util/mixin.js: -------------------------------------------------------------------------------- 1 | // Simple mixin utility that acts like 'extends' 2 | const mixin = (Parent, ...mixins) => { 3 | class Mixed extends Parent {} 4 | for (let mixin of mixins) { 5 | for (let prop in mixin) { 6 | Mixed.prototype[prop] = mixin[prop]; 7 | } 8 | } 9 | return Mixed; 10 | }; 11 | 12 | export default mixin; 13 | -------------------------------------------------------------------------------- /src/tasks/webpack-compile.js: -------------------------------------------------------------------------------- 1 | import Task from '../models/task'; 2 | import config from '../config'; 3 | import { compile } from 'vuex-cli-webpack' 4 | 5 | export default class extends Task { 6 | constructor(environment) { 7 | super(environment); 8 | } 9 | 10 | run(config) { 11 | const logger = this.logger; 12 | logger.writeInfo('正在编译代码...') 13 | compile() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/tasks/webpack-server.js: -------------------------------------------------------------------------------- 1 | import Task from '../models/task'; 2 | import config from '../config'; 3 | import { server } from 'vuex-cli-webpack' 4 | 5 | export default class extends Task { 6 | constructor(environment) { 7 | super(environment); 8 | } 9 | 10 | run(config) { 11 | const logger = this.logger; 12 | logger.writeInfo('正在启动开发服务器...') 13 | server() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/cli/vuex-new.js: -------------------------------------------------------------------------------- 1 | import commander from 'commander'; 2 | import New from '../sub-commands/new'; 3 | 4 | const subCommand = new New(); 5 | commander.on('--help', () => { 6 | subCommand.printUserHelp(); 7 | }); 8 | 9 | commander 10 | .arguments('') 11 | .action((dirName) => { 12 | subCommand.run({ 13 | dirName: dirName 14 | }); 15 | }) 16 | .parse(process.argv); -------------------------------------------------------------------------------- /src/cli/vuex-compile.js: -------------------------------------------------------------------------------- 1 | import commander from 'commander'; 2 | import { version } from '../version'; 3 | import Compile from '../sub-commands/compile'; 4 | 5 | const subCommand = new Compile(); 6 | 7 | commander.on('--help', () => { 8 | subCommand.printUserHelp(); 9 | }); 10 | 11 | commander 12 | .version(version()) 13 | .description(' 编译代码') 14 | .parse(process.argv); 15 | 16 | 17 | subCommand.run(); 18 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": "eslint:recommended", 4 | "rules": { 5 | "quotes": [2, "single"], 6 | "linebreak-style": [2, "unix"], 7 | "semi": [2, "always"], 8 | "no-console": [0] 9 | }, 10 | "env": { 11 | "es6": true, 12 | "node": true, 13 | "browser": true 14 | }, 15 | "ecmaFeatures": { 16 | "experimentalObjectRestSpread": true 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /blueprints/route/files/src/routes/__name__/index.js: -------------------------------------------------------------------------------- 1 | // Sync route definition 2 | export default { 3 | name: '<%= pascalEntityName %>', 4 | title: '<%= pascalEntityName %>', 5 | exact: true, 6 | component: resolve => require(['./components/<%= pascalEntityName %>View'], resolve) 7 | }; 8 | // <%= pascalEntityName %> 9 | // <%= camelEntityName %> 10 | // <%= snakeEntityName %> 11 | // <%= dashesEntityName %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /blueprints/component/files/src/components/__name__.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | -------------------------------------------------------------------------------- /src/cli/vuex-server.js: -------------------------------------------------------------------------------- 1 | import commander from 'commander'; 2 | import { version } from '../version'; 3 | import Server from '../sub-commands/server'; 4 | // import minimist from 'minimist'; 5 | 6 | const subCommand = new Server(); 7 | 8 | commander.on('--help', () => { 9 | subCommand.printUserHelp(); 10 | }); 11 | 12 | commander 13 | .version(version()) 14 | .description(' 启动开发服务器') 15 | .parse(process.argv); 16 | 17 | 18 | subCommand.run(); 19 | -------------------------------------------------------------------------------- /src/sub-commands/server.js: -------------------------------------------------------------------------------- 1 | import SubCommand from '../models/sub-command'; 2 | import WebServer from '../tasks/webpack-server'; 3 | 4 | // 开发服务器 5 | class Serve extends SubCommand { 6 | constructor() { 7 | super(); 8 | this.webserver = new WebServer(this.environment); 9 | } 10 | 11 | printUserHelp() { 12 | this.logger.write( 13 | ' vuex server --help ' 14 | ); 15 | } 16 | 17 | run(cliArg) { 18 | this.webserver.run(cliArg); 19 | } 20 | } 21 | 22 | export default Serve; 23 | -------------------------------------------------------------------------------- /src/sub-commands/compile.js: -------------------------------------------------------------------------------- 1 | import SubCommand from '../models/sub-command'; 2 | import CompileTask from '../tasks/webpack-compile'; 3 | 4 | // 开发服务器 5 | class Compile extends SubCommand { 6 | constructor() { 7 | super(); 8 | this.compile = new CompileTask(this.environment); 9 | } 10 | 11 | printUserHelp() { 12 | this.logger.write( 13 | ' vuex compile --help ' 14 | ); 15 | } 16 | 17 | run(cliArg) { 18 | this.compile.run(cliArg); 19 | } 20 | } 21 | 22 | export default Compile; 23 | -------------------------------------------------------------------------------- /src/models/sub-command.js: -------------------------------------------------------------------------------- 1 | import ProjectSettings from './project-settings'; 2 | import Logger from './logger'; 3 | 4 | /** 5 | * 子命令基类 6 | */ 7 | class SubCommand { 8 | constructor(options = {}) { 9 | this.rawOptions = options; 10 | this.settings = options.settings || new ProjectSettings(); 11 | this.logger = options.logger || new Logger(); 12 | 13 | this.environment = { 14 | logger: this.logger, 15 | settings: this.settings 16 | }; 17 | } 18 | 19 | run() { 20 | throw new Error('子命令必须实现 run()'); 21 | } 22 | 23 | availableOptions() { 24 | throw new Error('子命令必须实现 availableOptions()'); 25 | } 26 | } 27 | 28 | export default SubCommand; 29 | -------------------------------------------------------------------------------- /src/prompts/initPrompt.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | 3 | const schema = { 4 | properties: { 5 | sourceBase: { 6 | description: chalk.blue('源码路径? (相对跟路径)'), 7 | type: 'string', 8 | required: true 9 | }, 10 | testBase: { 11 | description: chalk.blue('测试代码路径? (相对跟路径)'), 12 | type: 'string', 13 | required: true 14 | }, 15 | dumbPath: { 16 | description: chalk.blue('业务组件路径?'), 17 | type: 'string', 18 | required: true 19 | }, 20 | fileCasing: { 21 | description: chalk.blue('文件命名风格? (default|snake|pascal|camel)'), 22 | pattern: /(default|snake|pascal|camel|dashes)/, 23 | required: true, 24 | type: 'string' 25 | } 26 | } 27 | }; 28 | 29 | export default schema; 30 | -------------------------------------------------------------------------------- /src/tasks/git-pull.js: -------------------------------------------------------------------------------- 1 | import Task from '../models/task'; 2 | import denodeify from 'denodeify'; 3 | 4 | const exec = denodeify(require('child_process').exec); 5 | 6 | export default class extends Task { 7 | constructor(environment) { 8 | super(environment); 9 | } 10 | 11 | run(gitUrl) { 12 | const logger = this.logger; 13 | logger.startProgress(`正在从Github获取 ${gitUrl}.`); 14 | 15 | return exec(`git pull ${gitUrl}`, {silent: true}).then((err, stdout, stderr) => { 16 | logger.stopProgress(); 17 | 18 | if (err) { 19 | logger.writeError('出了些问题... 请重试. 请检查你的网络连接是否正常'); 20 | logger.writeError(`错误代码: ${err}`); 21 | logger.writeError(stdout); 22 | logger.writeError(stderr); 23 | process.exit(1); 24 | } 25 | logger.writeInfo('pulled down repo'); 26 | Promise.resolve(); 27 | }); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/cli/vuex-generate.js: -------------------------------------------------------------------------------- 1 | import commander from 'commander'; 2 | import { version } from '../version'; 3 | import Generate from '../sub-commands/generate'; 4 | import minimist from 'minimist'; 5 | 6 | const subCommand = new Generate(); 7 | 8 | commander.on('--help', () => { 9 | subCommand.printUserHelp(); 10 | }); 11 | 12 | commander 13 | .version(version()) 14 | .arguments(' [entity name]') 15 | .option('-d, --debugger', '开启调试模式') 16 | .description('基于Blueprints 模板 生成代码') 17 | .action((blueprintName, entityName, command) => { 18 | const debug = command.debugger; 19 | const rawArgs = command.rawArgs; 20 | const options = minimist(rawArgs.slice(2)); 21 | 22 | const cliArgs = { 23 | entity: { 24 | name: entityName, 25 | options, 26 | rawArgs 27 | }, 28 | debug 29 | }; 30 | subCommand.run(blueprintName, cliArgs); 31 | }) 32 | .parse(process.argv); 33 | -------------------------------------------------------------------------------- /blueprints/route/files/src/routes/__name__/modules/__name__.js: -------------------------------------------------------------------------------- 1 | // ------------------------------------ 2 | // Constants 3 | // ------------------------------------ 4 | // export const ADD_USER = 'ADD_USER' 5 | 6 | // ------------------------------------ 7 | // States 8 | // ------------------------------------ 9 | const state = { 10 | 11 | }; 12 | 13 | // ------------------------------------ 14 | // Getters 15 | // ------------------------------------ 16 | export const gettes = { 17 | 18 | }; 19 | 20 | // ------------------------------------ 21 | // Action Handlers 22 | // ------------------------------------ 23 | export const actions = { 24 | 25 | }; 26 | 27 | // ------------------------------------ 28 | // Mutations 29 | // ------------------------------------ 30 | export const mutations = { 31 | 32 | }; 33 | 34 | export default { 35 | state, 36 | gettes, 37 | actions, 38 | mutations 39 | }; -------------------------------------------------------------------------------- /tests/utils/fs.spec.js: -------------------------------------------------------------------------------- 1 | import { fileExists, readFile } from 'util/fs'; 2 | import fse from 'fs-extra'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | 6 | describe('(Utils) fs', () => { 7 | describe('#fileExists', () => { 8 | it('returns true when file exists', () => { 9 | const finalPath = path.join(process.cwd(), 'tmp/example.js'); 10 | fse.outputFileSync(finalPath, 'path'); 11 | 12 | expect(fileExists(finalPath)).to.be.true; 13 | fse.removeSync(finalPath); 14 | }); 15 | 16 | it('returns false when file doesnt exist', () => { 17 | expect(fileExists('tmp/some/random/path')).to.be.false; 18 | }); 19 | 20 | it('throws error when not file present error', () => { 21 | const error = { 22 | message: 'random error', 23 | code: 'random code' 24 | }; 25 | sinon.stub(fs, 'accessSync').throws(error); 26 | 27 | try { 28 | fileExists('tmp/example.js'); 29 | } catch (e) { 30 | expect(e.code).to.eql('random code'); 31 | } 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/tasks/create-and-step-into-directory.js: -------------------------------------------------------------------------------- 1 | import { test, cd, exec } from 'shelljs'; 2 | import fs from 'fs'; 3 | import denodeify from 'denodeify'; 4 | 5 | import Task from '../models/task'; 6 | 7 | const mkdir = denodeify(fs.mkdir); 8 | 9 | export default class extends Task { 10 | constructor(environment) { 11 | super(environment); 12 | } 13 | 14 | run(options) { 15 | this.dirName = options.dirName; 16 | this.confirmDir(); 17 | 18 | this.logger.writeInfo('创建新文件夹中...'); 19 | return mkdir(this.dirName) 20 | .then(() => { 21 | cd(this.dirName); 22 | this.initGit(); 23 | this.logger.writeCreate(`创建完成: ${this.dirName}`); 24 | }); 25 | } 26 | 27 | confirmDir() { 28 | if (test('-d', this.dirName)) { 29 | this.logger.writeError(`${this.dirName} 文件夹以存在! 请换个名字重试`); 30 | process.exit(1); 31 | } 32 | } 33 | 34 | initGit() { 35 | this.logger.writeInfo('正在初始化 git...'); 36 | exec('git init', {silent: true}); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/cli/index.js: -------------------------------------------------------------------------------- 1 | import commander from 'commander'; 2 | import { version } from '../version'; 3 | 4 | const program = commander; 5 | 6 | program 7 | .version(version()); 8 | 9 | program 10 | .command('init', '初始化配置文件 .vuexrc'); 11 | 12 | program 13 | .command('new', '创建一个新的 vuex 项目'); 14 | 15 | // fake an alias for generate since commander doesn't support 16 | // aliases for sub-commands. 17 | program 18 | .command('generate', '基于Blueprints模板创建结构') 19 | .command('g', 'generate 的别名'); 20 | 21 | program 22 | .command('server', '开启开发服务器'); 23 | 24 | program 25 | .command('compile', '编译代码'); 26 | 27 | program.on('--help', () => { 28 | console.log(' Examples:'); 29 | console.log(''); 30 | console.log(' $ vuex init --help'); 31 | console.log(' $ vuex new my-vuex-project'); 32 | console.log(' $ vuex g component MyComponent'); 33 | console.log(' **** new:'); 34 | console.log(' $ vuex server'); 35 | console.log(' $ vuex compile'); 36 | console.log(''); 37 | }); 38 | 39 | program.parse(process.argv); 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 一个 vuex 应用脚手架。 2 | ``` 3 | _ _ _____ _ _____ 4 | | | | | / __ \| | |_ _| 5 | | | | |_ _ _____ ________| / \/| | | | 6 | | | | | | | |/ _ \ \/ /______| | | | | | 7 | \ \_/ / |_| | __/> < | \__/\| |_____| |_ 8 | \___/ \__,_|\___/_/\_\ \____/\_____/\___/ 9 | ``` 10 | 11 | ## 更新日志 12 | 13 | 14 | + 2016.08.01 同步更新`vuex-cli-webpack@1.0.10` 15 | 16 | + 2016.07.27 集成webpack 配置服务 17 | - 启动webpack开发服务 `vuex serve` 18 | - 编译代码 `vuex compile` 19 | 20 | ## 使用 21 | 22 | ```javascript 23 | npm i vuex-cli -g // 安装 vuex-cli 到全局 24 | vuex new // 创建一个 新的 vuex 项目 25 | vuex init 26 | 27 | 28 | // 开始生成component/routes/tests 29 | //(g 为 generate 的别名) 30 | vuex g component SimpleButton 31 | ``` 32 | 33 | ## 备注 34 | 本项目基于[`redux-cli`](https://github.com/SpencerCDixon/redux-cli)创建。该项目借鉴了 `ember-cli`的 `blueprints`来作为脚手架工具。 35 | 36 | 初始化的项目基于[`Vue Vuex Starter Kit`](https://github.com/sokis/vue-vuex-starter-kit)构建 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/sub-commands/init.js: -------------------------------------------------------------------------------- 1 | import prompt from 'prompt'; 2 | import figlet from 'figlet'; 3 | 4 | import SubCommand from '../models/sub-command'; 5 | 6 | import initPrompt from '../prompts/initPrompt'; 7 | import { setupPrompt } from '../prompts/setup'; 8 | import { success } from '../util/text-helper'; 9 | 10 | class Init extends SubCommand { 11 | constructor() { 12 | super(); 13 | setupPrompt('initialization', prompt); 14 | } 15 | 16 | printUserHelp() { 17 | this.logger.write( 18 | '初始化命令用于在.vuexrc中设置项目配置' 19 | ); 20 | } 21 | 22 | run() { 23 | this.logger.write(this.cliLogo()); 24 | prompt.get(initPrompt, (err, result) => { 25 | 26 | if (err) return; 27 | 28 | this.logger.writeInfo('正在保存你的设置'); 29 | this.settings.setAllSettings(result); 30 | this.settings.save(); 31 | this.logger.writeCreate('.vuexrc 已保存在项目根目录.'); 32 | }); 33 | } 34 | 35 | cliLogo() { 36 | return success( 37 | figlet.textSync('Vuex-CLI', { 38 | font: 'Doom', 39 | horizontalLayout: 'default', 40 | verticalLayout: 'default' 41 | }) 42 | ); 43 | } 44 | } 45 | 46 | export default Init; 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 撒子都学 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 | -------------------------------------------------------------------------------- /src/tasks/generate-from-blueprint.js: -------------------------------------------------------------------------------- 1 | import Task from '../models/task'; 2 | import Blueprint from '../models/blueprint'; 3 | 4 | export default class extends Task { 5 | constructor(environment) { 6 | super(environment); 7 | } 8 | 9 | // confirm blueprint exists 10 | // go fetch blueprint object 11 | // noramlize/setup args to be passed to install 12 | // install the blueprint 13 | run(blueprintName, cliArgs) { 14 | // if blueprint doesnt exist 15 | // this.logger.writeError( 16 | // 'this is not a valid blueprint. type help for help.. or w/e' 17 | // ); 18 | // process.exit(1); 19 | // } 20 | 21 | const mainBlueprint = this.lookupBlueprint(blueprintName); 22 | 23 | const entity = { 24 | name: cliArgs.entity.name, 25 | options: cliArgs.entity.options 26 | }; 27 | 28 | const blueprintOptions = { 29 | originalBlueprintName: blueprintName, 30 | logger: this.logger, 31 | settings: this.settings, 32 | entity 33 | }; 34 | 35 | mainBlueprint.install(blueprintOptions); 36 | } 37 | 38 | lookupBlueprint(name) { 39 | return Blueprint.lookup(name); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/util/text-helper.js: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk'; 2 | import { depascalize, pascalize, camelize } from 'humps'; 3 | 4 | // Bootstrap inspired text color helpers for the command line. 5 | export const success = (text) => { 6 | return chalk.green(text); 7 | }; 8 | 9 | export const danger = (text) => { 10 | return chalk.red(text); 11 | }; 12 | 13 | export const warning = (text) => { 14 | return chalk.yellow(text); 15 | }; 16 | 17 | // Random string/text helpers 18 | export const normalizeComponentName = (name) => { 19 | return pascalize(name); 20 | }; 21 | 22 | export const normalizeDuckName = (name) => { 23 | return camelize(name); 24 | }; 25 | 26 | export const normalizeCasing = (string, casing) => { 27 | const types = ['default', 'snake', 'pascal', 'camel', 'dashes']; 28 | 29 | if (types.indexOf(casing) === -1) { 30 | throw new Error( 31 | `Casing must be one of: ${types.join(', ')} types` 32 | ); 33 | } 34 | 35 | if (casing === 'snake') { 36 | return depascalize(pascalize(string)); 37 | } else if (casing === 'pascal') { 38 | return pascalize(string); 39 | } else if (casing === 'camel') { 40 | return camelize(string); 41 | } else if (casing === 'default') { 42 | return string; 43 | } else if (casing == 'dashes') { 44 | return depascalize(string, { separator: '-' }); 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /src/util/fs.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | // import fse from 'fs-extra'; 4 | // import temp from 'temp'; 5 | // import denodeify from 'denodeify'; 6 | 7 | const rootPath = process.cwd(); 8 | // const mkdir = denodeify(fs.mkdir); 9 | // const mkTmpDir = denodeify(temp.mkdir); 10 | 11 | /* 12 | Node deprecated existsSync so this is a simple 13 | helper function wrapping try/catch around the new 14 | recommended approach of accessSync 15 | https://nodejs.org/api/fs.html#fs_fs_existssync_path 16 | */ 17 | export const fileExists = (filename) => { 18 | try { 19 | fs.accessSync(filename); 20 | return true; 21 | } catch (e) { 22 | if (e.code === 'ENOENT') { 23 | return false; 24 | } else { 25 | throw e; 26 | } 27 | } 28 | }; 29 | 30 | export const readFile = (filename) => { 31 | const filePath = path.join(rootPath, filename); 32 | return fs.readFileSync(filePath, 'utf8'); 33 | }; 34 | 35 | // Promise based fs helpers 36 | // currently not being used after blueprint refactor. Keeping in case 37 | // I want to use later... 38 | 39 | // export const dirExists = (dirPath) => { 40 | // return new Promise(resolve => { 41 | // fse.exists(dirPath, resolve); 42 | // }); 43 | // }; 44 | 45 | // export const mkTmpDirIn = (dirPath) => { 46 | // return dirExists(dirPath).then(doesExist => { 47 | // if (!doesExist) { 48 | // return mkdir(dirPath); 49 | // } 50 | // }).then(() => { 51 | // return mkTmpDir({ dir: dirPath}); 52 | // }); 53 | // }; 54 | 55 | -------------------------------------------------------------------------------- /src/sub-commands/generate.js: -------------------------------------------------------------------------------- 1 | import SubCommand from '../models/sub-command'; 2 | import Blueprint from '../models/blueprint'; 3 | import GenerateFromBluePrint from '../tasks/generate-from-blueprint'; 4 | import chalk from 'chalk'; 5 | 6 | // Primary purpose is to take cli args and pass them through 7 | // to the proper task that will do the generation. 8 | // 9 | // Logic for displaying all blueprints and what their options 10 | // are will live in here. For now it's pretty baren. 11 | class Generate extends SubCommand { 12 | constructor() { 13 | super(); 14 | this.generateTask = new GenerateFromBluePrint(this.environment); 15 | } 16 | 17 | printUserHelp() { 18 | const blueprints = Blueprint.list(); 19 | 20 | this.logger.writeLine(`可用 Blueprints:`); 21 | this.logger.writeLine('(以下源码将被顶层源码覆盖)'); 22 | this.logger.writeLine(''); 23 | 24 | blueprints.forEach(blueprintSource => { 25 | this.logger.writeLine(` ${chalk.blue('Blueprint Source')} ===> ${chalk.green(blueprintSource.source)}:`); 26 | 27 | blueprintSource.blueprints.forEach(blueprint => { 28 | this.logger.writeLine(` ${blueprint.name} ${chalk.yellow('')}`); 29 | this.logger.writeLine(` ${chalk.gray(blueprint.description)}`); 30 | }); 31 | this.logger.writeLine(''); 32 | }); 33 | } 34 | 35 | run(blueprintName, cliArgs) { 36 | if (cliArgs.debug) { 37 | this.logger.setWriteLevel('DEBUG'); 38 | } 39 | 40 | this.generateTask.run(blueprintName, cliArgs); 41 | } 42 | } 43 | 44 | export default Generate; 45 | -------------------------------------------------------------------------------- /src/models/project-settings.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import { copySync } from 'fs-extra'; 3 | import jf from 'jsonfile'; 4 | import { pwd } from 'shelljs'; 5 | 6 | import { fileExists } from '../util/fs'; 7 | 8 | /* 9 | Look into using Yam for finding settings so it will get the first 10 | .vuexrc it finds and use that for project settings just like how 11 | eslintrc and ember-cli works 12 | */ 13 | 14 | export default class ProjectSettings { 15 | constructor(relativePath) { 16 | this.relativePath = relativePath || '../../templates/.vuexrc'; 17 | this.loadSettings(); 18 | } 19 | 20 | /** 21 | * load 设置 22 | * 如果没有.vuexrc 文件,从模板中创建一份 23 | */ 24 | loadSettings() { 25 | if (this.settingsExist()) { 26 | this.settings = jf.readFileSync(this.settingsPath()); 27 | } else { 28 | this.buildFromTemplate(); 29 | this.settings = jf.readFileSync(this.settingsPath()); 30 | } 31 | } 32 | 33 | templatePath() { 34 | return path.join( 35 | path.dirname(module.id), this.relativePath 36 | ); 37 | } 38 | 39 | buildFromTemplate() { 40 | copySync(this.templatePath(), this.settingsPath()); 41 | } 42 | 43 | settingsPath() { 44 | return path.join(pwd(), '.vuexrc'); 45 | } 46 | 47 | settingsExist() { 48 | return fileExists(this.settingsPath()); 49 | } 50 | 51 | getSetting(key) { 52 | return this.settings[key]; 53 | } 54 | 55 | getAllSettings() { 56 | return this.settings; 57 | } 58 | 59 | setSetting(key, val) { 60 | this.settings[key] = val; 61 | } 62 | 63 | setAllSettings(json) { 64 | this.settings = json; 65 | } 66 | 67 | save() { 68 | jf.writeFileSync(this.settingsPath(), this.settings); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/models/file-info.js: -------------------------------------------------------------------------------- 1 | 2 | import ejs from 'ejs'; 3 | import fs from 'fs'; 4 | import { outputFileSync } from 'fs-extra'; 5 | import { fileExists } from '../util/fs'; 6 | 7 | class FileInfo { 8 | constructor(args) { 9 | this.logger = args.logger; 10 | this.templateVariables = args.templateVariables; // locals passed to ejs template 11 | this.originalPath = args.originalPath; // path to template 12 | this.mappedPath = args.mappedPath; // destination path to be written to 13 | } 14 | 15 | writeFile() { 16 | this.logger.writeDebug(`试图写入文件: ${this.mappedPath}`); 17 | if (fileExists(this.mappedPath)) { 18 | this.logger.writeError( 19 | `无法写入文件. 文件已经存在: ${this.mappedPath}` 20 | ); 21 | } else { 22 | const fileContent = this.renderTemplate(); 23 | this.logger.writeDebug(`文件内容: ${fileContent}`); 24 | 25 | //写文件 26 | outputFileSync(this.mappedPath, fileContent); 27 | this.logger.writeCreate(this.mappedPath); 28 | } 29 | return; 30 | } 31 | 32 | renderTemplate() { 33 | let rendered; 34 | this.logger.writeDebug(`渲染模板: ${this.originalPath}`); 35 | const template = fs.readFileSync(this.originalPath, 'utf8'); 36 | 37 | try { 38 | rendered = ejs.render(template, this.templateVariables); 39 | } catch (err) { 40 | this.logger.writeDebug('渲染失败'); 41 | err.message += ' (Error in blueprint template: ' + this.originalPath + ')'; 42 | this.logger.writeError(`error was: ${err.message}`); 43 | throw err; 44 | } 45 | return rendered; 46 | } 47 | 48 | isFile() { 49 | let fileCheck; 50 | try { 51 | fileCheck = fs.lstatSync(this.originalPath).isFile(); 52 | } catch (e) { 53 | if (e.code === 'ENOENT') { 54 | return false; 55 | } else { 56 | throw e; 57 | } 58 | } 59 | this.logger.writeDebug(`文件校验: ${this.originalPath} - ${fileCheck}`); 60 | return fileCheck; 61 | } 62 | } 63 | 64 | export default FileInfo; 65 | -------------------------------------------------------------------------------- /src/sub-commands/new.js: -------------------------------------------------------------------------------- 1 | import { 2 | which, 3 | rm, 4 | exec 5 | } from 'shelljs'; 6 | import SubCommand from '../models/sub-command'; 7 | import CreateAndStepIntoDirectory from '../tasks/create-and-step-into-directory'; 8 | import GitPull from '../tasks/git-pull'; 9 | import ProjectSettings from '../models/project-settings'; 10 | 11 | class New extends SubCommand { 12 | constructor() { 13 | super(); 14 | this.createDirTask = new CreateAndStepIntoDirectory(this.environment); 15 | this.gitPullTask = new GitPull(this.environment); 16 | } 17 | 18 | printUserHelp() { 19 | this.logger.write('命令用于创建一个新的vuex项目'); 20 | } 21 | 22 | run(cliArgs) { 23 | this.confirmGit(); 24 | this.createDirTask.run(cliArgs).then(() => { 25 | let fetch_url = 'https://github.com/sokis/vue-vuex-starter-kit.git'; 26 | 27 | this.gitPullTask.run(fetch_url).then(() => { 28 | this.createProjectSettings(); 29 | this.resetGitHistory(); 30 | }); 31 | }); 32 | } 33 | 34 | confirmGit() { 35 | if (!which('git')) { 36 | this.logger.writeError('运行该脚本需要你安装Git.'); 37 | this.logger.writeInfo('如果你有安装Homebrew,请试试运行: brew install git'); 38 | process.exit(1); 39 | } 40 | } 41 | 42 | // Should maybe prompt user for permission to do this since it's dangerous. 43 | resetGitHistory() { 44 | this.logger.writeInfo('移除 Staret Kit项目的 .git 文件夹'); 45 | rm('-rf', '.git'); 46 | exec('git init && git add -A && git commit -m"Initial commit"', { 47 | silent: true 48 | }); 49 | this.logger.writeCreate('创建新的 .git 到的项目中'); 50 | this.logger.writeInfo('恭喜!新的vuex项目已经创建成功。接下来你可以配置CLI并继续使用它'); 51 | } 52 | 53 | // 创建.vuexrc到新的vuex项目中。 54 | createProjectSettings() { 55 | this.logger.writeInfo('创建默认的.vuexrc文件到项目中'); 56 | const vuexStarterKitTemplate = '../../templates/.starterrc'; 57 | const settings = new ProjectSettings(vuexStarterKitTemplate); 58 | settings.save(); 59 | 60 | this.logger.writeCreate('保存.vuexrc设置.'); 61 | } 62 | } 63 | 64 | export default New; 65 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vuex-cli", 3 | "version": "2.0.1", 4 | "description": "快速搭建 vuex/vuejs 应用脚手架。", 5 | "main": "bin/vuex.js", 6 | "engine": { 7 | "node": ">=6.2.0" 8 | }, 9 | "scripts": { 10 | "test": "env NODE_PATH=$NODE_PATH:$PWD/src ./node_modules/.bin/babel-node ./node_modules/.bin/_mocha --compilers js:babel-register --opts ./.mocharc ", 11 | "start": "npm run build:watch", 12 | "build": "./node_modules/.bin/babel src -d lib", 13 | "build:watch": "./node_modules/.bin/babel src --watch -d lib", 14 | "lint": "./node_modules/.bin/eslint ./src ./test ./blueprints", 15 | "clean": "rimraf lib", 16 | "publish:patch": "npm test && npm run clean && npm run build && npm version patch && npm publish", 17 | "publish:minor": "npm test && npm run clean && npm run build && npm version minor && npm publish", 18 | "release": "bash build/release.sh" 19 | }, 20 | "bin": { 21 | "vuex": "bin/vuex.js", 22 | "vuex-init": "bin/vuex-init.js", 23 | "vuex-generate": "bin/vuex-generate.js", 24 | "vuex-g": "bin/vuex-g.js", 25 | "vuex-new": "bin/vuex-new.js", 26 | "vuex-server": "bin/vuex-server.js", 27 | "vuex-compile": "bin/vuex-compile.js" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/sokis/vuex-cli.git" 32 | }, 33 | "keywords": [ 34 | "vuex", 35 | "vue", 36 | "cli", 37 | "generator", 38 | "vue.js", 39 | "kit", 40 | "starter-kit", 41 | "vuex-starter-kit", 42 | "vue-vuex-starter-kit" 43 | ], 44 | "author": "sokis ", 45 | "license": "MIT", 46 | "bugs": { 47 | "url": "https://github.com/sokis/vuex-cli/issues" 48 | }, 49 | "homepage": "https://github.com/sokis/vuex-cli#readme", 50 | "dependencies": { 51 | "chalk": "^1.1.1", 52 | "commander": "^2.9.0", 53 | "denodeify": "^1.2.1", 54 | "ejs": "^2.4.1", 55 | "elegant-spinner": "^1.0.1", 56 | "figlet": "^1.1.1", 57 | "fs-extra": "^0.26.5", 58 | "humps": "^1.0.0", 59 | "jsonfile": "^2.2.3", 60 | "koa": "^1.2.1", 61 | "lodash": "^4.5.1", 62 | "log-update": "^1.0.2", 63 | "minimist": "^1.2.0", 64 | "prompt": "^1.0.0", 65 | "shelljs": "^0.6.0", 66 | "temp": "^0.8.3", 67 | "through": "^2.3.8", 68 | "vuex-cli-webpack": "^1.0.12", 69 | "walk-sync": "^0.2.6" 70 | }, 71 | "devDependencies": { 72 | "babel-cli": "^6.5.1", 73 | "babel-eslint": "^4.1.8", 74 | "babel-preset-es2015": "^6.6.0", 75 | "babel-register": "^6.5.1", 76 | "chai": "^3.5.0", 77 | "codecov": "^1.0.1", 78 | "eslint": "^1.10.3", 79 | "eslint-plugin-ejs": "0.0.2", 80 | "isparta": "^4.0.0", 81 | "istanbul": "^0.4.2", 82 | "mocha": "^2.4.5", 83 | "rimraf": "^2.5.2", 84 | "sinon": "^1.17.3" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/models/logger.js: -------------------------------------------------------------------------------- 1 | import { EOL } from 'os'; 2 | import chalk from 'chalk'; 3 | import elegantSpinner from 'elegant-spinner'; 4 | import logUpdate from 'log-update'; 5 | 6 | const frame = elegantSpinner(); 7 | const DEFAULT_WRITE_LEVEL = 'INFO'; 8 | const WRITE_LEVELS = { 9 | 'DEBUG': 1, 10 | 'INFO': 2, 11 | 'WARNING': 3, 12 | 'ERROR': 4 13 | }; 14 | 15 | class Logger { 16 | constructor(options = {}) { 17 | this.inputStream = options.inputStream || process.stdin; 18 | this.outputStream = options.outputStream || process.stdout; 19 | this.errorStream = options.errorStream || process.stderr; 20 | 21 | this.writeLevel = options.writeLevel || DEFAULT_WRITE_LEVEL; 22 | this.streaming = false; 23 | } 24 | 25 | write(data, writeLevel) { 26 | if (writeLevel === 'ERROR') { 27 | this.errorStream.write(data); 28 | } else if (this.writeLevelVisible(writeLevel)) { 29 | this.outputStream.write(data); 30 | } 31 | } 32 | 33 | writeLine(text, writeLevel) { 34 | this.write(text + EOL, writeLevel); 35 | } 36 | 37 | writeInfo(text) { 38 | const content = chalk.blue(' info: ') + chalk.white(text); 39 | this.writeLine(content, 'INFO'); 40 | } 41 | 42 | writeDebug(text) { 43 | const content = chalk.gray(' debug: ') + chalk.white(text); 44 | this.writeLine(content, 'DEBUG'); 45 | } 46 | 47 | writeError(text) { 48 | const content = chalk.red(' error: ') + chalk.white(text); 49 | this.writeLine(content, 'ERROR'); 50 | } 51 | 52 | writeWarning(text) { 53 | const content = chalk.yellow(' warning: ') + chalk.white(text); 54 | this.writeLine(content, 'WARNING'); 55 | } 56 | 57 | writeCreate(text) { 58 | const content = chalk.green(' create: ') + chalk.white(text); 59 | this.writeLine(content, 'INFO'); 60 | } 61 | 62 | writeLevelVisible(writeLevel = DEFAULT_WRITE_LEVEL) { 63 | return WRITE_LEVELS[writeLevel] >= WRITE_LEVELS[this.writeLevel]; 64 | } 65 | 66 | setWriteLevel(newLevel) { 67 | const allowedLevels = Object.keys(WRITE_LEVELS); 68 | if (allowedLevels.indexOf(newLevel) === -1) { 69 | throw new Error( 70 | `Unknown write level. Valid values are: ${allowedLevels.join(', ')}` 71 | ); 72 | } 73 | 74 | this.writeLevel = newLevel; 75 | } 76 | 77 | startProgress(string, customStream) { 78 | const stream = customStream || logUpdate.create(this.outputStream); 79 | if (this.writeLevelVisible(this.writeLevel)) { 80 | this.streaming = true; 81 | this.progressInterval = setInterval(() => { 82 | stream(` ${chalk.green('loading:')} ${string} ${chalk.cyan.bold.dim(frame())}`); 83 | }, 100); 84 | } 85 | } 86 | 87 | stopProgress() { 88 | if (this.progressInterval) { 89 | this.streaming = false; 90 | clearInterval(this.progressInterval); 91 | } 92 | } 93 | } 94 | 95 | export default Logger; 96 | -------------------------------------------------------------------------------- /src/models/blueprint.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import _ from 'lodash'; 3 | import walkSync from 'walk-sync'; 4 | import fs from 'fs'; 5 | 6 | import { fileExists } from '../util/fs'; 7 | import mixin from '../util/mixin'; 8 | import { normalizeCasing } from '../util/text-helper'; 9 | import FileInfo from './file-info'; 10 | import config from '../config'; 11 | 12 | const { basePath } = config; 13 | 14 | const generateLookupPaths = lookupPaths => { 15 | lookupPaths = lookupPaths || []; 16 | lookupPaths = lookupPaths.concat(Blueprint.defaultLookupPaths()); 17 | return _.uniq(lookupPaths); 18 | }; 19 | 20 | const dir = fullPath => fileExists(fullPath) ? 21 | fs.readdirSync(fullPath).map(fileName => path.join(fullPath, fileName)) 22 | :[]; 23 | 24 | export default class Blueprint { 25 | constructor(blueprintPath) { 26 | this.path = blueprintPath; 27 | this.name = path.basename(blueprintPath); 28 | } 29 | 30 | // HOOK: that can be overridden. Defaults to look in /files. 31 | filesPath() { 32 | return path.join(this.path, 'files'); 33 | } 34 | 35 | files() { 36 | if (this._files) { return this._files; } 37 | 38 | let filesPath = this.filesPath(); 39 | 40 | if (fileExists(filesPath)) { 41 | this._files = walkSync(filesPath); 42 | } else { 43 | this._files = []; 44 | } 45 | return this._files; 46 | } 47 | 48 | static defaultLookupPaths() { 49 | return [ 50 | path.resolve(path.join(basePath, 'blueprints')), 51 | path.resolve(__dirname, '..', '..', 'blueprints') 52 | ]; 53 | } 54 | 55 | static lookup(name, options = {}) { 56 | const lookupPaths = generateLookupPaths(options.paths); 57 | 58 | let lookupPath; 59 | let blueprintPath; 60 | 61 | for (let i = 0; (lookupPath = lookupPaths[i]); i++) { 62 | blueprintPath = path.resolve(lookupPath, name); 63 | 64 | if (fileExists(blueprintPath)) { 65 | return Blueprint.load(blueprintPath); 66 | } 67 | } 68 | 69 | if (!options.ignoreMissing) { 70 | throw new Error('Unknown blueprint: ' + name); 71 | } 72 | } 73 | 74 | // load in the blueprint that was found, extend this class to load it 75 | static load(blueprintPath) { 76 | let Constructor; 77 | const constructorPath = path.resolve(blueprintPath, 'index.js'); 78 | 79 | if (fs.lstatSync(blueprintPath).isDirectory()) { 80 | if (fileExists(constructorPath)) { 81 | const blueprintModule = require(constructorPath); 82 | Constructor = mixin(Blueprint, blueprintModule); 83 | 84 | return new Constructor(blueprintPath); 85 | } 86 | } 87 | } 88 | 89 | static list(options = {}) { 90 | return generateLookupPaths(options.paths).map(lookupPath => { 91 | const blueprintFiles = dir(lookupPath); 92 | const packagePath = path.join(lookupPath, '../package.json'); 93 | let source; 94 | 95 | if (fileExists(packagePath)) { 96 | source = require(packagePath).name; 97 | } else { 98 | source = path.basename(path.join(lookupPath, '..')); 99 | } 100 | 101 | const blueprints = blueprintFiles.map(filePath => { 102 | const blueprint = this.load(filePath); 103 | 104 | if (blueprint) { 105 | let description; 106 | const name = blueprint.name; 107 | 108 | if (blueprint.description) { 109 | description = blueprint.description(); 110 | } else { 111 | description = 'N/A'; 112 | } 113 | 114 | return { 115 | name, 116 | description 117 | }; 118 | } 119 | }); 120 | 121 | return { 122 | source, 123 | blueprints: _.compact(blueprints) 124 | }; 125 | }); 126 | } 127 | 128 | _fileMapTokens(options) { 129 | const standardTokens = { 130 | __name__: (options) => { 131 | const name = options.entity.name; 132 | const casing = options.settings.getSetting('fileCasing') || 'default'; 133 | return normalizeCasing(name, casing); 134 | }, 135 | __path__: (options) => { 136 | return options.originalBlueprintName; 137 | }, 138 | __root__: (options) => { 139 | return options.settings.getSetting('sourceBase'); 140 | }, 141 | __test__: (options) => { 142 | return options.settings.getSetting('testBase'); 143 | } 144 | }; 145 | 146 | // HOOK: calling into the blueprints fileMapTokens() hook, passing along 147 | // an options hash coming from _generateFileMapVariables() 148 | const customTokens = this.fileMapTokens(options) || {}; 149 | return Object.assign({}, standardTokens, customTokens); 150 | } 151 | 152 | generateFileMap(fileMapOptions) { 153 | const tokens = this._fileMapTokens(fileMapOptions); 154 | const fileMapValues = _.values(tokens); 155 | const tokenValues = fileMapValues.map(token => token(fileMapOptions)); 156 | const tokenKeys = _.keys(tokens); 157 | return _.zipObject(tokenKeys, tokenValues); 158 | } 159 | 160 | // Set up options to be passed to fileMapTokens that get generated. 161 | _generateFileMapVariables(entityName, locals, options) { 162 | const originalBlueprintName = options.originalBlueprintName || this.name; 163 | const { settings, entity } = options; 164 | 165 | return { 166 | originalBlueprintName, 167 | settings, 168 | entity, 169 | locals 170 | }; 171 | } 172 | 173 | // Given a file and a fileMap from locals, convert path names 174 | // to be correct string 175 | mapFile(file, locals) { 176 | let pattern, i; 177 | const fileMap = locals.fileMap || { __name__: locals.camelEntityName }; 178 | for (i in fileMap) { 179 | pattern = new RegExp(i, 'g'); 180 | file = file.replace(pattern, fileMap[i]); 181 | } 182 | return file; 183 | } 184 | 185 | _locals(options) { 186 | const entityName = options.entity && options.entity.name; 187 | const customLocals = this.locals(options); 188 | const fileMapVariables = this._generateFileMapVariables(entityName, customLocals, options); 189 | const fileMap = this.generateFileMap(fileMapVariables); 190 | 191 | const standardLocals = { 192 | pascalEntityName: normalizeCasing(entityName, 'pascal'), 193 | camelEntityName: normalizeCasing(entityName, 'camel'), 194 | snakeEntityName: normalizeCasing(entityName, 'snake'), 195 | dashesEntityName: normalizeCasing(entityName, 'dashes'), 196 | fileMap 197 | }; 198 | 199 | return Object.assign({}, standardLocals, customLocals); 200 | } 201 | 202 | _process(options, beforeHook, process, afterHook) { 203 | const locals = this._locals(options); 204 | return Promise.resolve() 205 | .then(beforeHook.bind(this, options, locals)) 206 | .then(process.bind(this, locals)) 207 | .then(afterHook.bind(this, options)); 208 | } 209 | 210 | processFiles(locals) { 211 | const files = this.files(); 212 | const fileInfos = files.map(file => this.buildFileInfo(locals, file)); 213 | this.logger.writeDebug(`built file infos: ${fileInfos.length}`); 214 | const filesToWrite = fileInfos.filter(info => info.isFile()); 215 | this.logger.writeDebug(`files to write: ${filesToWrite.length}`); 216 | filesToWrite.map(file => file.writeFile()); 217 | } 218 | 219 | buildFileInfo(locals, file) { 220 | const mappedPath = this.mapFile(file, locals); 221 | this.logger.writeDebug(`mapped path: ${mappedPath}`); 222 | 223 | return new FileInfo({ 224 | logger: this.logger, 225 | templateVariables: locals, 226 | originalPath: this.srcPath(file), 227 | mappedPath: this.destPath(mappedPath), 228 | outputPath: this.destPath(file) 229 | }); 230 | } 231 | 232 | // where the files will be written to 233 | destPath(mappedPath) { 234 | return path.resolve(basePath, mappedPath); 235 | } 236 | 237 | // location of the string templates 238 | srcPath(file) { 239 | return path.resolve(this.filesPath(), file); 240 | } 241 | 242 | /* 243 | * install options: 244 | 245 | const blueprintOptions = { 246 | originalBlueprintName: name, 247 | logger: this.logger, 248 | settings: this.settings, 249 | entity: { 250 | name: cliArgs.entity.name, 251 | options cliArgs.entity.options 252 | } 253 | }; 254 | */ 255 | install(options) { 256 | const logger = this.logger = options.logger; 257 | 258 | logger.writeInfo(`开始创建: ${options.originalBlueprintName} - ${options.entity.name}`); 259 | 260 | return this._process( 261 | options, 262 | this.beforeInstall, 263 | this.processFiles, 264 | this.afterInstall 265 | ).then(() => { 266 | logger.writeInfo('创建完成。'); 267 | }); 268 | } 269 | 270 | // uninstall() { 271 | // } 272 | 273 | // HOOKS: 274 | locals() { } 275 | fileMapTokens() { } 276 | beforeInstall() { } 277 | afterInstall() { } 278 | 279 | // TODO: add uninstall hooks once support for uninstall exists 280 | // beforeUninstall() {} 281 | // afterUninstall() {} 282 | 283 | // HOOK: for normalizing entity name that gets passed in as an arg 284 | // via the CLI 285 | // normalizeEntityName(options) { 286 | // return normalizeEntityName(name); 287 | // } 288 | } 289 | 290 | --------------------------------------------------------------------------------