├── .felint ├── .gitignore ├── hooks │ ├── commit-msg │ ├── pre-push │ └── pre-commit ├── .stylelintignore ├── rules │ ├── .stylelintrc.json │ ├── .eslintrc_es6.json │ ├── .eslintrc_node.json │ ├── .eslintrc_react.json │ ├── .eslintrc_vue.json │ └── .eslintrc_es5.json ├── update_git_hooks.sh ├── config.js └── README.md ├── .felintrc ├── .multi_hooks ├── all_hooks │ ├── felint │ │ ├── commit-msg │ │ ├── pre-commit │ │ └── README │ ├── demo │ │ └── prepare-commit-msg │ └── demo2 │ │ └── prepare-commit-msg └── collector │ ├── pre-push │ ├── commit-msg │ ├── post-merge │ ├── pre-commit │ ├── pre-rebase │ ├── prepare-commit-msg │ └── runner.sh ├── src ├── index.babel.js ├── fetchConfig.js ├── felintConfig.js ├── felintrc.js ├── utils │ ├── checkPackage.js │ ├── versionUtil.js │ └── fileUtil.js ├── dependence.js ├── index.js └── ruleFile.js ├── docs ├── assets │ ├── with-dot.png │ ├── without-dot.png │ └── felint-main.css └── index.html ├── lib ├── index.babel.js ├── updateHooks.js ├── fetchConfig.js ├── felintConfig.js ├── utils │ ├── stylelintCodeGenerator.js │ ├── checkPackage.js │ ├── versionUtil.js │ └── fileUtil.js ├── felintrc.js ├── dependence.js ├── index.js └── ruleFile.js ├── .gitignore ├── .stylelintignore ├── .babelrc ├── CHANGELOG.md ├── .eslintrc.json ├── package.json └── README.md /.felint/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .DS_Store? -------------------------------------------------------------------------------- /.felintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plan": "node" 3 | } -------------------------------------------------------------------------------- /.multi_hooks/all_hooks/felint/commit-msg: -------------------------------------------------------------------------------- 1 | ../../../.felint/hooks/commit-msg -------------------------------------------------------------------------------- /.multi_hooks/all_hooks/felint/pre-commit: -------------------------------------------------------------------------------- 1 | ../../../.felint/hooks/pre-commit -------------------------------------------------------------------------------- /src/index.babel.js: -------------------------------------------------------------------------------- 1 | require('babel-register'); 2 | require('./index.js'); 3 | -------------------------------------------------------------------------------- /docs/assets/with-dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youzan/felint/HEAD/docs/assets/with-dot.png -------------------------------------------------------------------------------- /lib/index.babel.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('babel-register'); 4 | require('./index.js'); -------------------------------------------------------------------------------- /docs/assets/without-dot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/youzan/felint/HEAD/docs/assets/without-dot.png -------------------------------------------------------------------------------- /.multi_hooks/all_hooks/demo/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # echo 'this is demo-prepare-commit-msg' -------------------------------------------------------------------------------- /.multi_hooks/all_hooks/demo2/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # echo 'this is demo2-prepare-commit-msg' -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | npm-debug.log 4 | .git_hooks 5 | .eslintrc 6 | .eslintignore 7 | .scss-lint.yml -------------------------------------------------------------------------------- /.multi_hooks/collector/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | HOOK_TYPE="pre-push" 4 | PROJECT=`pwd` 5 | . $PROJECT/.multi_hooks/collector/runner.sh $* -------------------------------------------------------------------------------- /.multi_hooks/collector/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | HOOK_TYPE="commit-msg" 4 | PROJECT=`pwd` 5 | . $PROJECT/.multi_hooks/collector/runner.sh $* -------------------------------------------------------------------------------- /.multi_hooks/collector/post-merge: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | HOOK_TYPE="post-merge" 4 | PROJECT=`pwd` 5 | . $PROJECT/.multi_hooks/collector/runner.sh $* -------------------------------------------------------------------------------- /.multi_hooks/collector/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | HOOK_TYPE="pre-commit" 4 | PROJECT=`pwd` 5 | . $PROJECT/.multi_hooks/collector/runner.sh $* -------------------------------------------------------------------------------- /.multi_hooks/collector/pre-rebase: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | HOOK_TYPE="pre-rebase" 4 | PROJECT=`pwd` 5 | . $PROJECT/.multi_hooks/collector/runner.sh $* -------------------------------------------------------------------------------- /.multi_hooks/collector/prepare-commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | HOOK_TYPE="prepare-commit-msg" 4 | PROJECT=`pwd` 5 | . $PROJECT/.multi_hooks/collector/runner.sh $* -------------------------------------------------------------------------------- /.felint/hooks/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | #curl -G -s -o /dev/null "www.interface.com/commit/msg?user=$(git config user.name)" --data-urlencode "comments=$(cat $1)" -------------------------------------------------------------------------------- /.stylelintignore: -------------------------------------------------------------------------------- 1 | **/build 2 | **/bower_components 3 | **/vendor 4 | **/node_modules 5 | **/dist 6 | **/local_css 7 | **/local_js 8 | **/local 9 | **/lib 10 | **/dev -------------------------------------------------------------------------------- /.felint/.stylelintignore: -------------------------------------------------------------------------------- 1 | **/build 2 | **/bower_components 3 | **/vendor 4 | **/node_modules 5 | **/dist 6 | **/local_css 7 | **/local_js 8 | **/local 9 | **/lib 10 | **/dev -------------------------------------------------------------------------------- /.multi_hooks/all_hooks/felint/README: -------------------------------------------------------------------------------- 1 | 当前目录里的 commit-msg 和 pre-commit 实际上被软链到了 /Users/lianchengjie/works/youzanGit/felint/.felint 下真正的脚本文件,相应的钩子触发的时候会被执行。 2 | 3 | 只是勾子通过multihook来出发而已,后续的 felint 钩子、配置的更新都没有差别 4 | -------------------------------------------------------------------------------- /.felint/hooks/pre-push: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PUSH_DEFAULT=$(git config push.default) 4 | 5 | if [[ !($PUSH_DEFAULT == 'simple' || $PUSH_DEFAULT == 'current') ]]; then 6 | git config --replace-all push.default simple 7 | printf '\n\033[32m Massage:自动设置git config push.default => simple \033[0m\n' 8 | fi 9 | -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | "env", 5 | { 6 | "targets": { 7 | "node": "6.10" 8 | } 9 | } 10 | ] 11 | ], 12 | "plugins": [ 13 | "transform-decorators-legacy", 14 | "transform-object-rest-spread" 15 | ] 16 | } -------------------------------------------------------------------------------- /.felint/rules/.stylelintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "stylelint-config-standard", 3 | "processors": "stylelint-processor-html", 4 | "rules": { 5 | "selector-pseudo-element-colon-notation": null, 6 | "number-leading-zero": "never", 7 | "indentation": [2, {"severity": "warning"}] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.multi_hooks/collector/runner.sh: -------------------------------------------------------------------------------- 1 | HOOKS=$PROJECT/.multi_hooks/all_hooks/*/$HOOK_TYPE 2 | HOOKS_NUMBER=`ls -s $HOOKS 2>/dev/null | wc -l` 3 | 4 | if [ $(($HOOKS_NUMBER+0)) -gt 0 ];then 5 | 6 | for hook in $HOOKS 7 | do 8 | $hook $* 9 | exit_code=$? 10 | if [ $exit_code -ne 0 ];then 11 | exit 1 12 | fi 13 | done 14 | 15 | fi -------------------------------------------------------------------------------- /lib/updateHooks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let sh = require('shelljs'); 4 | 5 | let felintConfig = require('./felintConfig.js'); 6 | 7 | function update() { 8 | let initHooksFile = felintConfig.read().initHooks; 9 | if (initHooksFile) { 10 | sh.exec(`sh ./.felint/${initHooksFile}`); 11 | } 12 | } 13 | 14 | module.exports = { 15 | update 16 | }; -------------------------------------------------------------------------------- /src/fetchConfig.js: -------------------------------------------------------------------------------- 1 | const sh = require('shelljs'); 2 | const versionUtil = require('./utils/versionUtil.js'); 3 | const DEFAUTL_CONFIG_URL = 'https://github.com/youzan/felint-config.git'; 4 | 5 | // 拉取配置 6 | function fetchConfig(felintrc) { 7 | const configRepositoryUrl = felintrc.configRep || felintrc.gitHookUrl || DEFAUTL_CONFIG_URL; 8 | console.log(`felint将拉取位于${configRepositoryUrl}的配置\n`.green); 9 | const shellStr = `rm -rf ./.felint && git clone -b ${versionUtil.isBetaNow ? 'dev' : 'master'} ${configRepositoryUrl} .felint && cd .felint && rm -rf ./.git`; 10 | sh.exec(shellStr, {silent: true}); 11 | } 12 | 13 | 14 | module.exports = fetchConfig; 15 | -------------------------------------------------------------------------------- /lib/fetchConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sh = require('shelljs'); 4 | const versionUtil = require('./utils/versionUtil.js'); 5 | const DEFAUTL_CONFIG_URL = 'https://github.com/youzan/felint-config.git'; 6 | 7 | // 拉取配置 8 | function fetchConfig(felintrc) { 9 | const configRepositoryUrl = felintrc.configRep || felintrc.gitHookUrl || DEFAUTL_CONFIG_URL; 10 | console.log(`felint将拉取位于${configRepositoryUrl}的配置\n`.green); 11 | const shellStr = `rm -rf ./.felint && git clone -b ${versionUtil.isBetaNow ? 'dev' : 'master'} ${configRepositoryUrl} .felint && cd .felint && rm -rf ./.git`; 12 | sh.exec(shellStr, { silent: true }); 13 | } 14 | 15 | module.exports = fetchConfig; -------------------------------------------------------------------------------- /src/felintConfig.js: -------------------------------------------------------------------------------- 1 | const fileUtil = require('./utils/fileUtil.js'); 2 | 3 | /** 4 | * 读取felint配置文件内容 5 | */ 6 | function readFelintConfig() { 7 | const felintDir = felintDirPath(); 8 | let felintConfig = {}; 9 | 10 | if (felintDir && felintDir.path) { 11 | try { 12 | felintConfig = require(`${felintDir.path}/config.js`); 13 | } catch (e) { 14 | console.log('无法找到.felint/config.js,你需要先初始化'); 15 | } 16 | } 17 | 18 | return felintConfig; 19 | } 20 | 21 | /** 22 | * 获取`.felint`路径 23 | */ 24 | function felintDirPath() { 25 | return fileUtil.findUp(process.cwd(), '.felint', 'isDirectory'); 26 | } 27 | 28 | module.exports = { 29 | readFelintConfig, 30 | felintDirPath 31 | }; 32 | -------------------------------------------------------------------------------- /.felint/update_git_hooks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | projectPath=`pwd` 4 | RED='\033[0;31m' 5 | NC='\033[0m' 6 | LGREEN='\033[1;32m' 7 | my_dir="$(dirname "$0")" 8 | 9 | # cd to hooks folder 10 | cd ./.felint/hooks 11 | 12 | printf '\n========== init hook ==========\n' 13 | hooks="${projectPath}/.git/hooks/" 14 | rm -f "${hooks}/pre-commit" "${hooks}/pre-push" "${hooks}/commit-msg" 15 | ln -s ../../.felint/hooks/pre-commit "$hooks" 16 | ln -s ../../.felint/hooks/pre-push "$hooks" 17 | ln -s ../../.felint/hooks/pre-commit "$hooks" 18 | printf '\n========== chmod hook ==========\n' 19 | chmod -R a+x $hooks 20 | printf '\n========== chmod hook done ==========\n' 21 | 22 | printf '\n========== init hook done ==========\n' 23 | 24 | printf '\n========== ALL DONE, THANKS\n' 25 | -------------------------------------------------------------------------------- /lib/felintConfig.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fileUtil = require('./utils/fileUtil.js'); 4 | 5 | /** 6 | * 读取felint配置文件内容 7 | */ 8 | function readFelintConfig() { 9 | const felintDir = felintDirPath(); 10 | let felintConfig = {}; 11 | 12 | if (felintDir && felintDir.path) { 13 | try { 14 | felintConfig = require(`${felintDir.path}/config.js`); 15 | } catch (e) { 16 | console.log('无法找到.felint/config.js,你需要先初始化'); 17 | } 18 | } 19 | 20 | return felintConfig; 21 | } 22 | 23 | /** 24 | * 获取`.felint`路径 25 | */ 26 | function felintDirPath() { 27 | return fileUtil.findUp(process.cwd(), '.felint', 'isDirectory'); 28 | } 29 | 30 | module.exports = { 31 | readFelintConfig, 32 | felintDirPath 33 | }; -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.5 2 | 3 | 修改: 4 | 5 | 1. 修复felint init命令不带参数导致奔溃的问题 6 | 7 | ## 1.0.4 8 | 9 | 修改: 10 | 11 | 1. 重新开放update命令已兼容使用场景 12 | 13 | ## 1.0.3 14 | 15 | 修改: 16 | 17 | 1. 就该依赖安装方式为安装到项目本地 18 | 2. 去掉export命令 19 | 20 | ## 1.0.0 21 | 修改: 22 | 23 | 1. felint-config重新设计 24 | 2. 删除felint checkDependence、felint checkrc命令 25 | 3. 增加felint export命令 26 | 4. 对felint init、felint use命令的option进行修改 27 | 5. 去掉对全局eslint/scss_lint的依赖 28 | 6. 使用stylelint替换scss_lint进行css校验 29 | 30 | ## 0.3.0 31 | 修改: 32 | 33 | 1. 在felint init命令执行过程中加入对felint依赖的全局npm包的版本检测(e.g. felint依赖eslint,当全局没有安装eslint时或安装的eslint版本不符合要求的时候,将会提示) 34 | 2. felint init 和felint use命令从原来的强制覆盖原有eslintrc、scss_lint.yml文件改为在覆盖前加入是否覆盖原有文件的交互提示 35 | 3. 新增独立felint checkDependence命令,用于诊断felint依赖的全局npm包有无问题,效果同1(强烈推荐大家更新后执行看下自己的依赖是否正确) 36 | 4. 新增felint youzan命令,用于代替原有--youzan参数,--youzan参数已废弃。该命令用于生成基于有赞的felintrc文件,该文件可为后续init/update等命令提供 `felint config` 的仓库地址(e.g. felint init -6 --youzan === felint youzan && felint init -6) -------------------------------------------------------------------------------- /lib/utils/stylelintCodeGenerator.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | let codePattern = ` 7 | var child = require('child_process'); 8 | var islocal = false; 9 | var felintPath = ''; 10 | try { 11 | islocal = child.execSync('felint islocal', { 12 | encoding: 'utf-8' 13 | }).trim() === 'true'; 14 | if (!islocal) { 15 | felintPath = child.execSync('felint where', { 16 | encoding: 'utf-8' 17 | }).trim() + '/node_modules/'; 18 | } 19 | } catch(e) { 20 | } 21 | var ap = islocal ? '' : felintPath; 22 | module.exports = <%content%> 23 | `; 24 | 25 | let localCodePattern = 'module.exports = <%content%>'; 26 | 27 | exports.default = (content, islocal) => { 28 | return islocal ? localCodePattern.replace(/<%content%>/g, content) : codePattern.replace(/<%content%>/g, content).replace(/"<%path%>/g, 'ap+"'); 29 | }; -------------------------------------------------------------------------------- /.felint/config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dependence: { 3 | npm: { 4 | "eslint": "3.19.0", 5 | "babel-eslint": "7.2.1", 6 | "eslint-config-airbnb": "14.1.0", 7 | "eslint-config-vue": "2.0.2", 8 | "eslint-plugin-import": "2.2.0", 9 | "eslint-plugin-jsx-a11y": "4.0.0", 10 | "eslint-plugin-lean-imports": "0.3.3", 11 | "eslint-plugin-react": "6.10.3", 12 | "eslint-plugin-vue": "2.0.1", 13 | "stylelint": "7.12.0", 14 | "stylelint-config-standard": "16.0.0", 15 | "stylelint-processor-html": "1.0.0" 16 | } 17 | }, 18 | plan: { 19 | es6: ['.eslintrc_es6.json', '.stylelintrc.json'], 20 | es5: ['.eslintrc_es5.json', '.stylelintrc.json'], 21 | node: ['.eslintrc_node.json'], 22 | vue: ['.eslintrc_vue.json', '.stylelintrc.json'], 23 | react: ['.eslintrc_react.json', '.stylelintrc.json'], 24 | default: ['.eslintrc_es6.json', '.stylelintrc.json'] 25 | }, 26 | initHooks: 'update_git_hooks.sh' 27 | } -------------------------------------------------------------------------------- /src/felintrc.js: -------------------------------------------------------------------------------- 1 | const fileUtil = require('./utils/fileUtil.js'); 2 | 3 | function read() { 4 | return fileUtil.treeReadFile('.felintrc', 'json') || {}; 5 | } 6 | 7 | async function create(content, pathStr, force) { 8 | pathStr = pathStr || `${process.cwd()}/.felintrc`; 9 | 10 | if (force) { 11 | fileUtil.createFileSync(pathStr, content, 'json'); 12 | } else { 13 | const override = await fileUtil.checkOverride(pathStr); 14 | 15 | if (override) { 16 | fileUtil.createFileSync(pathStr, content, 'json'); 17 | } 18 | } 19 | } 20 | 21 | async function local() { 22 | await set({ 23 | local: true 24 | }); 25 | } 26 | 27 | function isLocal() { 28 | const felintrcFile = read() || {}; 29 | return !!felintrcFile.local; 30 | } 31 | 32 | async function set(value) { 33 | let felintrcFile = read(); 34 | felintrcFile = felintrcFile || {}; 35 | Object.assign(felintrcFile, value); 36 | await create(felintrcFile, null, true); 37 | } 38 | 39 | function getPlan() { 40 | return read().plan; 41 | } 42 | 43 | module.exports = { 44 | read, 45 | create, 46 | local, 47 | set, 48 | isLocal, 49 | getPlan 50 | }; 51 | -------------------------------------------------------------------------------- /.felint/rules/.eslintrc_es6.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "env": { 5 | "commonjs": true, 6 | "browser": true, 7 | "es6": true 8 | }, 9 | "extends": ["eslint:recommended"], 10 | "rules": { 11 | "one-var": 0, 12 | "no-var": 1, 13 | "semi": [1, "always", { "omitLastInOneLineBlock": true }], 14 | "max-len": 0, 15 | "comma-dangle": 0, 16 | "func-names": 0, 17 | "prefer-const": 0, 18 | "arrow-body-style": 0, 19 | "no-param-reassign": 0, 20 | "no-return-assign": 0, 21 | "no-underscore-dangle": [1, { "allowAfterThis": true }], 22 | "consistent-return": 0, 23 | "no-unused-expressions": 0, 24 | "no-multiple-empty-lines": [1, { "max": 2 }], 25 | "prefer-template": 1, 26 | "camelcase": [1, { "properties": "never" }], 27 | "indent": [1, 2, { "SwitchCase": 1 }], 28 | "chonsistent-this": 0, 29 | "keyword-spacing": [1, { "before": true, "after": true }], 30 | "space-in-parens": [1, "never"], 31 | "space-infix-ops": [1, { "int32Hint": false }], 32 | "space-before-blocks": [1, "always"], 33 | "key-spacing": [1, { "beforeColon": false, "afterColon": true }], 34 | "eqeqeq": 1 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "env": { 5 | "commonjs": true, 6 | "es6": true, 7 | "node": true 8 | }, 9 | "extends": ["eslint:recommended"], 10 | "rules": { 11 | "one-var": 0, 12 | "semi": [1, "always", { "omitLastInOneLineBlock": true }], 13 | "max-len": 0, 14 | "comma-dangle": 0, 15 | "func-names": 0, 16 | "prefer-const": 0, 17 | "arrow-body-style": 0, 18 | "no-param-reassign": 0, 19 | "no-return-assign": 0, 20 | "indent": [1, 4, { "SwitchCase": 1}], 21 | "no-console": 0, 22 | "no-underscore-dangle": [1, { "allowAfterThis": true }], 23 | "consistent-return": 0, 24 | "no-unused-expressions": 0, 25 | "no-multiple-empty-lines": [1, { "max": 2 }], 26 | "prefer-template": 1, 27 | "camelcase": [1, { "properties": "never" }], 28 | "chonsistent-this": 0, 29 | "keyword-spacing": [1, { "before": true, "after": true }], 30 | "space-in-parens": [1, "never"], 31 | "space-infix-ops": [1, { "int32Hint": false }], 32 | "space-before-blocks": [1, "always"], 33 | "key-spacing": [1, { "beforeColon": false, "afterColon": true }], 34 | "eqeqeq": 1, 35 | "no-return-await": 1 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.felint/rules/.eslintrc_node.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "babel-eslint", 4 | "env": { 5 | "commonjs": true, 6 | "es6": true, 7 | "node": true 8 | }, 9 | "extends": ["eslint:recommended"], 10 | "rules": { 11 | "one-var": 0, 12 | "semi": [1, "always", { "omitLastInOneLineBlock": true }], 13 | "max-len": 0, 14 | "comma-dangle": 0, 15 | "func-names": 0, 16 | "prefer-const": 0, 17 | "arrow-body-style": 0, 18 | "no-param-reassign": 0, 19 | "no-return-assign": 0, 20 | "indent": [1, 4, { "SwitchCase": 1}], 21 | "no-console": 0, 22 | "no-underscore-dangle": [1, { "allowAfterThis": true }], 23 | "consistent-return": 0, 24 | "no-unused-expressions": 0, 25 | "no-multiple-empty-lines": [1, { "max": 2 }], 26 | "prefer-template": 1, 27 | "camelcase": [1, { "properties": "never" }], 28 | "chonsistent-this": 0, 29 | "keyword-spacing": [1, { "before": true, "after": true }], 30 | "space-in-parens": [1, "never"], 31 | "space-infix-ops": [1, { "int32Hint": false }], 32 | "space-before-blocks": [1, "always"], 33 | "key-spacing": [1, { "beforeColon": false, "afterColon": true }], 34 | "eqeqeq": 1, 35 | "no-return-await": 1 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.felint/README.md: -------------------------------------------------------------------------------- 1 | Felint Config 2 | ======== 3 | 4 | ## 说明 5 | 6 | `felint-config`必须提供一个`config.js`文件作为`Felint`的功能配置。 7 | 8 | 以下为一个`config.js`的例子: 9 | 10 | ```javascript 11 | module.exports = { 12 | dependence: { 13 | npm: { 14 | "eslint": "3.19.0", 15 | "babel-eslint": "7.2.1", 16 | "eslint-config-airbnb": "14.1.0", 17 | "stylelint": "7.10.1", 18 | "stylelint-config-standard": "16.0.0" 19 | } 20 | }, 21 | plan: { 22 | es6: ['.eslintrc_es6', '.stylelintrc'], 23 | es5: ['.eslintrc_es5', '.stylelintrc'], 24 | default: ['.eslintrc_es6', '.stylelintrc'] 25 | }, 26 | initHooks: 'update_git_hooks.sh' 27 | } 28 | ``` 29 | 30 | 其中`dependence`指定了`felint`的依赖包。 31 | 32 | `plan`字段指定了`Felint`可用的代码规范方案。 33 | 34 | **e.g.** 35 | 36 | > es6: ['.eslintrc_es6.json', '.stylelintrc.json'] 37 | > > 该方案名为es6,使用`felint-config 的 rules目录`下的 .eslintrc_es6.json 和 .stylelintrc.json规范文件。 38 | 39 | `initHooks`指定了初始化钩子的脚本,将在执行felint init的时候被调用。 40 | 41 | ## 配置 42 | 43 | ### 配置规则文件 44 | 45 | 请在`rules`目录下加入需要的配置规则文件。 46 | 47 | 命名方式为`.eslintrc_type.json` `.stylelintrc_type.json`。 48 | 49 | ### 配置git钩子 50 | 51 | 增/删/修改hooks目录下的shell文件,然后修改`update_git_hooks`逻辑应用你自己的修改。 52 | 53 | ### 配置依赖 54 | 55 | 修改config.js文件内的dependence.npm内容 56 | 57 | ### 配置规则方案 58 | 59 | 修改config.js文件内plan字段,key为方案名,一个方案可以对应不同的eslintrc和stylelintrc文件组合 60 | -------------------------------------------------------------------------------- /src/utils/checkPackage.js: -------------------------------------------------------------------------------- 1 | const walk = require('walkdir'); 2 | let nodeModules; 3 | 4 | function getAllNodeModules(pathStr) { 5 | let modules = []; 6 | try { 7 | modules = walk.sync(pathStr, { 8 | no_recurse: true 9 | }); 10 | } catch (e) { 11 | modules = []; 12 | } 13 | 14 | modules.forEach((moduleName) => { 15 | // 私包 16 | if (moduleName[0] === '@') { 17 | modules = modules.concat(getAllNodeModules(`${pathStr}/${moduleName}`)); 18 | } 19 | }); 20 | 21 | return modules; 22 | } 23 | 24 | function check(name, version) { 25 | const fixName = `\/${name}$`; 26 | const nameReg = new RegExp(fixName); 27 | let result = false; 28 | 29 | if (!nodeModules) { 30 | nodeModules = getAllNodeModules(`${process.cwd()}/node_modules`); 31 | } 32 | 33 | nodeModules.some(n => { 34 | if (nameReg.test(n)) { 35 | const packageJson = require(`${n}/package.json`); 36 | 37 | if (packageJson.version === version) { 38 | result = true; 39 | } else { 40 | result = { 41 | current: packageJson.version, 42 | require: version 43 | }; 44 | } 45 | 46 | return true; 47 | } else { 48 | return false; 49 | } 50 | }); 51 | 52 | return result; 53 | } 54 | 55 | module.exports = check; 56 | -------------------------------------------------------------------------------- /lib/utils/checkPackage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const walk = require('walkdir'); 4 | let nodeModules; 5 | 6 | function getAllNodeModules(pathStr) { 7 | let modules = []; 8 | try { 9 | modules = walk.sync(pathStr, { 10 | no_recurse: true 11 | }); 12 | } catch (e) { 13 | modules = []; 14 | } 15 | 16 | modules.forEach(moduleName => { 17 | // 私包 18 | if (moduleName[0] === '@') { 19 | modules = modules.concat(getAllNodeModules(`${pathStr}/${moduleName}`)); 20 | } 21 | }); 22 | 23 | return modules; 24 | } 25 | 26 | function check(name, version) { 27 | const fixName = `\/${name}$`; 28 | const nameReg = new RegExp(fixName); 29 | let result = false; 30 | 31 | if (!nodeModules) { 32 | nodeModules = getAllNodeModules(`${process.cwd()}/node_modules`); 33 | } 34 | 35 | nodeModules.some(n => { 36 | if (nameReg.test(n)) { 37 | const packageJson = require(`${n}/package.json`); 38 | 39 | if (packageJson.version === version) { 40 | result = true; 41 | } else { 42 | result = { 43 | current: packageJson.version, 44 | require: version 45 | }; 46 | } 47 | 48 | return true; 49 | } else { 50 | return false; 51 | } 52 | }); 53 | 54 | return result; 55 | } 56 | 57 | module.exports = check; -------------------------------------------------------------------------------- /.felint/hooks/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # curl -s -o /dev/null "www.qima-inc.com/commit/create?user=$(git config user.name)&comments=pre-commit" 3 | 4 | JS_STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E "(\.vue|\.jsx?)$") 5 | SCSS_STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACM | grep -E "(\.vue|\.p?css)$") 6 | 7 | if [[ "$JS_STAGED_FILES" = "" && "$SCSS_STAGED_FILES" = "" ]]; then 8 | exit 0 9 | fi 10 | 11 | PASS=true 12 | 13 | # Check for felint 14 | which felint &> /dev/null 15 | if [[ "$?" == 1 ]]; then 16 | if [ -f ~/.zshrc ]; then 17 | source ~/.zshrc 18 | elif [ -f ~/.bash_profile ]; then 19 | source ~/.bash_profile 20 | fi 21 | fi 22 | 23 | if [[ "$JS_STAGED_FILES" != "" ]]; then 24 | echo "\nValidating Javascript:\n" 25 | 26 | felint lintjs --exitcode $JS_STAGED_FILES 27 | 28 | if [[ "$?" == 0 ]]; then 29 | echo "\033[32mESLint Passed\033[0m" 30 | else 31 | echo "\033[41mESLint Failed\033[0m" 32 | PASS=false 33 | fi 34 | 35 | echo "\nJavascript validation completed!\n" 36 | fi 37 | 38 | 39 | if [[ "$SCSS_STAGED_FILES" != "" ]]; then 40 | echo "\nValidating css:\n" 41 | 42 | felint lintcss --exitcode $SCSS_STAGED_FILES 43 | 44 | if [[ "$?" == 0 ]]; then 45 | echo "\033[32mstylelint Passed\033[0m" 46 | else 47 | echo "\033[41mstylelint Failed\033[0m" 48 | PASS=false 49 | fi 50 | 51 | echo "\ncss validation completed!\n" 52 | fi 53 | 54 | if ! $PASS; then 55 | echo "\033[41mCOMMIT FAILED:\033[0m Your commit contains files that should pass ESLint and stylelint but do not. Please fix the ESLint and stylelint errors and try again.\n" 56 | exit 1 57 | else 58 | echo "\033[42mCOMMIT SUCCEEDED\033[0m\n" 59 | curl -s -o /dev/null "www.qima-inc.com/commit/create?user=$(git config user.name)&comments=eslint" & 60 | fi 61 | 62 | exit $? 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "felint", 3 | "version": "1.1.7", 4 | "description": "linter for fe", 5 | "keywords": [ 6 | "eslint", 7 | "stylelint", 8 | "felint" 9 | ], 10 | "author": "lianchengjie ", 11 | "license": "MIT", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/youzan/felint.git" 15 | }, 16 | "main": "lib/index.js", 17 | "files": [ 18 | "lib" 19 | ], 20 | "bin": "lib/index.js", 21 | "dependencies": { 22 | "colors": "1.1.2", 23 | "commander": "2.9.0", 24 | "js-yaml": "3.6.1", 25 | "shelljs": "0.7.7", 26 | "walkdir": "0.0.11" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/youzan/felint/issues" 30 | }, 31 | "homepage": "https://github.com/youzan/felint#readme", 32 | "devDependencies": { 33 | "babel-cli": "^6.24.1", 34 | "babel-core": "^6.24.1", 35 | "babel-eslint": "^7.2.1", 36 | "babel-plugin-transform-decorators-legacy": "^1.3.4", 37 | "babel-plugin-transform-object-rest-spread": "^6.23.0", 38 | "babel-plugin-transform-runtime": "^6.23.0", 39 | "babel-preset-env": "^1.4.0", 40 | "babel-register": "^6.24.1", 41 | "eslint": "^3.19.0", 42 | "eslint-config-airbnb": "^14.1.0", 43 | "eslint-config-vue": "^2.0.2", 44 | "eslint-plugin-import": "^2.2.0", 45 | "eslint-plugin-jsx-a11y": "^4.0.0", 46 | "eslint-plugin-lean-imports": "^0.3.3", 47 | "eslint-plugin-react": "^6.10.3", 48 | "eslint-plugin-vue": "^2.0.1", 49 | "stylelint": "^7.12.0", 50 | "stylelint-config-standard": "16.0.0", 51 | "stylelint-processor-html": "^1.0.0" 52 | }, 53 | "scripts": { 54 | "test": "echo \"Error: no test specified\" && exit 1", 55 | "build": "babel src/ --out-dir lib/", 56 | "prepublish": "babel src/ --out-dir lib/", 57 | "postinstall": "echo \"\\033[32m请前往https://github.com/youzan/felint/blob/master/README.md查看更新内容! \\033[0m\"" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /lib/felintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | let create = (() => { 4 | var _ref = _asyncToGenerator(function* (content, pathStr, force) { 5 | pathStr = pathStr || `${process.cwd()}/.felintrc`; 6 | 7 | if (force) { 8 | fileUtil.createFileSync(pathStr, content, 'json'); 9 | } else { 10 | const override = yield fileUtil.checkOverride(pathStr); 11 | 12 | if (override) { 13 | fileUtil.createFileSync(pathStr, content, 'json'); 14 | } 15 | } 16 | }); 17 | 18 | return function create(_x, _x2, _x3) { 19 | return _ref.apply(this, arguments); 20 | }; 21 | })(); 22 | 23 | let local = (() => { 24 | var _ref2 = _asyncToGenerator(function* () { 25 | yield set({ 26 | local: true 27 | }); 28 | }); 29 | 30 | return function local() { 31 | return _ref2.apply(this, arguments); 32 | }; 33 | })(); 34 | 35 | let set = (() => { 36 | var _ref3 = _asyncToGenerator(function* (value) { 37 | let felintrcFile = read(); 38 | felintrcFile = felintrcFile || {}; 39 | Object.assign(felintrcFile, value); 40 | yield create(felintrcFile, null, true); 41 | }); 42 | 43 | return function set(_x4) { 44 | return _ref3.apply(this, arguments); 45 | }; 46 | })(); 47 | 48 | function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } 49 | 50 | const fileUtil = require('./utils/fileUtil.js'); 51 | 52 | function read() { 53 | return fileUtil.treeReadFile('.felintrc', 'json') || {}; 54 | } 55 | 56 | function isLocal() { 57 | const felintrcFile = read() || {}; 58 | return !!felintrcFile.local; 59 | } 60 | 61 | function getPlan() { 62 | return read().plan; 63 | } 64 | 65 | module.exports = { 66 | read, 67 | create, 68 | local, 69 | set, 70 | isLocal, 71 | getPlan 72 | }; -------------------------------------------------------------------------------- /.felint/rules/.eslintrc_react.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": [ 4 | "airbnb" 5 | ], 6 | "parser": "babel-eslint", 7 | "plugins": [ 8 | "lean-imports" 9 | ], 10 | "env": { 11 | "browser": true, 12 | "commonjs": true 13 | }, 14 | "rules": { 15 | "no-lonely-if": 1, 16 | "no-void": 0, 17 | "jsx-a11y/anchor-has-content": 0, 18 | "react/self-closing-comp": 1, 19 | "jsx-a11y/tabindex-no-positive": 1, 20 | "import/first": 1, 21 | "one-var": 0, 22 | "no-var": 2, 23 | "max-len": 0, 24 | "no-tabs": 0, 25 | "comma-dangle": 0, 26 | "func-names": 0, 27 | "prefer-const": 0, 28 | "arrow-body-style": 0, 29 | "operator-assignment": 0, 30 | "react/prefer-stateless-function": 0, 31 | "react/sort-comp": 0, 32 | "react/no-multi-comp": 0, 33 | "react/prop-types": 0, 34 | "react/prefer-es6-class": 0, 35 | "react/jsx-closing-bracket-location": 0, 36 | "react/jsx-no-bind": 0, 37 | "react/require-default-props": 0, 38 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 39 | "react/no-unused-prop-types": 0, 40 | "react/no-find-dom-node": 0, 41 | "react/no-array-index-key": 0, 42 | "react/forbid-prop-types": 0, 43 | "react/no-render-return-value": 0, 44 | "react/no-string-refs": 0, 45 | "no-param-reassign": 0, 46 | "no-return-assign": 0, 47 | "no-underscore-dangle": [ 48 | 1, 49 | { 50 | "allowAfterThis": true 51 | } 52 | ], 53 | "consistent-return": 0, 54 | "no-unused-expressions": 0, 55 | "prefer-template": 1, 56 | "no-plusplus": 0, 57 | "no-multi-assign": 0, 58 | "no-useless-escape": 0, 59 | "no-use-before-define": [2, { "functions": false, "classes": false, "variables": true }], 60 | "no-prototype-builtins": 0, 61 | "camelcase": [ 62 | 1, 63 | { 64 | "properties": "never" 65 | } 66 | ], 67 | "consistent-this": 0, 68 | "class-methods-use-this": 0, 69 | "lean-imports/import": [ 70 | 2, [ 71 | "lodash" 72 | ] 73 | ], 74 | "eqeqeq": 1, 75 | "import/no-unresolved": 0, 76 | "import/no-extraneous-dependencies": 0, 77 | "import/extensions": 0, 78 | "import/prefer-default-export": 0, 79 | "jsx-a11y/no-static-element-interactions": 0, 80 | "jsx-a11y/label-has-for": 0, 81 | "prefer-spread": 1 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/dependence.js: -------------------------------------------------------------------------------- 1 | const sh = require('shelljs'); 2 | const felintConfig = require('./felintConfig.js'); 3 | const checkPackage = require('./utils/checkPackage.js'); 4 | const felintrc = require('./felintrc.js'); 5 | 6 | /** 7 | * 安装npm包 8 | * @param {String} packageName 包名 9 | * @param {String} version 版本号 10 | */ 11 | function installPackage(packageName, version) { 12 | const felintrcFile = felintrc.read(); 13 | const { registryUrl, disturl, useYarn = true } = felintrcFile; 14 | const result = checkPackage(packageName, version); 15 | let installCommand = useYarn ? `yarn add ${packageName}@${version} --dev` : `npm i -D ${packageName}@${version}`; 16 | 17 | if (registryUrl) { 18 | installCommand += ` --registry=${registryUrl}`; 19 | } 20 | 21 | if (disturl) { 22 | installCommand += ` --disturl=${disturl}`; 23 | } 24 | 25 | if (!result) { 26 | try { 27 | console.log(`开始安装${packageName}@${version}`.green); 28 | sh.exec(installCommand); 29 | } catch (e) { 30 | console.log(`${packageName}@${version}安装失败,请检查`.red); 31 | } 32 | } else { 33 | return result; 34 | } 35 | } 36 | 37 | // 安装单个依赖 38 | function installSinglePackage(typeInfo = {}) { 39 | return new Promise(res => { 40 | const npmDepList = Object.keys(typeInfo); 41 | const msgInfo = []; 42 | 43 | npmDepList.forEach(packageName => { 44 | let result = installPackage(packageName, typeInfo[packageName]); 45 | if (result && result.current) { 46 | msgInfo.push(`你已安装${`${packageName}@${result.current}`.red},默认版本为${typeInfo[packageName].green},${'请确认!'.red}`); 47 | } 48 | }); 49 | 50 | res(msgInfo); 51 | }); 52 | } 53 | 54 | /** 55 | * 安装配置指定的依赖 56 | */ 57 | async function install(plan) { 58 | const dependenceList = ['npm']; 59 | if (typeof plan === 'string') { 60 | dependenceList.push(plan); 61 | } else if (typeof plan === 'object') { 62 | Object.keys(plan).forEach(item => { 63 | const planName = plan[item]; 64 | if (dependenceList.indexOf(planName) === -1) { 65 | dependenceList.push(planName); 66 | } 67 | }); 68 | } 69 | 70 | const configInfo = felintConfig.readFelintConfig(); 71 | const dependenceConfig = configInfo.dependence; 72 | let dependencePackages = {}; 73 | dependenceList.forEach(item => { 74 | dependencePackages = Object.assign({}, dependencePackages, dependenceConfig[item] || {}); 75 | }); 76 | 77 | let msgInfo = await installSinglePackage(dependencePackages); 78 | return msgInfo || ''; 79 | } 80 | 81 | module.exports = { 82 | install 83 | }; 84 | -------------------------------------------------------------------------------- /src/utils/versionUtil.js: -------------------------------------------------------------------------------- 1 | /* global Promise */ 2 | 3 | let sh = require('shelljs'); 4 | var readline = require('readline'); 5 | 6 | let VERSION = require('../../package.json').version; 7 | 8 | let isBetaNow = VERSION.indexOf('alpha') > -1; 9 | 10 | function versionParser(version) { 11 | let info = {version: []}; 12 | if (version) { 13 | let part = version.split('-'); 14 | info.isBeta = part[1] && part[1].indexOf('alpha') > -1; 15 | if (info.isBeta) { 16 | info.betaVersion = +part[1].split('.')[1]; 17 | } 18 | let versionInfo = part[0].split('.'); 19 | info.version = [+versionInfo[0], +versionInfo[1], +versionInfo[2]]; 20 | } 21 | return info; 22 | } 23 | 24 | function compareVersion(versionA, versionB) { 25 | let versionAInfo = versionParser(versionA); 26 | let versionBInfo = versionParser(versionB); 27 | let compareValue = 0; 28 | let index = 0; 29 | do { 30 | compareValue = versionAInfo.version[index] - versionBInfo.version[index]; 31 | index ++; 32 | } while (compareValue === 0 && index <= 2); 33 | if (compareValue === 0) { 34 | if (versionAInfo.isBeta) { 35 | compareValue = -1; 36 | if (versionBInfo.isBeta) { 37 | compareValue = versionAInfo.betaVersion - versionBInfo.betaVersion; 38 | } 39 | } else if (versionBInfo.isBeta) { 40 | compareValue = 1; 41 | } else { 42 | compareValue = 0; 43 | } 44 | } 45 | return compareValue; 46 | } 47 | 48 | /** 49 | * 判断是否需要更新felint 50 | * @param {[type]} version [description] 51 | * @return {[type]} [description] 52 | */ 53 | function checkUpdate() { 54 | let olVersion; 55 | let isUpdating = false; 56 | return new Promise(res => { 57 | try { 58 | olVersion = sh.exec('npm view felint@latest version', {silent: true}); 59 | } catch (e) { 60 | console.log('检查更新失败'); 61 | res(isUpdating); 62 | } 63 | let compareValue = compareVersion(VERSION, olVersion); 64 | if (compareValue > 0 && isBetaNow) { 65 | try { 66 | olVersion = sh.exec('npm view felint@beta version', {silent: true}); 67 | } catch (e) { 68 | console.log('检查更新失败'); 69 | res(isUpdating); 70 | } 71 | compareValue = compareVersion(VERSION, olVersion); 72 | } 73 | if (compareValue < 0) { 74 | let rl = readline.createInterface({ 75 | input: process.stdin, 76 | output: process.stdout 77 | }); 78 | rl.question(`发现felint新版本${olVersion.trim().red},立即更新(Y/n)?`, (ans) => { 79 | rl.close(); 80 | if (ans !== 'n') { 81 | isUpdating = true; 82 | console.log('更新felint版本中...'); 83 | sh.exec(`npm install -d -g felint@${versionParser(olVersion).isBeta ? 'beta' : 'latest'}`); 84 | } 85 | res(isUpdating); 86 | }); 87 | } else { 88 | res(isUpdating); 89 | } 90 | }); 91 | } 92 | 93 | module.exports = { 94 | VERSION, 95 | isBetaNow, 96 | checkUpdate 97 | }; 98 | -------------------------------------------------------------------------------- /lib/utils/versionUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global Promise */ 4 | 5 | let sh = require('shelljs'); 6 | var readline = require('readline'); 7 | 8 | let VERSION = require('../../package.json').version; 9 | 10 | let isBetaNow = VERSION.indexOf('alpha') > -1; 11 | 12 | function versionParser(version) { 13 | let info = { version: [] }; 14 | if (version) { 15 | let part = version.split('-'); 16 | info.isBeta = part[1] && part[1].indexOf('alpha') > -1; 17 | if (info.isBeta) { 18 | info.betaVersion = +part[1].split('.')[1]; 19 | } 20 | let versionInfo = part[0].split('.'); 21 | info.version = [+versionInfo[0], +versionInfo[1], +versionInfo[2]]; 22 | } 23 | return info; 24 | } 25 | 26 | function compareVersion(versionA, versionB) { 27 | let versionAInfo = versionParser(versionA); 28 | let versionBInfo = versionParser(versionB); 29 | let compareValue = 0; 30 | let index = 0; 31 | do { 32 | compareValue = versionAInfo.version[index] - versionBInfo.version[index]; 33 | index++; 34 | } while (compareValue === 0 && index <= 2); 35 | if (compareValue === 0) { 36 | if (versionAInfo.isBeta) { 37 | compareValue = -1; 38 | if (versionBInfo.isBeta) { 39 | compareValue = versionAInfo.betaVersion - versionBInfo.betaVersion; 40 | } 41 | } else if (versionBInfo.isBeta) { 42 | compareValue = 1; 43 | } else { 44 | compareValue = 0; 45 | } 46 | } 47 | return compareValue; 48 | } 49 | 50 | /** 51 | * 判断是否需要更新felint 52 | * @param {[type]} version [description] 53 | * @return {[type]} [description] 54 | */ 55 | function checkUpdate() { 56 | let olVersion; 57 | let isUpdating = false; 58 | return new Promise(res => { 59 | try { 60 | olVersion = sh.exec('npm view felint@latest version', { silent: true }); 61 | } catch (e) { 62 | console.log('检查更新失败'); 63 | res(isUpdating); 64 | } 65 | let compareValue = compareVersion(VERSION, olVersion); 66 | if (compareValue > 0 && isBetaNow) { 67 | try { 68 | olVersion = sh.exec('npm view felint@beta version', { silent: true }); 69 | } catch (e) { 70 | console.log('检查更新失败'); 71 | res(isUpdating); 72 | } 73 | compareValue = compareVersion(VERSION, olVersion); 74 | } 75 | if (compareValue < 0) { 76 | let rl = readline.createInterface({ 77 | input: process.stdin, 78 | output: process.stdout 79 | }); 80 | rl.question(`发现felint新版本${olVersion.trim().red},立即更新(Y/n)?`, ans => { 81 | rl.close(); 82 | if (ans !== 'n') { 83 | isUpdating = true; 84 | console.log('更新felint版本中...'); 85 | sh.exec(`npm install -d -g felint@${versionParser(olVersion).isBeta ? 'beta' : 'latest'}`); 86 | } 87 | res(isUpdating); 88 | }); 89 | } else { 90 | res(isUpdating); 91 | } 92 | }); 93 | } 94 | 95 | module.exports = { 96 | VERSION, 97 | isBetaNow, 98 | checkUpdate 99 | }; -------------------------------------------------------------------------------- /lib/dependence.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * 安装配置指定的依赖 5 | */ 6 | let install = (() => { 7 | var _ref = _asyncToGenerator(function* (plan) { 8 | const dependenceList = ['npm']; 9 | if (typeof plan === 'string') { 10 | dependenceList.push(plan); 11 | } else if (typeof plan === 'object') { 12 | Object.keys(plan).forEach(function (item) { 13 | const planName = plan[item]; 14 | if (dependenceList.indexOf(planName) === -1) { 15 | dependenceList.push(planName); 16 | } 17 | }); 18 | } 19 | 20 | const configInfo = felintConfig.readFelintConfig(); 21 | const dependenceConfig = configInfo.dependence; 22 | let dependencePackages = {}; 23 | dependenceList.forEach(function (item) { 24 | dependencePackages = Object.assign({}, dependencePackages, dependenceConfig[item] || {}); 25 | }); 26 | 27 | let msgInfo = yield installSinglePackage(dependencePackages); 28 | return msgInfo || ''; 29 | }); 30 | 31 | return function install(_x) { 32 | return _ref.apply(this, arguments); 33 | }; 34 | })(); 35 | 36 | function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } 37 | 38 | const sh = require('shelljs'); 39 | const felintConfig = require('./felintConfig.js'); 40 | const checkPackage = require('./utils/checkPackage.js'); 41 | const felintrc = require('./felintrc.js'); 42 | 43 | /** 44 | * 安装npm包 45 | * @param {String} packageName 包名 46 | * @param {String} version 版本号 47 | */ 48 | function installPackage(packageName, version) { 49 | const felintrcFile = felintrc.read(); 50 | const { registryUrl, disturl, useYarn = true } = felintrcFile; 51 | const result = checkPackage(packageName, version); 52 | let installCommand = useYarn ? `yarn add ${packageName}@${version} --dev` : `npm i -D ${packageName}@${version}`; 53 | 54 | if (registryUrl) { 55 | installCommand += ` --registry=${registryUrl}`; 56 | } 57 | 58 | if (disturl) { 59 | installCommand += ` --disturl=${disturl}`; 60 | } 61 | 62 | if (!result) { 63 | try { 64 | console.log(`开始安装${packageName}@${version}`.green); 65 | sh.exec(installCommand); 66 | } catch (e) { 67 | console.log(`${packageName}@${version}安装失败,请检查`.red); 68 | } 69 | } else { 70 | return result; 71 | } 72 | } 73 | 74 | // 安装单个依赖 75 | function installSinglePackage(typeInfo = {}) { 76 | return new Promise(res => { 77 | const npmDepList = Object.keys(typeInfo); 78 | const msgInfo = []; 79 | 80 | npmDepList.forEach(packageName => { 81 | let result = installPackage(packageName, typeInfo[packageName]); 82 | if (result && result.current) { 83 | msgInfo.push(`你已安装${`${packageName}@${result.current}`.red},默认版本为${typeInfo[packageName].green},${'请确认!'.red}`); 84 | } 85 | }); 86 | 87 | res(msgInfo); 88 | }); 89 | } 90 | 91 | module.exports = { 92 | install 93 | }; -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('colors'); 3 | const program = require('commander'); 4 | const process = require('process'); 5 | const path = require('path'); 6 | const sh = require('shelljs'); 7 | const versionUtil = require('./utils/versionUtil.js'); 8 | const fetchConfig = require('./fetchConfig.js'); 9 | const dependence = require('./dependence.js'); 10 | const ruleFile = require('./ruleFile.js'); 11 | const felintrc = require('./felintrc.js'); 12 | const DEFAUTL_CONFIG_URL = 'https://github.com/youzan/felint-config.git'; 13 | 14 | program 15 | .version(versionUtil.VERSION) 16 | .command('init') 17 | .option('-p, --plan [value]', '使用指定代码规范方案') 18 | .option('-f, --force', '强制更新规则文件') 19 | .description('使用felint初始化项目。更多信息请参考:https://github.com/youzan/felint/blob/master/README.md') 20 | .action(async (options) => { 21 | const isUpdating = await versionUtil.checkUpdate(); 22 | if (!isUpdating) { 23 | const felintrcFile = felintrc.read(); 24 | await fetchConfig(felintrcFile || {}); 25 | 26 | const plan = options.plan || felintrc.getPlan() || 'default'; 27 | console.log('开始安装本地依赖...'.green); 28 | let msgInfo = await dependence.install(plan); 29 | console.log(msgInfo.join('\n')); 30 | 31 | sh.exec('rm ./.eslintrc ./.sass-lint.yml'); 32 | await ruleFile.createIgnore(); 33 | await felintrc.set({ 34 | plan 35 | }); 36 | await ruleFile.createPlan(plan, options.force); 37 | 38 | sh.exec('rm -rf ./.felint', { silent: true }); 39 | } 40 | }); 41 | 42 | // 更新依赖 43 | program 44 | .command('dep') 45 | .description('安装 eslint/stylelint 及其依赖,并写入package.json') 46 | .action(async () => { 47 | const isUpdating = await versionUtil.checkUpdate(); 48 | if (!isUpdating) { 49 | const felintrcFile = felintrc.read(); 50 | // 拉取配置 51 | await fetchConfig(felintrcFile || {}); 52 | 53 | // 更新依赖 54 | const plan = felintrc.getPlan() || 'default'; 55 | console.log('开始更新依赖...'.green); 56 | let msgInfo = await dependence.install(plan); 57 | console.log(msgInfo.join('\n')); 58 | 59 | sh.exec('rm -rf ./.felint', { silent: true }); 60 | } 61 | }); 62 | 63 | // 更新规则文件 64 | program 65 | .command('rules') 66 | .option('-f, --force', '强制更新规则文件') 67 | .description('更新规则文件') 68 | .action(async (options) => { 69 | const felintrcFile = felintrc.read(); 70 | // 拉取最新配置 71 | await fetchConfig(felintrcFile || {}); 72 | 73 | // 根据plan更新校验规则 74 | const plan = felintrc.getPlan() || 'default'; 75 | await ruleFile.createPlan(plan, options.force); 76 | 77 | sh.exec('rm -rf ./.felint', { silent: true }); 78 | }); 79 | 80 | // 返回felint base path 81 | program 82 | .command('where') 83 | .description('返回felint安装路径') 84 | .action(() => { 85 | console.log(path.dirname(__dirname)); 86 | }); 87 | 88 | // 输出felint-config仓库地址 89 | program 90 | .command('config-url') 91 | .description('输出felint-config仓库地址') 92 | .action(() => { 93 | const felintrcFile = felintrc.read(); 94 | console.log(felintrcFile.configRep || felintrcFile.gitHookUrl || DEFAUTL_CONFIG_URL); 95 | }); 96 | 97 | program.parse(process.argv); 98 | 99 | process.once('uncaughtException', function() { 100 | process.exitCode = 1; 101 | }); 102 | 103 | if (!process.argv.slice(2).length) { 104 | program.outputHelp(); 105 | } 106 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } 5 | 6 | require('colors'); 7 | const program = require('commander'); 8 | const process = require('process'); 9 | const path = require('path'); 10 | const sh = require('shelljs'); 11 | const versionUtil = require('./utils/versionUtil.js'); 12 | const fetchConfig = require('./fetchConfig.js'); 13 | const dependence = require('./dependence.js'); 14 | const ruleFile = require('./ruleFile.js'); 15 | const felintrc = require('./felintrc.js'); 16 | const DEFAUTL_CONFIG_URL = 'https://github.com/youzan/felint-config.git'; 17 | 18 | program.version(versionUtil.VERSION).command('init').option('-p, --plan [value]', '使用指定代码规范方案').option('-f, --force', '强制更新规则文件').description('使用felint初始化项目。更多信息请参考:https://github.com/youzan/felint/blob/master/README.md').action((() => { 19 | var _ref = _asyncToGenerator(function* (options) { 20 | const isUpdating = yield versionUtil.checkUpdate(); 21 | if (!isUpdating) { 22 | const felintrcFile = felintrc.read(); 23 | yield fetchConfig(felintrcFile || {}); 24 | 25 | const plan = options.plan || felintrc.getPlan() || 'default'; 26 | console.log('开始安装本地依赖...'.green); 27 | let msgInfo = yield dependence.install(plan); 28 | console.log(msgInfo.join('\n')); 29 | 30 | sh.exec('rm ./.eslintrc ./.sass-lint.yml'); 31 | yield ruleFile.createIgnore(); 32 | yield felintrc.set({ 33 | plan 34 | }); 35 | yield ruleFile.createPlan(plan, options.force); 36 | 37 | sh.exec('rm -rf ./.felint', { silent: true }); 38 | } 39 | }); 40 | 41 | return function (_x) { 42 | return _ref.apply(this, arguments); 43 | }; 44 | })()); 45 | 46 | // 更新依赖 47 | program.command('dep').description('安装 eslint/stylelint 及其依赖,并写入package.json').action(_asyncToGenerator(function* () { 48 | const isUpdating = yield versionUtil.checkUpdate(); 49 | if (!isUpdating) { 50 | const felintrcFile = felintrc.read(); 51 | // 拉取配置 52 | yield fetchConfig(felintrcFile || {}); 53 | 54 | // 更新依赖 55 | const plan = felintrc.getPlan() || 'default'; 56 | console.log('开始更新依赖...'.green); 57 | let msgInfo = yield dependence.install(plan); 58 | console.log(msgInfo.join('\n')); 59 | 60 | sh.exec('rm -rf ./.felint', { silent: true }); 61 | } 62 | })); 63 | 64 | // 更新规则文件 65 | program.command('rules').option('-f, --force', '强制更新规则文件').description('更新规则文件').action((() => { 66 | var _ref3 = _asyncToGenerator(function* (options) { 67 | const felintrcFile = felintrc.read(); 68 | // 拉取最新配置 69 | yield fetchConfig(felintrcFile || {}); 70 | 71 | // 根据plan更新校验规则 72 | const plan = felintrc.getPlan() || 'default'; 73 | yield ruleFile.createPlan(plan, options.force); 74 | 75 | sh.exec('rm -rf ./.felint', { silent: true }); 76 | }); 77 | 78 | return function (_x2) { 79 | return _ref3.apply(this, arguments); 80 | }; 81 | })()); 82 | 83 | // 返回felint base path 84 | program.command('where').description('返回felint安装路径').action(() => { 85 | console.log(path.dirname(__dirname)); 86 | }); 87 | 88 | // 输出felint-config仓库地址 89 | program.command('config-url').description('输出felint-config仓库地址').action(() => { 90 | const felintrcFile = felintrc.read(); 91 | console.log(felintrcFile.configRep || felintrcFile.gitHookUrl || DEFAUTL_CONFIG_URL); 92 | }); 93 | 94 | program.parse(process.argv); 95 | 96 | process.once('uncaughtException', function () { 97 | process.exitCode = 1; 98 | }); 99 | 100 | if (!process.argv.slice(2).length) { 101 | program.outputHelp(); 102 | } -------------------------------------------------------------------------------- /src/utils/fileUtil.js: -------------------------------------------------------------------------------- 1 | /* global Promise */ 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const YAML = require('js-yaml'); 5 | const readline = require('readline'); 6 | 7 | /** 8 | * 判断是否需要覆盖对应文件 9 | * @param {String} filePath 需要覆盖的文件路径 10 | */ 11 | function checkOverride(filePath) { 12 | return new Promise(res => { 13 | const fileStat = has(filePath); 14 | if (fileStat && fileStat.isFile()) { 15 | const rl = readline.createInterface({ 16 | input: process.stdin, 17 | output: process.stdout 18 | }); 19 | rl.question(`${filePath}文件已存在,是否要覆盖(Y/n)?`, ans => { 20 | rl.close(); 21 | if (ans !== 'n') { 22 | res(true); 23 | } else { 24 | res(false); 25 | } 26 | }); 27 | } else { 28 | res(true); 29 | } 30 | }); 31 | } 32 | 33 | /** 34 | * 获取文件扩展名 35 | * @param {String} filename 文件名 36 | */ 37 | function getFileExtension(filename) { 38 | return filename.slice((filename.lastIndexOf('.') - 1 >>> 0) + 2); 39 | } 40 | 41 | /** 42 | * 判断是否存在对应文件 43 | * @param {String} filePath 文件路径 44 | */ 45 | function has(filePath) { 46 | if (filePath) { 47 | let s; 48 | try { 49 | s = fs.statSync(filePath); 50 | } catch (e) { 51 | return false; 52 | } 53 | return s; 54 | } else { 55 | return false; 56 | } 57 | } 58 | 59 | function treeHas(prePath, pathStr, targetFold) { 60 | if (prePath !== pathStr && pathStr && targetFold) { 61 | const fp = `${pathStr}/${targetFold}`; 62 | const s = has(fp); 63 | if (!s) { 64 | return treeHas(pathStr, path.dirname(pathStr), targetFold); 65 | } else { 66 | return { 67 | 'stat': s, 68 | 'path': fp, 69 | 'dirname': pathStr 70 | }; 71 | } 72 | } else { 73 | return false; 74 | } 75 | } 76 | 77 | function findUp(pathStr, target, type) { 78 | const result = treeHas('', pathStr, target); 79 | if (type && result) { 80 | if (!result.stat[type]()) { 81 | return findUp(path.dirname(result.path), target, type); 82 | } 83 | } 84 | return result; 85 | } 86 | 87 | /** 88 | * 读取文件内容 89 | * @param {String} pathStr 文件路径 90 | * @param {String} ext 扩展名 91 | */ 92 | function readFile(pathStr, ext) { 93 | if (!pathStr) { 94 | return; 95 | } 96 | 97 | let fileContent; 98 | if (ext === 'js') { 99 | fileContent = require(pathStr); 100 | } else { 101 | try { 102 | fileContent = fs.readFileSync(pathStr, 'utf8'); 103 | } catch (e) { 104 | console.log(`读取${pathStr}失败`.red); 105 | return; 106 | } 107 | } 108 | 109 | if (ext === 'json') { 110 | fileContent = JSON.parse(fileContent); 111 | } 112 | 113 | if (ext === 'yaml' || ext === 'yml') { 114 | try { 115 | fileContent = YAML.safeLoad(fileContent); 116 | } catch (e) { 117 | console.log(`解析${pathStr}为yaml格式失败`.red); 118 | return; 119 | } 120 | } 121 | 122 | return fileContent; 123 | } 124 | 125 | /** 126 | * 创建文件 127 | * @param {String} pathStr 文件路径 128 | * @param {String} contentStr 文件内容 129 | * @param {String} ext 扩展名 130 | */ 131 | function createFileSync(pathStr, contentStr, ext) { 132 | if (pathStr && contentStr) { 133 | if (typeof contentStr === 'object') { 134 | if (ext === 'yaml' || ext === 'yml') { 135 | contentStr = YAML.safeDump(contentStr || {}); 136 | } else { 137 | contentStr = JSON.stringify(contentStr, null, 2); 138 | } 139 | } 140 | 141 | fs.writeFileSync(pathStr, contentStr); 142 | } 143 | } 144 | 145 | /** 146 | * 树形读取文件 147 | * @param {String} filename 文件路径 148 | * @param {String} ext 扩展名 149 | */ 150 | function treeReadFile(filename, ext = 'json') { 151 | // read file content 152 | const fileInfo = findUp(process.cwd(), filename, 'isFile'); 153 | let content; 154 | 155 | if (fileInfo) { 156 | content = readFile(fileInfo.path, ext); 157 | } 158 | 159 | return content; 160 | } 161 | 162 | module.exports = { 163 | readFile, 164 | treeReadFile, 165 | findUp, 166 | has, 167 | getFileExtension, 168 | checkOverride, 169 | createFileSync 170 | }; 171 | -------------------------------------------------------------------------------- /lib/utils/fileUtil.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* global Promise */ 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const YAML = require('js-yaml'); 7 | const readline = require('readline'); 8 | 9 | /** 10 | * 判断是否需要覆盖对应文件 11 | * @param {String} filePath 需要覆盖的文件路径 12 | */ 13 | function checkOverride(filePath) { 14 | return new Promise(res => { 15 | const fileStat = has(filePath); 16 | if (fileStat && fileStat.isFile()) { 17 | const rl = readline.createInterface({ 18 | input: process.stdin, 19 | output: process.stdout 20 | }); 21 | rl.question(`${filePath}文件已存在,是否要覆盖(Y/n)?`, ans => { 22 | rl.close(); 23 | if (ans !== 'n') { 24 | res(true); 25 | } else { 26 | res(false); 27 | } 28 | }); 29 | } else { 30 | res(true); 31 | } 32 | }); 33 | } 34 | 35 | /** 36 | * 获取文件扩展名 37 | * @param {String} filename 文件名 38 | */ 39 | function getFileExtension(filename) { 40 | return filename.slice((filename.lastIndexOf('.') - 1 >>> 0) + 2); 41 | } 42 | 43 | /** 44 | * 判断是否存在对应文件 45 | * @param {String} filePath 文件路径 46 | */ 47 | function has(filePath) { 48 | if (filePath) { 49 | let s; 50 | try { 51 | s = fs.statSync(filePath); 52 | } catch (e) { 53 | return false; 54 | } 55 | return s; 56 | } else { 57 | return false; 58 | } 59 | } 60 | 61 | function treeHas(prePath, pathStr, targetFold) { 62 | if (prePath !== pathStr && pathStr && targetFold) { 63 | const fp = `${pathStr}/${targetFold}`; 64 | const s = has(fp); 65 | if (!s) { 66 | return treeHas(pathStr, path.dirname(pathStr), targetFold); 67 | } else { 68 | return { 69 | 'stat': s, 70 | 'path': fp, 71 | 'dirname': pathStr 72 | }; 73 | } 74 | } else { 75 | return false; 76 | } 77 | } 78 | 79 | function findUp(pathStr, target, type) { 80 | const result = treeHas('', pathStr, target); 81 | if (type && result) { 82 | if (!result.stat[type]()) { 83 | return findUp(path.dirname(result.path), target, type); 84 | } 85 | } 86 | return result; 87 | } 88 | 89 | /** 90 | * 读取文件内容 91 | * @param {String} pathStr 文件路径 92 | * @param {String} ext 扩展名 93 | */ 94 | function readFile(pathStr, ext) { 95 | if (!pathStr) { 96 | return; 97 | } 98 | 99 | let fileContent; 100 | if (ext === 'js') { 101 | fileContent = require(pathStr); 102 | } else { 103 | try { 104 | fileContent = fs.readFileSync(pathStr, 'utf8'); 105 | } catch (e) { 106 | console.log(`读取${pathStr}失败`.red); 107 | return; 108 | } 109 | } 110 | 111 | if (ext === 'json') { 112 | fileContent = JSON.parse(fileContent); 113 | } 114 | 115 | if (ext === 'yaml' || ext === 'yml') { 116 | try { 117 | fileContent = YAML.safeLoad(fileContent); 118 | } catch (e) { 119 | console.log(`解析${pathStr}为yaml格式失败`.red); 120 | return; 121 | } 122 | } 123 | 124 | return fileContent; 125 | } 126 | 127 | /** 128 | * 创建文件 129 | * @param {String} pathStr 文件路径 130 | * @param {String} contentStr 文件内容 131 | * @param {String} ext 扩展名 132 | */ 133 | function createFileSync(pathStr, contentStr, ext) { 134 | if (pathStr && contentStr) { 135 | if (typeof contentStr === 'object') { 136 | if (ext === 'yaml' || ext === 'yml') { 137 | contentStr = YAML.safeDump(contentStr || {}); 138 | } else { 139 | contentStr = JSON.stringify(contentStr, null, 2); 140 | } 141 | } 142 | 143 | fs.writeFileSync(pathStr, contentStr); 144 | } 145 | } 146 | 147 | /** 148 | * 树形读取文件 149 | * @param {String} filename 文件路径 150 | * @param {String} ext 扩展名 151 | */ 152 | function treeReadFile(filename, ext = 'json') { 153 | // read file content 154 | const fileInfo = findUp(process.cwd(), filename, 'isFile'); 155 | let content; 156 | 157 | if (fileInfo) { 158 | content = readFile(fileInfo.path, ext); 159 | } 160 | 161 | return content; 162 | } 163 | 164 | module.exports = { 165 | readFile, 166 | treeReadFile, 167 | findUp, 168 | has, 169 | getFileExtension, 170 | checkOverride, 171 | createFileSync 172 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | youzan 3 |

4 |

5 | 6 | 7 | 8 |

9 |

A smart way to eslint stylelint and git hooks for front end

10 | 11 | 12 | [![npm version](https://img.shields.io/npm/v/felint.svg?style=flat)](https://www.npmjs.com/package/felint) [![downloads](https://img.shields.io/npm/dt/felint.svg)](https://www.npmjs.com/package/felint) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](CONTRIBUTING.md) 13 | 14 | [![NPM](https://nodei.co/npm/felint.png?downloads=true&downloadRank=true)](https://nodei.co/npm/felint/) 15 | 16 | ## 一、什么是Felint 17 | 18 | felint 是一个同步前端代码校验规则的工具。 19 | 20 | felint 为你的项目做以下两件事: 21 | 22 | 1. 初始化 eslint/stylelint 配置文件,无论是 react 项目、vue 项目、es5 还是 es6 都提供了针对性的配置方案 23 | 1. 安装 eslint/stylelint 及其依赖到当前项目的 node_modules 里 24 | 25 | 如果你需要利用`git`的钩子来校验代码,推荐使用:[husky](https://github.com/typicode/husky) 和 [lint-staged](https://github.com/okonet/lint-staged)。 26 | 27 | ## 二、安装 felint 28 | 29 | ``` 30 | npm install -g felint 31 | ``` 32 | 33 | ## 三、快速开始 34 | 35 | ### 第一步 36 | 37 | 在项目的根目录,执行 38 | 39 | ``` 40 | felint init 41 | ``` 42 | 43 | `felint` 初始化完成后你的项目中将会产生如下文件: 44 | 45 | ``` 46 | |_.felintrc // 第一次被执行 felint init 后会在项目根目录生成这个文件,具体见下面 .felintrc 章节 47 | |_.eslintrc.json // eslint 规则文件,用于检测js代码(使用的是官方推荐的配置) 48 | |_.eslintignore // eslint ignore配置文件 49 | |_.stylelintrc.js // stylelint 规则文件,用于检测css代码(使用的是官方推荐的配置) 50 | |_.stylelintignore // stylelint ignore配置文件 51 | ``` 52 | 53 | ### 第二步 54 | 55 | 将这些 `felint` 生成的文件提交到 git 仓库。 56 | 57 | ### 第三步(高阶用法) 58 | 59 | 默认情况下,`felint` 是同步 `ES6` 方案的校验规则,当你需要在项目中使用`Vue` 或 `React`规则时,我们可以在初始化时指定一个规则方案: 60 | 61 | ``` 62 | felint init -p vue 63 | ``` 64 | 65 | 此时,`felint` 将会在你的项目中初始化适用于 `Vue` 的校验规则。 66 | 67 | 如果你需要在一个项目的不同目录使用不同的代码规范时,可以修改 `.felintrc` 文件自定义校验规则: 68 | 69 | ``` 70 | { 71 | "plan": { 72 | "./app": "node", 73 | "./client": "vue" 74 | } 75 | } 76 | ``` 77 | 78 | 此时在 `./app` 目录生成 node 的校验规则,在 `./client` 目录会生成 vue 的校验规则。 79 | 80 | 如果你不想使用默认的[felint-config](https://github.com/youzan/felint-config)规则来校验,你可以fork出来修改为自己的 felint-config(修改方法参考 [felint-config 的 readme](https://github.com/youzan/felint-config/blob/master/README.md) ),然后将 `.felintrc` 文件的 `gitHookUrl` 字段配置为你自己的仓库地址即可。 81 | 82 | ``` 83 | { 84 | "gitHookUrl": "https://github.com/youzan/felint-config.git" 85 | } 86 | ``` 87 | 88 | 当你更新了 felint-config 中的规则依赖,你需要在项目的根目录执行以下命令同步最新的依赖: 89 | 90 | ``` 91 | felint dep 92 | ``` 93 | 94 | 当你更新了 felint-config 中的校验规则,你需要在项目的根目录执行以下命令同步最新的规则文件: 95 | 96 | ``` 97 | felint rules 98 | ``` 99 | 100 | ## 四、名词解释 101 | 102 | - `felint-config`:里面包含了代码校验规则的配置信息,默认为:[felint-config](https://github.com/youzan/felint-config) 103 | - `.felintrc`:类似于 `.babelrc`文件,放在项目根目录用于配置 `felint-config` 的git仓库地址,和项目的代码规范方案。 104 | - `eslint`:`JavaScript` 代码校验工具,详细文档点[这里](https://eslint.org/) 105 | - `stylelint`:`CSS` 代码校验工具,详细文档点[这里](https://github.com/stylelint/stylelint) 106 | 107 | ## 五、felint命令详解 108 | 109 | ### 1. felint init 110 | 111 | ``` 112 | felint init -p planname -f 113 | 114 | -p planname: 115 | 用于指定初始化规则方案,如果不指定则会使用默认的规则 116 | -f: 117 | 是否强制覆盖已有的规则 118 | ``` 119 | 120 | 执行 `felint init` 命令后,felint会读取 `.felintrc` 中 `felint-config` 配置的校验规则仓库地址(如未指定则会使用默认地址),当配置文件下载完成后,`felint` 将自动执行配置文件内部的初始化脚本文件,并生成最终规则文件。 121 | 122 | 关于规则方案声明请参见[felint-config介绍](https://github.com/youzan/felint-config) 123 | 124 | ### 2. felint dep 125 | 126 | 该命令会下载 `eslint` 和 `stylelint` 所需要的依赖,并写入到 `package.json` 中。 127 | 128 | ``` 129 | felint dep 130 | ``` 131 | 132 | ### 3. felint rules 133 | 134 | 该命令会根据 `.felintrc` 配置文件同步最新的校验规则文件。 135 | 136 | ``` 137 | felint rules -f 138 | 139 | -f: 140 | 是否强制覆盖已有的规则 141 | ``` 142 | 143 | ### 4. felint config-url 144 | 145 | ``` 146 | felint config-url 147 | ``` 148 | 149 | 输出 `felint config` 配置的仓库地址。 150 | 151 | ## 六、.felintrc文件 152 | 153 | **.felintrc**用于配置`felint-config`的git仓库地址、对默认规则进行一定程度的自定义覆盖以及记录该项目所使用的代码规则方案。 154 | 155 | **e.g.** 156 | 157 | ``` 158 | { 159 | gitHookUrl // 用于指定使用的`felint-config`仓库地址 160 | plan // 用于指定当前项目所使用的规则方案,比如`es5/es6/vue/react`等 161 | disturl // 用于指定安装依赖时的`disturl` 162 | registryUrl // 用于指定安装依赖时的`registry` 163 | useYarn // 是否使用yarn来安装依赖,默认为`true` 164 | } 165 | ``` 166 | 167 | ## 七、开源协议 168 | 本项目基于 [MIT](https://zh.wikipedia.org/wiki/MIT%E8%A8%B1%E5%8F%AF%E8%AD%89)协议,请自由地享受和参与开源。 169 | -------------------------------------------------------------------------------- /.felint/rules/.eslintrc_vue.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 6, 4 | "ecmaFeatures": { 5 | "experimentalObjectRestSpread": true, 6 | "jsx": true 7 | }, 8 | "sourceType": "module" 9 | }, 10 | 11 | "env": { 12 | "es6": true, 13 | "node": true, 14 | "browser": true 15 | }, 16 | 17 | "plugins": ["vue"], 18 | 19 | "rules": { 20 | "accessor-pairs": 2, 21 | "arrow-spacing": [2, { "before": true, "after": true }], 22 | "block-spacing": [2, "always"], 23 | "brace-style": [2, "1tbs", { "allowSingleLine": true }], 24 | "camelcase": [2, { "properties": "always" }], 25 | "comma-dangle": [2, "never"], 26 | "comma-spacing": [2, { "before": false, "after": true }], 27 | "comma-style": [2, "last"], 28 | "constructor-super": 2, 29 | "curly": [2, "multi-line"], 30 | "dot-location": [2, "property"], 31 | "eol-last": 2, 32 | "eqeqeq": [2, "allow-null"], 33 | "generator-star-spacing": [2, { "before": true, "after": true }], 34 | "handle-callback-err": [2, "^(err|error)$" ], 35 | "indent": [2, 2, { "SwitchCase": 1 }], 36 | "jsx-quotes": [2, "prefer-single"], 37 | "key-spacing": [2, { "beforeColon": false, "afterColon": true }], 38 | "keyword-spacing": [2, { "before": true, "after": true }], 39 | "new-cap": [2, { "newIsCap": true, "capIsNew": false }], 40 | "new-parens": 2, 41 | "no-array-constructor": 2, 42 | "no-caller": 2, 43 | "no-class-assign": 2, 44 | "no-cond-assign": 2, 45 | "no-const-assign": 2, 46 | "no-control-regex": 2, 47 | "no-delete-var": 2, 48 | "no-dupe-args": 2, 49 | "no-dupe-class-members": 2, 50 | "no-dupe-keys": 2, 51 | "no-duplicate-case": 2, 52 | "no-empty-character-class": 2, 53 | "no-empty-pattern": 2, 54 | "no-eval": 2, 55 | "no-ex-assign": 2, 56 | "no-extend-native": 2, 57 | "no-extra-bind": 2, 58 | "no-extra-boolean-cast": 2, 59 | "no-extra-parens": [2, "functions"], 60 | "no-fallthrough": 2, 61 | "no-floating-decimal": 2, 62 | "no-func-assign": 2, 63 | "no-implied-eval": 2, 64 | "no-inner-declarations": [2, "functions"], 65 | "no-invalid-regexp": 2, 66 | "no-irregular-whitespace": 2, 67 | "no-iterator": 2, 68 | "no-label-var": 2, 69 | "no-labels": [2, { "allowLoop": false, "allowSwitch": false }], 70 | "no-lone-blocks": 2, 71 | "no-mixed-spaces-and-tabs": 2, 72 | "no-multi-spaces": 2, 73 | "no-multi-str": 2, 74 | "no-multiple-empty-lines": [2, { "max": 1 }], 75 | "no-native-reassign": 2, 76 | "no-negated-in-lhs": 2, 77 | "no-new-object": 2, 78 | "no-new-require": 2, 79 | "no-new-symbol": 2, 80 | "no-new-wrappers": 2, 81 | "no-obj-calls": 2, 82 | "no-octal": 2, 83 | "no-octal-escape": 2, 84 | "no-path-concat": 2, 85 | "no-proto": 2, 86 | "no-redeclare": 2, 87 | "no-regex-spaces": 2, 88 | "no-return-assign": [2, "except-parens"], 89 | "no-self-assign": 2, 90 | "no-self-compare": 2, 91 | "no-sequences": 2, 92 | "no-shadow-restricted-names": 2, 93 | "no-spaced-func": 2, 94 | "no-sparse-arrays": 2, 95 | "no-this-before-super": 2, 96 | "no-throw-literal": 2, 97 | "no-trailing-spaces": 2, 98 | "no-undef": 2, 99 | "no-undef-init": 2, 100 | "no-unexpected-multiline": 2, 101 | "no-unmodified-loop-condition": 2, 102 | "no-unneeded-ternary": [2, { "defaultAssignment": false }], 103 | "no-unreachable": 2, 104 | "no-unsafe-finally": 2, 105 | "no-unused-vars": [2, { "vars": "all", "args": "none" }], 106 | "no-useless-call": 2, 107 | "no-useless-computed-key": 2, 108 | "no-useless-constructor": 2, 109 | "no-useless-escape": 0, 110 | "no-whitespace-before-property": 2, 111 | "no-with": 2, 112 | "one-var": [2, { "initialized": "never" }], 113 | "operator-linebreak": [2, "after", { "overrides": { "?": "before", ":": "before" } }], 114 | "padded-blocks": [2, "never"], 115 | "quotes": [2, "single", { "avoidEscape": true, "allowTemplateLiterals": true }], 116 | "semi": [2, "always"], 117 | "semi-spacing": [2, { "before": false, "after": true }], 118 | "space-before-blocks": [2, "always"], 119 | "space-before-function-paren": [2, "never"], 120 | "space-in-parens": [2, "never"], 121 | "space-infix-ops": 2, 122 | "space-unary-ops": [2, { "words": true, "nonwords": false }], 123 | "spaced-comment": [2, "always", { "markers": ["global", "globals", "eslint", "eslint-disable", "*package", "!", ","] }], 124 | "template-curly-spacing": [2, "never"], 125 | "use-isnan": 2, 126 | "valid-typeof": 2, 127 | "wrap-iife": [2, "any"], 128 | "yield-star-spacing": [2, "both"], 129 | "yoda": [2, "never"], 130 | "prefer-const": 2, 131 | "no-debugger": 2, 132 | "object-curly-spacing": [2, "always", { "objectsInObjects": false }], 133 | "array-bracket-spacing": [2, "never"], 134 | "vue/jsx-uses-vars": 2 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/ruleFile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const sh = require('shelljs'); 3 | const fileUtil = require('./utils/fileUtil.js'); 4 | const felintConfig = require('./felintConfig.js'); 5 | 6 | /** 7 | * 合并两个对象 8 | * @param {Object} target 9 | * @param {Object} another 10 | */ 11 | function mergeObject(target, another) { 12 | if (target && another) { 13 | Object.keys(target).concat(Object.keys(another)).forEach((key) => { 14 | if (target[key] === undefined) { 15 | target[key] = another[key]; 16 | } else { 17 | if (toString.call(target[key]) === '[object Object]') { 18 | Object.assign(target[key], another[key] || {}); 19 | } else { 20 | target[key] = another[key] === undefined ? target[key] : another[key]; 21 | } 22 | } 23 | }); 24 | } 25 | return target; 26 | } 27 | 28 | /** 29 | * 生成对应的规则 30 | * @param {String} planName plan名 31 | */ 32 | async function createPlan(planName = 'default', force) { 33 | const ruleConfig = felintConfig.readFelintConfig(); 34 | const planConfig = {}; 35 | 36 | if (typeof planName === 'object') { 37 | Object.keys(planName).forEach(planPath => { 38 | if (!planPath) return; 39 | 40 | planConfig[planPath] = ruleConfig.plan[planName[planPath]]; 41 | }); 42 | } else { 43 | planConfig[process.cwd()] = ruleConfig.plan[planName]; 44 | } 45 | 46 | const planKeys = Object.keys(planConfig); 47 | if (planKeys.length !== 0) { 48 | for (let i = 0; i < planKeys.length; i++) { 49 | const planPath = planKeys[i]; 50 | const ruleList = planConfig[planPath]; 51 | 52 | for (let j = ruleList.length - 1; j >= 0; j--) { 53 | const filename = ruleList[j]; 54 | await createFile(filename, planPath, force); 55 | } 56 | } 57 | } 58 | } 59 | 60 | /** 61 | * 文件名命名规则 62 | * 最终产生规则文件 63 | * @param {String} felintDirPath .felint路径 64 | * @param {String} fileName 文件名 65 | * @param {Boolean} force 是否强制更新文件 66 | */ 67 | async function createFile(fileName, targetFolder, force) { 68 | const felintDirPath = felintConfig.felintDirPath(); 69 | 70 | if (felintDirPath && fileName) { 71 | const ext = fileUtil.getFileExtension(fileName).toLowerCase(); 72 | const fileNE = fileName.slice(0, ext.length ? (-ext.length - 1) : fileName.length); 73 | 74 | // 需要生成的目录 75 | const targetFilePath = targetFolder ? path.resolve(process.cwd(), targetFolder) : process.cwd(); 76 | 77 | // 判断目标目录路径是否存在 78 | if (!fileUtil.has(targetFilePath)) return; 79 | 80 | // 生成的文件路径 81 | let targetFileName = `${targetFilePath}/${fileNE.split('_')[0]}${ext ? `.${ext}` : ''}`; 82 | 83 | if (fileNE.indexOf('stylelint') > -1) { 84 | targetFileName = `${targetFilePath}/${fileNE.split('_')[0]}.json`; 85 | } 86 | 87 | const override = force || await fileUtil.checkOverride(targetFileName); 88 | const sourceFilePath = `${felintDirPath.path}/rules/${fileName}`; 89 | 90 | // 覆盖文件 91 | if (override) { 92 | if (ext === 'json') { 93 | await createJsonFile(targetFileName, sourceFilePath, ext); 94 | } else { 95 | sh.cp(sourceFilePath, targetFileName); 96 | } 97 | console.log(`你在目录${targetFilePath}已创建${fileName}规则`.green); 98 | } 99 | } 100 | } 101 | 102 | async function createJsonFile(targetFileName, sourceFilePath, ext) { 103 | const targetFileContent = fileUtil.readFile(targetFileName, ext); 104 | let fileContent = ''; 105 | 106 | if (ext === 'json') { 107 | fileContent = await fileUtil.readFile(sourceFilePath, ext); 108 | fileContent = mergeObject(fileContent, targetFileContent); 109 | 110 | fileUtil.createFileSync(targetFileName, JSON.stringify(fileContent || {}, null, 2), ext); 111 | } 112 | } 113 | 114 | /** 115 | * 创建ignore文件 116 | */ 117 | async function createIgnore() { 118 | const felintDirPath = felintConfig.felintDirPath(); 119 | 120 | if (felintDirPath && felintDirPath.path) { 121 | // 查看.felint下有无.eslintignore文件 122 | const hasEslintIgnoreFile = fileUtil.has(`${felintDirPath.path}/.eslintignore`); 123 | 124 | if (hasEslintIgnoreFile) { 125 | const override = await fileUtil.checkOverride(`${process.cwd()}/.eslintignore`); 126 | 127 | if (override) { 128 | sh.cp(`${felintDirPath.path}/.eslintignore`, `${process.cwd()}/.eslintignore`); 129 | } 130 | } 131 | 132 | // 查看.felint下有无.stylelintignore文件 133 | const hasStylelintIgnoreFile = fileUtil.has(`${felintDirPath.path}/.stylelintignore`); 134 | 135 | if (hasStylelintIgnoreFile) { 136 | const override = await fileUtil.checkOverride(`${process.cwd()}/.stylelintignore`); 137 | 138 | if (override) { 139 | sh.cp(`${felintDirPath.path}/.stylelintignore`, `${process.cwd()}/.stylelintignore`); 140 | } 141 | } 142 | } 143 | } 144 | 145 | module.exports = { 146 | createPlan, 147 | createIgnore 148 | }; 149 | -------------------------------------------------------------------------------- /lib/ruleFile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * 生成对应的规则 5 | * @param {String} planName plan名 6 | */ 7 | let createPlan = (() => { 8 | var _ref = _asyncToGenerator(function* (planName = 'default', force) { 9 | const ruleConfig = felintConfig.readFelintConfig(); 10 | const planConfig = {}; 11 | 12 | if (typeof planName === 'object') { 13 | Object.keys(planName).forEach(function (planPath) { 14 | if (!planPath) return; 15 | 16 | planConfig[planPath] = ruleConfig.plan[planName[planPath]]; 17 | }); 18 | } else { 19 | planConfig[process.cwd()] = ruleConfig.plan[planName]; 20 | } 21 | 22 | const planKeys = Object.keys(planConfig); 23 | if (planKeys.length !== 0) { 24 | for (let i = 0; i < planKeys.length; i++) { 25 | const planPath = planKeys[i]; 26 | const ruleList = planConfig[planPath]; 27 | 28 | for (let j = ruleList.length - 1; j >= 0; j--) { 29 | const filename = ruleList[j]; 30 | yield createFile(filename, planPath, force); 31 | } 32 | } 33 | } 34 | }); 35 | 36 | return function createPlan() { 37 | return _ref.apply(this, arguments); 38 | }; 39 | })(); 40 | 41 | /** 42 | * 文件名命名规则 43 | * 最终产生规则文件 44 | * @param {String} felintDirPath .felint路径 45 | * @param {String} fileName 文件名 46 | * @param {Boolean} force 是否强制更新文件 47 | */ 48 | 49 | 50 | let createFile = (() => { 51 | var _ref2 = _asyncToGenerator(function* (fileName, targetFolder, force) { 52 | const felintDirPath = felintConfig.felintDirPath(); 53 | 54 | if (felintDirPath && fileName) { 55 | const ext = fileUtil.getFileExtension(fileName).toLowerCase(); 56 | const fileNE = fileName.slice(0, ext.length ? -ext.length - 1 : fileName.length); 57 | 58 | // 需要生成的目录 59 | const targetFilePath = targetFolder ? path.resolve(process.cwd(), targetFolder) : process.cwd(); 60 | 61 | // 判断目标目录路径是否存在 62 | if (!fileUtil.has(targetFilePath)) return; 63 | 64 | // 生成的文件路径 65 | let targetFileName = `${targetFilePath}/${fileNE.split('_')[0]}${ext ? `.${ext}` : ''}`; 66 | 67 | if (fileNE.indexOf('stylelint') > -1) { 68 | targetFileName = `${targetFilePath}/${fileNE.split('_')[0]}.json`; 69 | } 70 | 71 | const override = force || (yield fileUtil.checkOverride(targetFileName)); 72 | const sourceFilePath = `${felintDirPath.path}/rules/${fileName}`; 73 | 74 | // 覆盖文件 75 | if (override) { 76 | if (ext === 'json') { 77 | yield createJsonFile(targetFileName, sourceFilePath, ext); 78 | } else { 79 | sh.cp(sourceFilePath, targetFileName); 80 | } 81 | console.log(`你在目录${targetFilePath}已创建${fileName}规则`.green); 82 | } 83 | } 84 | }); 85 | 86 | return function createFile(_x, _x2, _x3) { 87 | return _ref2.apply(this, arguments); 88 | }; 89 | })(); 90 | 91 | let createJsonFile = (() => { 92 | var _ref3 = _asyncToGenerator(function* (targetFileName, sourceFilePath, ext) { 93 | const targetFileContent = fileUtil.readFile(targetFileName, ext); 94 | let fileContent = ''; 95 | 96 | if (ext === 'json') { 97 | fileContent = yield fileUtil.readFile(sourceFilePath, ext); 98 | fileContent = mergeObject(fileContent, targetFileContent); 99 | 100 | fileUtil.createFileSync(targetFileName, JSON.stringify(fileContent || {}, null, 2), ext); 101 | } 102 | }); 103 | 104 | return function createJsonFile(_x4, _x5, _x6) { 105 | return _ref3.apply(this, arguments); 106 | }; 107 | })(); 108 | 109 | /** 110 | * 创建ignore文件 111 | */ 112 | 113 | 114 | let createIgnore = (() => { 115 | var _ref4 = _asyncToGenerator(function* () { 116 | const felintDirPath = felintConfig.felintDirPath(); 117 | 118 | if (felintDirPath && felintDirPath.path) { 119 | // 查看.felint下有无.eslintignore文件 120 | const hasEslintIgnoreFile = fileUtil.has(`${felintDirPath.path}/.eslintignore`); 121 | 122 | if (hasEslintIgnoreFile) { 123 | const override = yield fileUtil.checkOverride(`${process.cwd()}/.eslintignore`); 124 | 125 | if (override) { 126 | sh.cp(`${felintDirPath.path}/.eslintignore`, `${process.cwd()}/.eslintignore`); 127 | } 128 | } 129 | 130 | // 查看.felint下有无.stylelintignore文件 131 | const hasStylelintIgnoreFile = fileUtil.has(`${felintDirPath.path}/.stylelintignore`); 132 | 133 | if (hasStylelintIgnoreFile) { 134 | const override = yield fileUtil.checkOverride(`${process.cwd()}/.stylelintignore`); 135 | 136 | if (override) { 137 | sh.cp(`${felintDirPath.path}/.stylelintignore`, `${process.cwd()}/.stylelintignore`); 138 | } 139 | } 140 | } 141 | }); 142 | 143 | return function createIgnore() { 144 | return _ref4.apply(this, arguments); 145 | }; 146 | })(); 147 | 148 | function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } 149 | 150 | const path = require('path'); 151 | const sh = require('shelljs'); 152 | const fileUtil = require('./utils/fileUtil.js'); 153 | const felintConfig = require('./felintConfig.js'); 154 | 155 | /** 156 | * 合并两个对象 157 | * @param {Object} target 158 | * @param {Object} another 159 | */ 160 | function mergeObject(target, another) { 161 | if (target && another) { 162 | Object.keys(target).concat(Object.keys(another)).forEach(key => { 163 | if (target[key] === undefined) { 164 | target[key] = another[key]; 165 | } else { 166 | if (toString.call(target[key]) === '[object Object]') { 167 | Object.assign(target[key], another[key] || {}); 168 | } else { 169 | target[key] = another[key] === undefined ? target[key] : another[key]; 170 | } 171 | } 172 | }); 173 | } 174 | return target; 175 | } 176 | 177 | module.exports = { 178 | createPlan, 179 | createIgnore 180 | }; -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | felint 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | 17 | youzan 18 | 19 | 20 | github 21 | 22 | 23 |
24 |
25 | 26 |
27 |
A smart way to eslint and stylelint for front end
28 | 立即使用 29 |
30 | 31 |

A smart way to eslint stylelint and git hooks for front end

32 | 33 |

npm version downloads PRs Welcome

34 | 35 |

NPM

36 | 37 |

一、什么是Felint

38 | 39 |

felint 是一个同步前端代码检查规则的工具。

40 | 41 |

felint 为你的项目做以下两件事:

42 | 43 |
    44 |
  1. 初始化 eslint/stylelint 配置文件,无论是 react 项目、vue 项目、es5 还是 es6 都提供了针对性的配置方案
  2. 45 | 46 |
  3. 安装 eslint/stylelint 及其依赖到当前项目的 node_modules 里
  4. 47 |
48 | 49 |

如果你需要利用git的钩子来校验代码,推荐使用:huskylint-staged

50 | 51 |

二、安装 felint

52 | 53 |
npm install -g felint
 54 | 
55 | 56 |

三、快速开始

57 | 58 |

第一步

59 | 60 |

在项目的根目录,执行

61 | 62 |
felint init
 63 | 
64 | 65 |

felint 初始化完成后你的项目中将会产生如下目录和文件:

66 | 67 |
|_.felintrc        // 第一次被执行 felint init 后会在项目根目录生成这个文件,里面包含了使用哪个配置方案等信息
 68 | |_.felint          // felint-config文件夹
 69 | |_.eslintrc.json   // eslint 规则文件,用于检测js代码(使用的是官方推荐的配置)
 70 | |_.eslintignore    // eslint ignore配置文件
 71 | |_.stylelintrc.js  // stylelint 规则文件,用于检测css代码(使用的是官方推荐的配置)
 72 | |_.stylelintignore // stylelint ignore配置文件
 73 | 
74 | 75 |

同时,当你在运行 git commit 时自动检测待提交的文件是否符合相应规范。如无法通过校验,将无法提交。

76 | 77 |

当你需要再一个项目的不同目录使用不同的代码规范时,我们可以通过 .felintrc 自定义校验规则:

78 | 79 |
{
 80 |     "plan": {
 81 |         "./app": "node",
 82 |         "./client": "vue"
 83 |     }
 84 | }
 85 | 
86 | 87 |

此时在 ./app 目录生成 node 相关的校验规则,在 ./client 目录会生成 vue 相关的校验规则。

88 | 89 |

第二步

90 | 91 |

将这些新增的代码提交到 git 仓库

92 | 93 |

四、名词解释

94 | 95 | 104 | 105 |

五、felint命令详解

106 | 107 |

1. felint init

108 | 109 |
felint init -p planname -f force
110 | 
111 | planname:
112 | 用于指定初始化规则方案
113 | force:
114 | 是否强制覆盖已有的规则
115 | 
116 | 117 |

执行 felint init 命令后,felint将从 .felintrc 中读取 felint-config git仓库地址 或 使用默认地址https://github.com/youzan/felint-config(如没有.felintrc文件)下载所需的默认的配置文件并保存在项目的 .felint 文件夹下。

118 | 119 |

当配置文件下载完成后,felint 将自动执行配置文件内部的初始化脚本文件,并生成最终规则文件。

120 | 121 |

关于规则方案声明请参见felint-config介绍

122 | 123 |

2. felint dep

124 | 125 |

该命令会下载 eslintstylelint 需要的依赖,并写入到 package.json 中。

126 | 127 |
felint dep
128 | 
129 | 130 |

3. felint rules

131 | 132 |

该命令会先将最新的 felint-config 下载到本地,然后依据 .felintrc 里配置的 plan 规则生成对应的规则文件。

133 | 134 |
felint rule -f force
135 | 
136 | force:
137 | 是否强制覆盖已有的规则
138 | 
139 | 140 |

4. felint config-url

141 | 142 |
felint config-url
143 | 
144 | 145 |

输出 felint config 配置的仓库地址。

146 | 147 |

六、.felintrc文件

148 | 149 |

.felintrc用于配置felint-config的git仓库地址、对默认规则进行一定程度的自定义覆盖以及记录该项目所使用的代码规则方案。

150 | 151 |

e.g.

152 | 153 |
{
154 |     gitHookUrl   // 用于指定使用的felint-config仓库地址
155 |     plan        // 用于指定当前项目所使用的规则方案,比如es5/es6/vue/react等
156 | }
157 | 
158 | 159 |

1. gitHookUrl

160 | 161 |

如果你不想使用我们默认的felint-config校验,你可以fork出来修改为自己的felint-config(修改方法参考 felint-config 的 readme ),然后在 .felintrc 文件的 gitHookUrl 字段中手动配置你自己的 felint-config 仓库地址。

162 | 163 |

然后重新执行一次 felint init 即可。

164 | 165 |

2. plan

166 | 167 |

该字段用于记录执行felint init -p value时所使用的规范方案(如果不指定则为default)。

168 | 169 |

七、felint升级

170 | 171 |

felint将在你执行felint init命令的时候自动检查更新。当发现有新版本felint时,将在命令行提醒你是否需要更新。

172 | 173 |

八、开源协议

174 | 175 |

本项目基于 MIT协议,请自由地享受和参与开源。

176 | 177 | 178 | 191 | 192 | 193 | 194 | 195 | -------------------------------------------------------------------------------- /.felint/rules/.eslintrc_es5.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "node": true, 5 | "browser": true, 6 | "es6": false, 7 | "mocha": true, 8 | "commonjs": true, 9 | "amd": true 10 | }, 11 | 12 | "plugins": [ 13 | "react" 14 | ], 15 | 16 | "extends": ["plugin:react/recommended"], 17 | 18 | "parserOptions": { 19 | "ecmaVersion": 5, 20 | "ecmaFeatures": { 21 | "jsx": true, 22 | "globalReturn": true, 23 | "experimentalObjectRestSpread": true 24 | } 25 | }, 26 | 27 | "rules": { 28 | "no-var": 0, 29 | "comma-dangle": [2, "never"], 30 | "no-cond-assign": 0, 31 | "no-console": 0, 32 | "no-constant-condition": 0, 33 | "no-control-regex": 0, 34 | "no-debugger": 2, 35 | "no-dupe-args": 2, 36 | "no-dupe-keys": 2, 37 | "no-duplicate-case": 2, 38 | "no-empty-character-class": 1, 39 | "no-empty": 0, 40 | "no-ex-assign": 1, 41 | "no-extra-boolean-cast": 1, 42 | "no-extra-parens": [1, "functions"], 43 | "no-extra-semi": 1, 44 | "no-func-assign": 2, 45 | "no-inner-declarations": 0, 46 | "no-invalid-regexp": 1, 47 | "no-irregular-whitespace": 2, 48 | "no-negated-in-lhs": 2, 49 | "no-obj-calls": 2, 50 | "no-regex-spaces": 1, 51 | "no-reserved-keys": 0, 52 | "no-sparse-arrays": 2, 53 | "no-unexpected-multiline": 0, 54 | "no-unreachable": 2, 55 | "use-isnan": 2, 56 | "valid-jsdoc": 0, 57 | "valid-typeof": 2, 58 | 59 | "accessor-pairs": [1, { "setWithoutGet": true }], 60 | "block-scoped-var": 0, 61 | "complexity": 0, 62 | "consistent-return": 0, 63 | "curly": [2, "multi-line"], 64 | "default-case": 0, 65 | "dot-notation": 0, 66 | "dot-location": 0, 67 | "eqeqeq": 1, 68 | "guard-for-in": 0, 69 | "no-alert": 0, 70 | "no-caller": 2, 71 | "no-div-regex": 0, 72 | "no-else-return": 0, 73 | "no-eq-null": 0, 74 | "no-eval": 2, 75 | "no-extend-native": 1, 76 | "no-extra-bind": 1, 77 | "no-fallthrough": 1, 78 | "no-floating-decimal": 2, 79 | "no-implicit-coercion": 0, 80 | "no-implied-eval": 2, 81 | "no-invalid-this": 0, 82 | "no-iterator": 0, 83 | "no-labels": [2, { "allowLoop": true, "allowSwitch": true }], 84 | "no-lone-blocks": 1, 85 | "no-loop-func": 0, 86 | "no-magic-numbers": 0, 87 | "no-multi-spaces": 0, 88 | "no-multi-str": 0, 89 | "no-native-reassign": [2, { "exceptions": ["Map", "Set"] }], 90 | "no-new-func": 2, 91 | "no-new-wrappers": 1, 92 | "no-new": 0, 93 | "no-octal-escape": 1, 94 | "no-octal": 1, 95 | "no-param-reassign": 0, 96 | "no-process-env": 0, 97 | "no-proto": 2, 98 | "no-redeclare": 0, 99 | "no-return-assign": 0, 100 | "no-script-url": 0, 101 | "no-self-compare": 1, 102 | "no-sequences": 1, 103 | "no-throw-literal": 0, 104 | "no-unused-expressions": 0, 105 | "no-useless-call": 1, 106 | "no-useless-concat": 1, 107 | "no-void": 0, 108 | "no-warning-comments": 0, 109 | "no-with": 2, 110 | "radix": 2, 111 | "vars-on-top": 0, 112 | "wrap-iife": 0, 113 | "yoda": 0, 114 | 115 | "strict": 0, 116 | 117 | "init-declarations": 0, 118 | "no-catch-shadow": 0, 119 | "no-delete-var": 2, 120 | "no-label-var": 1, 121 | "no-shadow-restricted-names": 2, 122 | "no-shadow": 0, 123 | "no-undef-init": 0, 124 | "no-undef": 2, 125 | "no-undefined": 0, 126 | "no-unused-vars": [1, { "args": "none" }], 127 | "no-use-before-define": 0, 128 | 129 | "callback-return": 0, 130 | "global-require": 0, 131 | "handle-callback-err": 0, 132 | "no-mixed-requires": 0, 133 | "no-new-require": 0, 134 | "no-path-concat": 0, 135 | "no-process-exit": 0, 136 | "no-restricted-modules": 0, 137 | "no-sync": 0, 138 | 139 | "array-bracket-spacing": 2, 140 | "block-spacing": 0, 141 | "brace-style": [1, "1tbs", { "allowSingleLine": true }], 142 | "camelcase": [1, { "properties": "never" }], 143 | "comma-spacing": [1, { "before": false, "after": true }], 144 | "comma-style": [1, "last"], 145 | "computed-property-spacing": [1, "never"], 146 | "consistent-this": [1, "_this"], 147 | "eol-last": 0, 148 | "func-names": 0, 149 | "func-style": [0, "declaration"], 150 | "id-length": 0, 151 | "id-match": 0, 152 | "indent": [1, 4, { "SwitchCase": 1 }], 153 | "jsx-quotes": 0, 154 | "key-spacing": [1, { "beforeColon": false, "afterColon": true }], 155 | "linebreak-style": [0, "unix"], 156 | "lines-around-comment": 0, 157 | "max-nested-callbacks": 0, 158 | "new-cap": 1, 159 | "new-parens": 2, 160 | "newline-after-var": 0, 161 | "no-array-constructor": 2, 162 | "no-continue": 0, 163 | "no-inline-comments": 1, 164 | "no-lonely-if": 0, 165 | "no-mixed-spaces-and-tabs": 2, 166 | "no-multiple-empty-lines": [1, { "max": 2 }], 167 | "no-negated-condition": 0, 168 | "no-nested-ternary": 0, 169 | "no-new-object": 2, 170 | "no-restricted-syntax": 0, 171 | "no-spaced-func": 2, 172 | "no-ternary": 0, 173 | "no-trailing-spaces": [1, { "skipBlankLines": true }], 174 | "no-underscore-dangle": 0, 175 | "no-unneeded-ternary": 1, 176 | "object-curly-spacing": 0, 177 | "one-var": 0, 178 | "operator-assignment": [1, "always"], 179 | "operator-linebreak": 0, 180 | "padded-blocks": 0, 181 | "quote-props": [0, "as-needed"], 182 | "quotes": [1, "single", "avoid-escape"], 183 | "require-jsdoc": 0, 184 | "semi-spacing": [1, { "before": false, "after": true }], 185 | "semi": [1, "always", { "omitLastInOneLineBlock": true }], 186 | "sort-vars": 0, 187 | "space-before-blocks": [1, "always"], 188 | "space-before-function-paren": [1, { "anonymous": "never", "named": "never" }], 189 | "keyword-spacing": [1, { "before": true, "after": true }], 190 | "space-in-parens": [1, "never"], 191 | "space-infix-ops": [1, { "int32Hint": false }], 192 | "space-unary-ops": [1, { "words": true, "nonwords": false }], 193 | "spaced-comment": [1, "always"], 194 | "wrap-regex": 0, 195 | "max-len": [1, 180, 4], 196 | "arrow-spacing": [1, { "before": true, "after": true }], 197 | "no-const-assign": 2, 198 | "no-this-before-super": 2, 199 | "react/jsx-uses-vars": 2, 200 | "react/jsx-uses-react": 2, 201 | "chonsistent-this": 0 202 | } 203 | } 204 | -------------------------------------------------------------------------------- /docs/assets/felint-main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Helvetica, arial, sans-serif; 3 | font-size: 14px; 4 | line-height: 1.6; 5 | padding-top: 10px; 6 | padding-bottom: 10px; 7 | background-color: white; 8 | padding: 30px 9 | } 10 | 11 | body>*:first-child { 12 | margin-top: 0!important 13 | } 14 | 15 | body>*:last-child { 16 | margin-bottom: 0!important 17 | } 18 | 19 | a { 20 | color: #4183c4 21 | } 22 | 23 | a.absent { 24 | color: #c00 25 | } 26 | 27 | a.anchor { 28 | display: block; 29 | padding-left: 30px; 30 | margin-left: -30px; 31 | cursor: pointer; 32 | position: absolute; 33 | top: 0; 34 | left: 0; 35 | bottom: 0 36 | } 37 | 38 | h1, 39 | h2, 40 | h3, 41 | h4, 42 | h5, 43 | h6 { 44 | margin: 20px 0 10px; 45 | padding: 0; 46 | font-weight: bold; 47 | -webkit-font-smoothing: antialiased; 48 | cursor: text; 49 | position: relative 50 | } 51 | 52 | h1:hover a.anchor, 53 | h2:hover a.anchor, 54 | h3:hover a.anchor, 55 | h4:hover a.anchor, 56 | h5:hover a.anchor, 57 | h6:hover a.anchor { 58 | background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA09pVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoMTMuMCAyMDEyMDMwNS5tLjQxNSAyMDEyLzAzLzA1OjIxOjAwOjAwKSAgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OUM2NjlDQjI4ODBGMTFFMTg1ODlEODNERDJBRjUwQTQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OUM2NjlDQjM4ODBGMTFFMTg1ODlEODNERDJBRjUwQTQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5QzY2OUNCMDg4MEYxMUUxODU4OUQ4M0REMkFGNTBBNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5QzY2OUNCMTg4MEYxMUUxODU4OUQ4M0REMkFGNTBBNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PsQhXeAAAABfSURBVHjaYvz//z8DJYCRUgMYQAbAMBQIAvEqkBQWXI6sHqwHiwG70TTBxGaiWwjCTGgOUgJiF1J8wMRAIUA34B4Q76HUBelAfJYSA0CuMIEaRP8wGIkGMA54bgQIMACAmkXJi0hKJQAAAABJRU5ErkJggg==) no-repeat 10px center; 59 | text-decoration: none 60 | } 61 | 62 | h1 tt, 63 | h1 code { 64 | font-size: inherit 65 | } 66 | 67 | h2 tt, 68 | h2 code { 69 | font-size: inherit 70 | } 71 | 72 | h3 tt, 73 | h3 code { 74 | font-size: inherit 75 | } 76 | 77 | h4 tt, 78 | h4 code { 79 | font-size: inherit 80 | } 81 | 82 | h5 tt, 83 | h5 code { 84 | font-size: inherit 85 | } 86 | 87 | h6 tt, 88 | h6 code { 89 | font-size: inherit 90 | } 91 | 92 | h1 { 93 | font-size: 28px; 94 | color: black 95 | } 96 | 97 | h2 { 98 | font-size: 24px; 99 | border-bottom: 1px solid #ccc; 100 | color: black 101 | } 102 | 103 | h3 { 104 | font-size: 18px 105 | } 106 | 107 | h4 { 108 | font-size: 16px 109 | } 110 | 111 | h5 { 112 | font-size: 14px 113 | } 114 | 115 | h6 { 116 | color: #777; 117 | font-size: 14px 118 | } 119 | 120 | p, 121 | blockquote, 122 | ul, 123 | ol, 124 | dl, 125 | li, 126 | table, 127 | pre { 128 | margin: 15px 0 129 | } 130 | 131 | hr { 132 | background: transparent url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAYAAAAECAYAAACtBE5DAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyJpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBNYWNpbnRvc2giIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OENDRjNBN0E2NTZBMTFFMEI3QjRBODM4NzJDMjlGNDgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OENDRjNBN0I2NTZBMTFFMEI3QjRBODM4NzJDMjlGNDgiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo4Q0NGM0E3ODY1NkExMUUwQjdCNEE4Mzg3MkMyOUY0OCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo4Q0NGM0E3OTY1NkExMUUwQjdCNEE4Mzg3MkMyOUY0OCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PqqezsUAAAAfSURBVHjaYmRABcYwBiM2QSA4y4hNEKYDQxAEAAIMAHNGAzhkPOlYAAAAAElFTkSuQmCC) repeat-x 0 0; 133 | border: 0 none; 134 | color: #ccc; 135 | height: 4px; 136 | padding: 0 137 | } 138 | 139 | body>h2:first-child { 140 | margin-top: 0; 141 | padding-top: 0 142 | } 143 | 144 | body>h1:first-child { 145 | margin-top: 0; 146 | padding-top: 0 147 | } 148 | 149 | body>h1:first-child+h2 { 150 | margin-top: 0; 151 | padding-top: 0 152 | } 153 | 154 | body>h3:first-child, 155 | body>h4:first-child, 156 | body>h5:first-child, 157 | body>h6:first-child { 158 | margin-top: 0; 159 | padding-top: 0 160 | } 161 | 162 | a:first-child h1, 163 | a:first-child h2, 164 | a:first-child h3, 165 | a:first-child h4, 166 | a:first-child h5, 167 | a:first-child h6 { 168 | margin-top: 0; 169 | padding-top: 0 170 | } 171 | 172 | h1 p, 173 | h2 p, 174 | h3 p, 175 | h4 p, 176 | h5 p, 177 | h6 p { 178 | margin-top: 0 179 | } 180 | 181 | li p.first { 182 | display: inline-block 183 | } 184 | 185 | li { 186 | margin: 0 187 | } 188 | 189 | ul, 190 | ol { 191 | padding-left: 30px 192 | } 193 | 194 | ul:first-child, 195 | ol:first-child { 196 | margin-top: 0 197 | } 198 | 199 | dl { 200 | padding: 0 201 | } 202 | 203 | dl dt { 204 | font-size: 14px; 205 | font-weight: bold; 206 | font-style: italic; 207 | padding: 0; 208 | margin: 15px 0 5px 209 | } 210 | 211 | dl dt:first-child { 212 | padding: 0 213 | } 214 | 215 | dl dt>:first-child { 216 | margin-top: 0 217 | } 218 | 219 | dl dt>:last-child { 220 | margin-bottom: 0 221 | } 222 | 223 | dl dd { 224 | margin: 0 0 15px; 225 | padding: 0 15px 226 | } 227 | 228 | dl dd>:first-child { 229 | margin-top: 0 230 | } 231 | 232 | dl dd>:last-child { 233 | margin-bottom: 0 234 | } 235 | 236 | blockquote { 237 | border-left: 4px solid #ddd; 238 | padding: 0 15px; 239 | color: #777 240 | } 241 | 242 | blockquote>:first-child { 243 | margin-top: 0 244 | } 245 | 246 | blockquote>:last-child { 247 | margin-bottom: 0 248 | } 249 | 250 | table { 251 | padding: 0; 252 | border-collapse: collapse 253 | } 254 | 255 | table tr { 256 | border-top: 1px solid #ccc; 257 | background-color: white; 258 | margin: 0; 259 | padding: 0 260 | } 261 | 262 | table tr:nth-child(2n) { 263 | background-color: #f8f8f8 264 | } 265 | 266 | table tr th { 267 | font-weight: bold; 268 | border: 1px solid #ccc; 269 | margin: 0; 270 | padding: 6px 13px 271 | } 272 | 273 | table tr td { 274 | border: 1px solid #ccc; 275 | margin: 0; 276 | padding: 6px 13px 277 | } 278 | 279 | table tr th:first-child, 280 | table tr td:first-child { 281 | margin-top: 0 282 | } 283 | 284 | table tr th:last-child, 285 | table tr td:last-child { 286 | margin-bottom: 0 287 | } 288 | 289 | img { 290 | max-width: 100% 291 | } 292 | 293 | span.frame { 294 | display: block; 295 | overflow: hidden 296 | } 297 | 298 | span.frame>span { 299 | border: 1px solid #ddd; 300 | display: block; 301 | float: left; 302 | overflow: hidden; 303 | margin: 13px 0 0; 304 | padding: 7px; 305 | width: auto 306 | } 307 | 308 | span.frame span img { 309 | display: block; 310 | float: left 311 | } 312 | 313 | span.frame span span { 314 | clear: both; 315 | color: #333; 316 | display: block; 317 | padding: 5px 0 0 318 | } 319 | 320 | span.align-center { 321 | display: block; 322 | overflow: hidden; 323 | clear: both 324 | } 325 | 326 | span.align-center>span { 327 | display: block; 328 | overflow: hidden; 329 | margin: 13px auto 0; 330 | text-align: center 331 | } 332 | 333 | span.align-center span img { 334 | margin: 0 auto; 335 | text-align: center 336 | } 337 | 338 | span.align-right { 339 | display: block; 340 | overflow: hidden; 341 | clear: both 342 | } 343 | 344 | span.align-right>span { 345 | display: block; 346 | overflow: hidden; 347 | margin: 13px 0 0; 348 | text-align: right 349 | } 350 | 351 | span.align-right span img { 352 | margin: 0; 353 | text-align: right 354 | } 355 | 356 | span.float-left { 357 | display: block; 358 | margin-right: 13px; 359 | overflow: hidden; 360 | float: left 361 | } 362 | 363 | span.float-left span { 364 | margin: 13px 0 0 365 | } 366 | 367 | span.float-right { 368 | display: block; 369 | margin-left: 13px; 370 | overflow: hidden; 371 | float: right 372 | } 373 | 374 | span.float-right>span { 375 | display: block; 376 | overflow: hidden; 377 | margin: 13px auto 0; 378 | text-align: right 379 | } 380 | 381 | code, 382 | tt { 383 | margin: 0 2px; 384 | padding: 0 5px; 385 | white-space: nowrap; 386 | border: 1px solid #eaeaea; 387 | background-color: #f8f8f8; 388 | border-radius: 3px 389 | } 390 | 391 | pre code { 392 | margin: 0; 393 | padding: 0; 394 | white-space: pre; 395 | border: 0; 396 | background: transparent 397 | } 398 | 399 | .highlight pre { 400 | background-color: #f8f8f8; 401 | border: 1px solid #ccc; 402 | font-size: 13px; 403 | line-height: 19px; 404 | overflow: auto; 405 | padding: 6px 10px; 406 | border-radius: 3px 407 | } 408 | 409 | pre { 410 | background-color: #f8f8f8; 411 | border: 1px solid #ccc; 412 | font-size: 13px; 413 | line-height: 19px; 414 | overflow: auto; 415 | padding: 6px 10px; 416 | border-radius: 3px 417 | } 418 | 419 | pre code, 420 | pre tt { 421 | background-color: transparent; 422 | border: 0 423 | } 424 | 425 | sup { 426 | font-size: .83em; 427 | vertical-align: super; 428 | line-height: 0 429 | } 430 | 431 | kbd { 432 | display: inline-block; 433 | padding: 3px 5px; 434 | font-size: 11px; 435 | line-height: 10px; 436 | color: #555; 437 | vertical-align: middle; 438 | background-color: #fcfcfc; 439 | border: solid 1px #ccc; 440 | border-bottom-color: #bbb; 441 | border-radius: 3px; 442 | box-shadow: inset 0 -1px 0 #bbb 443 | } 444 | 445 | * { 446 | -webkit-print-color-adjust: exact 447 | } 448 | 449 | @media screen and (min-width:914px) { 450 | body { 451 | width: 854px; 452 | margin: 0 auto 453 | } 454 | } 455 | 456 | @media print { 457 | table, 458 | pre { 459 | page-break-inside: avoid 460 | } 461 | pre { 462 | word-wrap: break-word 463 | } 464 | } 465 | 466 | .header { 467 | position: relative; 468 | margin-top: 10px; 469 | text-align: center; 470 | font-family: 'PingFangSC-Regular' 471 | } 472 | 473 | .header section:first-child { 474 | overflow: hidden 475 | } 476 | 477 | img[alt="youzan"] { 478 | width: 36px; 479 | float: left 480 | } 481 | 482 | img[alt="github"] { 483 | width: 36px; 484 | float: right 485 | } 486 | 487 | img.logo { 488 | width: 226px 489 | } 490 | 491 | .header .logo-wrapper { 492 | text-align: center; 493 | margin-top: 5px 494 | } 495 | 496 | .header .slogan { 497 | font-family: '.SFNSDisplay-Regular'; 498 | color: #666; 499 | font-size: 20px; 500 | margin-top: 40px 501 | } 502 | 503 | .header .button { 504 | display: block; 505 | width: 129px; 506 | line-height: 36px; 507 | border: 2px solid #484848; 508 | border-radius: 20px; 509 | text-align: center; 510 | color: #484848; 511 | margin: 80px auto; 512 | text-decoration: none 513 | } 514 | 515 | .red-ball { 516 | width: 10px; 517 | height: 10px; 518 | border-radius: 50%; 519 | background-color: #fc0002; 520 | z-index: 10; 521 | margin: 0; 522 | padding: 0; 523 | line-height: 1; 524 | position: absolute; 525 | top: -45px; 526 | left: 333px 527 | } 528 | 529 | @media only screen and (min-width:914px) { 530 | .red-ball { 531 | animation-name: red-ball-keyframes; 532 | animation-duration: 3500ms; 533 | animation-delay: 0ms; 534 | animation-fill-mode: forwards; 535 | animation-timing-function: linear; 536 | animation-iteration-count: 1; 537 | animation-play-state: paused; 538 | transform-origin: 0 0 539 | } 540 | @keyframes red-ball-keyframes { 541 | 0% { 542 | transform: translate(0px, 0px) translate(-50%, -50%) 543 | } 544 | 0.95% { 545 | transform: translate(0px, 0.3556px) translate(-50%, -50%) 546 | } 547 | 1.9% { 548 | transform: translate(0px, 1.4222px) translate(-50%, -50%) 549 | } 550 | 2.86% { 551 | transform: translate(0px, 3.2px) translate(-50%, -50%) 552 | } 553 | 3.81% { 554 | transform: translate(0px, 5.6889px) translate(-50%, -50%) 555 | } 556 | 4.76% { 557 | transform: translate(0px, 8.8889px) translate(-50%, -50%) 558 | } 559 | 5.71% { 560 | transform: translate(0px, 12.8px) translate(-50%, -50%) 561 | } 562 | 6.67% { 563 | transform: translate(0px, 17.4222px) translate(-50%, -50%) 564 | } 565 | 7.62% { 566 | transform: translate(0px, 22.7556px) translate(-50%, -50%) 567 | } 568 | 8.57% { 569 | transform: translate(0px, 28.8px) translate(-50%, -50%) 570 | } 571 | 9.52% { 572 | transform: translate(0px, 35.5556px) translate(-50%, -50%) 573 | } 574 | 10.48% { 575 | transform: translate(0px, 43.0222px) translate(-50%, -50%) 576 | } 577 | 11.43% { 578 | transform: translate(0px, 51.2px) translate(-50%, -50%) 579 | } 580 | 12.38% { 581 | transform: translate(0px, 60.0889px) translate(-50%, -50%) 582 | } 583 | 13.33% { 584 | transform: translate(0px, 69.6889px) translate(-50%, -50%) 585 | } 586 | 14.29% { 587 | transform: translate(0px, 80px) translate(-50%, -50%) 588 | } 589 | 15.24% { 590 | transform: translate(3px, 72.6528px) translate(-50%, -50%) 591 | } 592 | 16.19% { 593 | transform: translate(6px, 65.9444px) translate(-50%, -50%) 594 | } 595 | 17.14% { 596 | transform: translate(9px, 59.875px) translate(-50%, -50%) 597 | } 598 | 18.1% { 599 | transform: translate(12px, 54.4444px) translate(-50%, -50%) 600 | } 601 | 19.05% { 602 | transform: translate(15px, 49.6528px) translate(-50%, -50%) 603 | } 604 | 20% { 605 | transform: translate(18px, 45.5px) translate(-50%, -50%) 606 | } 607 | 20.95% { 608 | transform: translate(21px, 41.9861px) translate(-50%, -50%) 609 | } 610 | 21.9% { 611 | transform: translate(24px, 39.1111px) translate(-50%, -50%) 612 | } 613 | 22.86% { 614 | transform: translate(27px, 36.875px) translate(-50%, -50%) 615 | } 616 | 23.81% { 617 | transform: translate(30px, 35.2778px) translate(-50%, -50%) 618 | } 619 | 24.76% { 620 | transform: translate(33px, 34.3194px) translate(-50%, -50%) 621 | } 622 | 25.71% { 623 | transform: translate(36px, 34px) translate(-50%, -50%) 624 | } 625 | 26.67% { 626 | transform: translate(39.3333px, 34.3042px) translate(-50%, -50%) 627 | } 628 | 27.62% { 629 | transform: translate(42.6667px, 35.2167px) translate(-50%, -50%) 630 | } 631 | 28.57% { 632 | transform: translate(46px, 36.7376px) translate(-50%, -50%) 633 | } 634 | 29.52% { 635 | transform: translate(49.3333px, 38.8669px) translate(-50%, -50%) 636 | } 637 | 30.48% { 638 | transform: translate(52.6667px, 41.6045px) translate(-50%, -50%) 639 | } 640 | 31.43% { 641 | transform: translate(56px, 44.9504px) translate(-50%, -50%) 642 | } 643 | 32.38% { 644 | transform: translate(59.3333px, 48.9047px) translate(-50%, -50%) 645 | } 646 | 33.33% { 647 | transform: translate(62.6667px, 53.4674px) translate(-50%, -50%) 648 | } 649 | 34.29% { 650 | transform: translate(66px, 58.6384px) translate(-50%, -50%) 651 | } 652 | 35.24% { 653 | transform: translate(69.3333px, 64.4178px) translate(-50%, -50%) 654 | } 655 | 36.19% { 656 | transform: translate(72.6667px, 70.8056px) translate(-50%, -50%) 657 | } 658 | 37.14% { 659 | transform: translate(76px, 77.8017px) translate(-50%, -50%) 660 | } 661 | 38.1% { 662 | transform: translate(79.3333px, 85.4061px) translate(-50%, -50%) 663 | } 664 | 39.05% { 665 | transform: translate(81.4667px, 81.5244px) translate(-50%, -50%) 666 | } 667 | 40% { 668 | transform: translate(83.3px, 75.78px) translate(-50%, -50%) 669 | } 670 | 40.95% { 671 | transform: translate(85.1333px, 71.2578px) translate(-50%, -50%) 672 | } 673 | 41.9% { 674 | transform: translate(86.9667px, 67.9578px) translate(-50%, -50%) 675 | } 676 | 42.86% { 677 | transform: translate(88.8px, 65.88px) translate(-50%, -50%) 678 | } 679 | 43.81% { 680 | transform: translate(90.6333px, 65.0244px) translate(-50%, -50%) 681 | } 682 | 44.76% { 683 | transform: translate(92.1294px, 65.283px) translate(-50%, -50%) 684 | } 685 | 45.71% { 686 | transform: translate(93.5412px, 66.4325px) translate(-50%, -50%) 687 | } 688 | 46.67% { 689 | transform: translate(94.9529px, 68.4664px) translate(-50%, -50%) 690 | } 691 | 47.62% { 692 | transform: translate(96.3647px, 71.3845px) translate(-50%, -50%) 693 | } 694 | 48.57% { 695 | transform: translate(97.7765px, 75.1869px) translate(-50%, -50%) 696 | } 697 | 49.52% { 698 | transform: translate(99.1882px, 79.8735px) translate(-50%, -50%) 699 | } 700 | 50.48% { 701 | transform: translate(100.6px, 85.4444px) translate(-50%, -50%) 702 | } 703 | 51.43% { 704 | transform: translate(102.0118px, 91.8997px) translate(-50%, -50%) 705 | } 706 | 52.38% { 707 | transform: translate(103.4235px, 99.2391px) translate(-50%, -50%) 708 | } 709 | 53.33% { 710 | transform: translate(104.8353px, 107.4629px) translate(-50%, -50%) 711 | } 712 | 54.29% { 713 | transform: translate(105.4px, 107.1111px) translate(-50%, -50%) 714 | } 715 | 55.24% { 716 | transform: translate(105.4px, 101.1235px) translate(-50%, -50%) 717 | } 718 | 56.19% { 719 | transform: translate(105.4px, 95.7531px) translate(-50%, -50%) 720 | } 721 | 57.14% { 722 | transform: translate(105.4px, 91px) translate(-50%, -50%) 723 | } 724 | 58.1% { 725 | transform: translate(105.4px, 86.8642px) translate(-50%, -50%) 726 | } 727 | 59.05% { 728 | transform: translate(105.4px, 83.3457px) translate(-50%, -50%) 729 | } 730 | 60% { 731 | transform: translate(105.4px, 80.4444px) translate(-50%, -50%) 732 | } 733 | 60.95% { 734 | transform: translate(105.4px, 78.1605px) translate(-50%, -50%) 735 | } 736 | 61.9% { 737 | transform: translate(105.4px, 76.4938px) translate(-50%, -50%) 738 | } 739 | 62.86% { 740 | transform: translate(105.4px, 75.4444px) translate(-50%, -50%) 741 | } 742 | 63.81% { 743 | transform: translate(105.4px, 75.0123px) translate(-50%, -50%) 744 | } 745 | 64.76% { 746 | transform: translate(105.4px, 75.1975px) translate(-50%, -50%) 747 | } 748 | 65.71% { 749 | transform: translate(105.4px, 76px) translate(-50%, -50%) 750 | } 751 | 66.67% { 752 | transform: translate(105.4px, 77.4198px) translate(-50%, -50%) 753 | } 754 | 67.62% { 755 | transform: translate(105.4px, 79.4568px) translate(-50%, -50%) 756 | } 757 | 68.57% { 758 | transform: translate(105.4px, 82.1111px) translate(-50%, -50%) 759 | } 760 | 69.52% { 761 | transform: translate(105.4px, 85.3827px) translate(-50%, -50%) 762 | } 763 | 70.48% { 764 | transform: translate(105.4px, 89.2716px) translate(-50%, -50%) 765 | } 766 | 71.43% { 767 | transform: translate(105.4px, 93.7778px) translate(-50%, -50%) 768 | } 769 | 72.38% { 770 | transform: translate(105.4px, 98.9012px) translate(-50%, -50%) 771 | } 772 | 73.33% { 773 | transform: translate(105.4px, 104.642px) translate(-50%, -50%) 774 | } 775 | 74.29% { 776 | transform: translate(105.4px, 111px) translate(-50%, -50%) 777 | } 778 | 75.24% { 779 | transform: translate(105.4px, 104.7607px) translate(-50%, -50%) 780 | } 781 | 76.19% { 782 | transform: translate(105.4px, 99.3761px) translate(-50%, -50%) 783 | } 784 | 77.14% { 785 | transform: translate(105.4px, 94.8462px) translate(-50%, -50%) 786 | } 787 | 78.1% { 788 | transform: translate(105.4px, 91.1709px) translate(-50%, -50%) 789 | } 790 | 79.05% { 791 | transform: translate(105.4px, 88.3504px) translate(-50%, -50%) 792 | } 793 | 80% { 794 | transform: translate(105.4px, 86.3846px) translate(-50%, -50%) 795 | } 796 | 80.95% { 797 | transform: translate(105.4px, 85.2735px) translate(-50%, -50%) 798 | } 799 | 81.9% { 800 | transform: translate(105.4px, 85.0115px) translate(-50%, -50%) 801 | } 802 | 82.86% { 803 | transform: translate(105.4px, 85.4136px) translate(-50%, -50%) 804 | } 805 | 83.81% { 806 | transform: translate(105.4px, 86.3901px) translate(-50%, -50%) 807 | } 808 | 84.76% { 809 | transform: translate(105.4px, 87.941px) translate(-50%, -50%) 810 | } 811 | 85.71% { 812 | transform: translate(105.4px, 90.0663px) translate(-50%, -50%) 813 | } 814 | 86.67% { 815 | transform: translate(105.4px, 92.766px) translate(-50%, -50%) 816 | } 817 | 87.62% { 818 | transform: translate(105.4px, 96.0401px) translate(-50%, -50%) 819 | } 820 | 88.57% { 821 | transform: translate(105.4px, 98.5762px) translate(-50%, -50%) 822 | } 823 | 89.52% { 824 | transform: translate(105.4px, 96.9835px) translate(-50%, -50%) 825 | } 826 | 90.48% { 827 | transform: translate(105.4px, 95.9652px) translate(-50%, -50%) 828 | } 829 | 91.43% { 830 | transform: translate(105.4px, 95.5214px) translate(-50%, -50%) 831 | } 832 | 92.38% { 833 | transform: translate(105.4px, 95.6519px) translate(-50%, -50%) 834 | } 835 | 93.33% { 836 | transform: translate(105.4px, 96.3569px) translate(-50%, -50%) 837 | } 838 | 94.29% { 839 | transform: translate(105.4px, 97.6362px) translate(-50%, -50%) 840 | } 841 | 95.24% { 842 | transform: translate(105.4px, 98.7791px) translate(-50%, -50%) 843 | } 844 | 96.19% { 845 | transform: translate(105.4px, 98.1994px) translate(-50%, -50%) 846 | } 847 | 97.14% { 848 | transform: translate(105.4px, 98.1942px) translate(-50%, -50%) 849 | } 850 | 98.1% { 851 | transform: translate(105.4px, 98.7634px) translate(-50%, -50%) 852 | } 853 | 99.05% { 854 | transform: translate(105.4px, 98.7859px) translate(-50%, -50%) 855 | } 856 | 100% { 857 | transform: translate(105.39999999999998px, 99px) translate(-50%, -50%) 858 | } 859 | } 860 | } 861 | 862 | @media only screen and (max-width:913px) { 863 | .red-ball { 864 | display: none 865 | } 866 | body { 867 | margin: 0; 868 | padding: 20px 869 | } 870 | .header { 871 | display: flex; 872 | flex-flow: column nowrap; 873 | justify-content: space-between 874 | } 875 | .header .slogan { 876 | margin-top: 40px 877 | } 878 | .header .button { 879 | margin-top: 0 880 | } 881 | } 882 | --------------------------------------------------------------------------------