├── .eslintrc ├── .gitignore ├── .jsdoc.json ├── .prettierrc ├── .travis.yml ├── LICENSE ├── README.md ├── core ├── app.js ├── ast.js ├── config.js ├── create.js ├── dependents.js ├── deps.js ├── element.js ├── files.js ├── handleCommand.js ├── logger.js ├── paths.js ├── plugin.js ├── refactor │ ├── array.js │ ├── cls.js │ ├── common.js │ ├── format.js │ ├── func.js │ ├── generate.js │ ├── identifier.js │ ├── importExport.js │ ├── index.js │ ├── lines.js │ ├── object.js │ ├── string.js │ ├── style.js │ └── updateRefs.js ├── repo.js ├── template.js ├── ua.js ├── utils.js └── vio.js ├── index.js ├── package-lock.json ├── package.json ├── plugins ├── common-core │ ├── app.js │ ├── index.js │ └── readme.md ├── rekit-plugin │ ├── app.js │ ├── appType.json │ ├── hooks.js │ ├── index.js │ └── logo.png └── rekit-react │ ├── action.js │ ├── app.js │ ├── appType.json │ ├── cli.js │ ├── colors.js │ ├── component.js │ ├── constant.js │ ├── entry.js │ ├── feature.js │ ├── hooks.js │ ├── index.js │ ├── logo.png │ ├── prefix.js │ ├── route.js │ ├── style.js │ ├── templates │ ├── Component.js.tpl │ ├── Component.less.tpl │ ├── Component.test.js.tpl │ ├── Component.test.jsx.tpl │ ├── ConnectedComponent.js.tpl │ ├── ConnectedComponent.test.js.tpl │ ├── FuncComponent.js.tpl │ ├── index.js.tpl │ ├── redux │ │ ├── action.js.tpl │ │ ├── action.test.js.tpl │ │ ├── action.test.ts.tpl │ │ ├── action.ts.tpl │ │ ├── actions.js.tpl │ │ ├── asyncAction.js.tpl │ │ ├── asyncAction.test.js.tpl │ │ ├── asyncAction.ts.tpl │ │ ├── constants.js.tpl │ │ ├── constants.ts.tpl │ │ ├── hooks.js.tpl │ │ ├── initialState.js.tpl │ │ ├── initialState.ts.tpl │ │ ├── reducer.js.tpl │ │ ├── reducer.test.js.tpl │ │ ├── reducer.test.ts.tpl │ │ └── reducer.ts.tpl │ ├── route.js.tpl │ ├── style.less.tpl │ ├── style.scss.tpl │ └── ts │ │ ├── Component.tsx.tpl │ │ ├── ConnectedComponent.tsx.tpl │ │ ├── ConnectedCopmonent.test.tsx.tpl │ │ ├── index.ts.tpl │ │ └── route.ts.tpl │ ├── test.js │ ├── tests │ ├── action.test.js │ ├── async-action.test.js │ ├── component.test.js │ ├── constant.test.js │ ├── entry.test.js │ ├── feature.test.js │ └── route.test.js │ ├── tools │ └── craToRekit.js │ └── utils.js ├── rekit.json ├── scripts ├── copyTo.js └── runTests.js └── tests ├── .eslintrc ├── before-all.js ├── deps.test.js ├── helpers.js ├── npm.js ├── paths.test.js ├── refactor ├── array.test.js ├── cls.test.js ├── common.test.js ├── format.test.js ├── generate.test.js ├── identifier.test.js ├── importExport.test.js ├── object.test.js ├── string.test.js └── style.test.js ├── rekit.js ├── template.test.js ├── test-prj ├── package.json ├── rekit.json └── src │ ├── common │ ├── configStore.js │ ├── rootReducer.js │ └── routeConfig.js │ └── styles │ └── index.less ├── utils.test.js └── vio.test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "rekit": "readonly" 4 | }, 5 | "extends": "node", 6 | "rules": { 7 | "import/no-nodejs-modules": 0, 8 | "import/no-commonjs": 0, 9 | "no-console": 0, 10 | "prefer-rest-params": 0, 11 | "dot-notation": 0, 12 | "no-sync": 0 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | .DS_Store 40 | .nyc_output 41 | coverage 42 | -------------------------------------------------------------------------------- /.jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "opts": { 3 | "destination": "docs", 4 | "recurse": true, 5 | "readme": "API.md", 6 | "template": "node_modules/docdash" 7 | }, 8 | "plugins": ["plugins/markdown"] 9 | } 10 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 100 5 | } 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" 4 | - "11" 5 | after_success: npm run codecov 6 | install: npm install 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Nate Wang 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # rekit-core 2 | The core functions of Rekit. 3 | 4 | [![Version](http://img.shields.io/npm/v/rekit-core.svg)](https://www.npmjs.org/package/rekit-core) 5 | [![Build Status](https://travis-ci.org/rekit/rekit-core.svg?branch=master)](https://travis-ci.org/rekit/rekit-core) 6 | [![Dependency Status](https://david-dm.org/rekit/rekit-core.svg?style=flat-square)](https://david-dm.org/rekit/rekit-core) 7 | [![Coverage Status](https://img.shields.io/codecov/c/github/rekit/rekit-core/master.svg)](https://codecov.io/github/rekit/rekit-core) 8 | [![MIT licensed](https://img.shields.io/badge/license-MIT-blue.svg)](./LICENSE) 9 | 10 | # Prerequisites 11 | node 10+ 12 | 13 | # LICENSE 14 | MIT 15 | -------------------------------------------------------------------------------- /core/app.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const _ = require('lodash'); 3 | const fs = require('fs-extra'); 4 | const files = require('./files'); 5 | const paths = require('./paths'); 6 | const plugin = require('./plugin'); 7 | const config = require('./config'); 8 | const repo = require('./repo'); 9 | const logger = require('./logger'); 10 | 11 | const app = {}; 12 | 13 | function getProjectData(args = {}) { 14 | const prjData = files.readDir(paths.getProjectRoot(), args); 15 | plugin.getPlugins('app.processProjectData').forEach(p => p.app.processProjectData(prjData, args)); 16 | return prjData; 17 | } 18 | 19 | function syncAppRegistryRepo() { 20 | const registryDir = paths.configPath('app-registry'); 21 | const appRegistry = config.getAppRegistry(); 22 | if (path.isAbsolute(appRegistry)) { 23 | // if it's local folder, copy to it 24 | console.log('Sync app registry from local folder.'); 25 | fs.removeSync(registryDir); 26 | fs.copySync(appRegistry, registryDir); 27 | return Promise.resolve(); 28 | } 29 | return repo.sync(appRegistry, registryDir); 30 | } 31 | 32 | function getAppTypes(args) { 33 | if (args && !args.noSync) syncAppRegistryRepo(); 34 | fs.ensureDirSync(paths.configPath('plugins')); 35 | const appTypes = ['rekit-react', 'rekit-plugin'] 36 | .map(dir => path.join(__dirname, '../plugins', dir)) 37 | .concat( 38 | fs 39 | .readdirSync(paths.configPath('plugins')) 40 | .map(f => paths.configPath(`plugins/${f}`)) 41 | .filter(f => fs.existsSync(path.join(f, 'appType.json'))), 42 | ) 43 | .map(f => { 44 | try { 45 | const obj = fs.readJsonSync(path.join(f, 'appType.json')); 46 | obj.logo = path.join(f, 'logo.png'); 47 | return obj; 48 | } catch (err) { 49 | logger.error(`Failed to load appType.json from: ${f}`, err); 50 | return null; 51 | } 52 | }) 53 | .filter(Boolean); 54 | 55 | const registryJson = paths.configPath('app-registry/appTypes.json'); 56 | if (fs.existsSync(registryJson)) { 57 | try { 58 | fs.readJsonSync(registryJson).forEach(appType => { 59 | appType.logo = paths.configPath(`app-registry/app-types/${appType.id}/logo.png`); 60 | const found = _.find(appTypes, { id: appType.id }); 61 | if (found) { 62 | Object.keys(found).forEach(key => delete found[key]); 63 | Object.assign(found, appType); 64 | } else appTypes.push(appType); 65 | }); 66 | } catch (err) { 67 | logger.info('Failed to read appTypes.json: ', err); 68 | } 69 | } 70 | // appTypes.forEach(appType => { 71 | // appType.logo = paths.configPath(`app-registry/app-types/${appType.id}/logo.png`); 72 | // }); 73 | // const appTypes = fs 74 | // .readJsonSync(paths.configPath('app-registry/appTypes.json')) 75 | // .filter(t => !t.disabled); 76 | // appTypes.forEach(appType => { 77 | // appType.logo = 'file://' + paths.configPath(`app-registry/app-types/${appType.id}/logo.png`); 78 | // }); 79 | return appTypes; 80 | } 81 | 82 | Object.assign(app, { 83 | getProjectData, 84 | getAppTypes, 85 | }); 86 | 87 | module.exports = app; 88 | -------------------------------------------------------------------------------- /core/ast.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const parser = require('@babel/parser'); 3 | const minimatch = require('minimatch'); 4 | const vio = require('./vio'); 5 | const logger = require('./logger'); 6 | const config = require('./config'); 7 | 8 | let cache = {}; 9 | const failedToParse = {}; 10 | 11 | function getAst(filePath, throwIfError) { 12 | const astFolders = config.getRekitConfig().astFolders || []; 13 | const excludeAstFolders = config.getRekitConfig().excludeAstFolders || []; 14 | 15 | if ( 16 | (astFolders.length && 17 | !astFolders.some(d => _.startsWith(filePath, d + '/') || minimatch(filePath, d))) || 18 | excludeAstFolders.some(d => _.startsWith(filePath, d + '/') || minimatch(filePath, d)) 19 | ) { 20 | return; 21 | } 22 | const checkAst = ast => { 23 | if (!ast && throwIfError) { 24 | throw new Error(`Failed to parse ast or file not exists, please check syntax: ${filePath}`); 25 | } 26 | }; 27 | 28 | if (!vio.fileExists(filePath)) { 29 | checkAst(null); 30 | return null; 31 | } 32 | 33 | const code = vio.getContent(filePath); 34 | 35 | if (!cache[filePath] || cache[filePath].code !== code) { 36 | try { 37 | const ast = parser.parse(code, { 38 | // parse in strict mode and allow module declarations 39 | sourceType: 'module', 40 | plugins: [ 41 | 'jsx', 42 | 'flow', 43 | 'doExpressions', 44 | 'objectRestSpread', 45 | 'decorators-legacy', 46 | 'classProperties', 47 | 'exportExtensions', 48 | 'asyncGenerators', 49 | 'functionBind', 50 | 'functionSent', 51 | 'dynamicImport', 52 | 'optionalChaining' 53 | ], 54 | }); 55 | 56 | checkAst(ast); 57 | 58 | if (!ast) { 59 | failedToParse[filePath] = true; 60 | logger.warn(`Failed to parse ast, please check syntax: ${filePath}`); 61 | return null; 62 | } 63 | delete failedToParse[filePath]; 64 | cache[filePath] = { ast, code }; 65 | ast._filePath = filePath; 66 | } catch (e) { 67 | console.log('parse ast failed: ', e); 68 | checkAst(null); 69 | failedToParse[filePath] = true; 70 | logger.warn(`Failed to parse ast, please check syntax: ${filePath}`); 71 | return null; 72 | } 73 | } 74 | return cache[filePath].ast; 75 | } 76 | 77 | function getFilesFailedToParse() { 78 | return failedToParse; 79 | } 80 | 81 | function clearCache(filePath) { 82 | if (filePath) delete cache[filePath]; 83 | else cache = {}; 84 | } 85 | 86 | module.exports = { 87 | getAst, 88 | clearCache, 89 | getFilesFailedToParse, 90 | }; 91 | -------------------------------------------------------------------------------- /core/config.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const path = require('path'); 3 | const paths = require('./paths'); 4 | const chokidar = require('chokidar'); 5 | const EventEmitter = require('events'); 6 | const _ = require('lodash'); 7 | const logger = require('./logger'); 8 | 9 | const config = new EventEmitter(); 10 | 11 | const debouncedEmit = _.debounce(evt => { 12 | config.emit(evt); 13 | }, 50); 14 | 15 | let appRegistry = 'rekit/app-registry'; 16 | let pluginRegistry = 'rekit/plugin-registry'; 17 | if (fs.existsSync(paths.configFile('config.json'))) { 18 | try { 19 | const rekitConfig = require(paths.configFile('config.json')); 20 | appRegistry = rekitConfig.appRegistry; 21 | pluginRegistry = rekitConfig.pluginRegistry; 22 | } catch (err) { 23 | // Do nothing if config.json broken 24 | logger.warn('Failed to load config.json, maybe there is some syntax error.'); 25 | } 26 | } 27 | 28 | let pkgJson = null; 29 | let pkgJsonWatcher = null; 30 | function getPkgJson(noCache, prjRoot) { 31 | const pkgJsonPath = prjRoot ? paths.join(prjRoot, 'package.json') : paths.map('package.json'); 32 | if (!fs.existsSync(pkgJsonPath)) return null; 33 | const refreshPkgJson = () => { 34 | try { 35 | pkgJson = fs.readJsonSync(pkgJsonPath); 36 | } catch (err) { 37 | logger.warn('Failed to load package.json, maybe there is some syntax error.', err); 38 | pkgJson = null; 39 | } 40 | }; 41 | if (noCache || !pkgJson) { 42 | refreshPkgJson(); 43 | } 44 | 45 | if (!pkgJsonWatcher && !global.__REKIT_NO_WATCH) { 46 | pkgJsonWatcher = chokidar.watch([pkgJsonPath], { persistent: true }); 47 | pkgJsonWatcher.on('all', () => { 48 | refreshPkgJson(); 49 | debouncedEmit('change'); 50 | }); 51 | } 52 | 53 | return pkgJson; 54 | } 55 | 56 | let rekitConfig = null; 57 | let rekitConfigWatcher = null; 58 | function getRekitConfig(noCache, prjRoot) { 59 | const rekitConfigFile = prjRoot ? paths.join(prjRoot, 'rekit.json') : paths.map('rekit.json'); 60 | 61 | const refreshRekitConfig = () => { 62 | try { 63 | rekitConfig = fs.readJsonSync(rekitConfigFile); 64 | } catch (e) { 65 | // Do nothing if rekit config is broken. Will use the last available one 66 | logger.info('Failed to load rekit.json, it doesn\'t exist or there is some syntax error.'); 67 | } 68 | }; 69 | if (!rekitConfigWatcher && !global.__REKIT_NO_WATCH) { 70 | rekitConfigWatcher = chokidar.watch([rekitConfigFile], { persistent: true }); 71 | rekitConfigWatcher.on('all', () => { 72 | refreshRekitConfig(); 73 | debouncedEmit('change'); 74 | }); 75 | } 76 | 77 | if (!rekitConfig) { 78 | refreshRekitConfig(); 79 | } 80 | 81 | if (!rekitConfig) { 82 | // config broken or not exist 83 | if (fs.existsSync(rekitConfigFile)) { 84 | // when rekit starts but config file is invalid, throw error. 85 | throw new Error('Config file broken: failed to parse rekit.json'); 86 | } else { 87 | // Support rekit 2.x project. 88 | const pkgJson = getPkgJson(); 89 | if (pkgJson && pkgJson.rekit) { 90 | rekitConfig = { 91 | appType: 'rekit-react', 92 | devPort: pkgJson.rekit.devPort, 93 | css: pkgJson.rekit.css || 'less', 94 | }; 95 | } else { 96 | // normal project 97 | rekitConfig = { 98 | appType: 'common', 99 | }; 100 | } 101 | } 102 | } 103 | if (!rekitConfig.appType) { 104 | rekitConfig.appType = 'common'; // app type not defined 105 | } 106 | return rekitConfig; 107 | } 108 | 109 | function getAppName() { 110 | const pkgJson = getPkgJson(); 111 | return pkgJson ? pkgJson.name : path.basename(paths.getProjectRoot()); 112 | } 113 | 114 | // function setAppType(_appType) { 115 | // if (rekitConfig) rekitConfig.appType = _appType; 116 | // appType = _appType; 117 | // } 118 | 119 | function setAppRegistry(reg) { 120 | appRegistry = reg; 121 | } 122 | 123 | function getAppRegistry() { 124 | return appRegistry; 125 | } 126 | 127 | function setPluginRegistry(reg) { 128 | pluginRegistry = reg; 129 | } 130 | 131 | function getPluginRegistry() { 132 | return pluginRegistry; 133 | } 134 | 135 | // Load rekit configuration from package.json 136 | Object.assign(config, { 137 | getAppName, 138 | getPkgJson, 139 | getRekitConfig, 140 | setAppRegistry, 141 | setPluginRegistry, 142 | getAppRegistry, 143 | getPluginRegistry, 144 | }); 145 | 146 | module.exports = config; 147 | -------------------------------------------------------------------------------- /core/create.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Create a Rekit project. Boilerplates are provided by Rekit plugins. 3 | * 4 | * To create a Rekit project: 5 | * - If options.source is a local folder, then copy content from it (excepts node_modules and .git folders), 6 | * if it's a git url, clone it. 7 | * - Otherwise, looks for project type registry from https://github.com/supnate/rekit-registry/appTypes.json, 8 | * clone the repo to the project folder. 9 | * - Execute postCreate.js 10 | * 11 | */ 12 | 13 | const path = require('path'); 14 | const _ = require('lodash'); 15 | const fs = require('fs-extra'); 16 | const config = require('./config'); 17 | const paths = require('./paths'); 18 | const repo = require('./repo'); 19 | const app = require('./app'); 20 | /* 21 | options: { 22 | status: callback 23 | source: where to create app from 24 | } 25 | */ 26 | function create(options) { 27 | console.log('Creating app: ', options.name); 28 | 29 | if (!options.status) 30 | options.status = (code, msg) => { 31 | console.log(msg); 32 | }; 33 | if (!options.type) options.type = 'rekit-react'; 34 | 35 | const prjDir = path.join(options.location || process.cwd(), options.name); 36 | return new Promise(async (resolve, reject) => { 37 | try { 38 | if (fs.existsSync(prjDir)) { 39 | reject('FOLDER_EXISTS'); 40 | return; 41 | } 42 | fs.mkdirSync(prjDir); 43 | let gitRepo; 44 | if (options.source) { 45 | if (/^https?:|^git@|^direct:/.test(options.source)) { 46 | // It's a git repo 47 | gitRepo = options.source; 48 | } else { 49 | // It's a local folder 50 | const srcDir = path.isAbsolute(options.source) 51 | ? options.source 52 | : path.join(process.cwd(), options.source); 53 | options.status('CREATE_APP_COPY_FILES', `Copy files from ${srcDir}...`); 54 | await fs.copy(srcDir, prjDir, { 55 | filter: src => 56 | !/\/(\.git|node_modules\/|node_modules$)/.test(src) || 57 | path.basename(src) === '.gitignore', 58 | }); 59 | } 60 | } else if (options.type) { 61 | // Get gitRepo 62 | options.status( 63 | 'QUERY_APP_TYPES_GIT_REPO', 64 | `Looking for the git repo for app type ${options.type}...`, 65 | ); 66 | const appTypes = app.getAppTypes(); 67 | const appType = _.find(appTypes, { id: options.type }); 68 | if (!appType) reject('APP_TYPE_NOT_SUPPORTED'); 69 | gitRepo = appType.repo; 70 | } else { 71 | await fs.remove(prjDir); 72 | reject('NO_SOURCE_OR_APP_TYPE'); 73 | } 74 | 75 | if (gitRepo) { 76 | options.status('CLONE_PROJECT', `Downloading project from ${gitRepo}...`); 77 | await repo.clone(gitRepo, prjDir); 78 | } 79 | 80 | postCreate(prjDir, options); 81 | options.status('CREATION_SUCCESS', '😃App creation success.'); 82 | resolve(); 83 | } catch (err) { 84 | console.log('Failed to create project.'); 85 | fs.removeSync(prjDir); 86 | reject(err); 87 | } 88 | }); 89 | } 90 | 91 | // function getAppTypes() { 92 | // return new Promise((resolve, reject) => { 93 | // syncAppRegistryRepo() 94 | // .then(() => { 95 | // resolve(fs.readJsonSync(paths.configFile('app-registry/appTypes.json'))); 96 | // }) 97 | // .catch(err => { 98 | // console.log('Failed to get app types: ', err); 99 | // reject('GET_APP_TYPES_FAILED'); 100 | // }); 101 | // }); 102 | // } 103 | 104 | function postCreate(prjDir, options) { 105 | const postCreateScript = path.join(prjDir, 'postCreate.js'); 106 | if (fs.existsSync(postCreateScript)) { 107 | options.status('POST_CREATE', 'Executing post create script...'); 108 | require(postCreateScript)(options); 109 | fs.removeSync(postCreateScript); 110 | } 111 | } 112 | 113 | function syncAppRegistryRepo() { 114 | const registryDir = paths.configFile('app-registry'); 115 | const appRegistry = config.getAppRegistry(); 116 | if (path.isAbsolute(appRegistry)) { 117 | // if it's local folder, copy to it 118 | console.log('Sync app registry from local folder.'); 119 | fs.removeSync(registryDir); 120 | fs.copySync(appRegistry, registryDir); 121 | return Promise.resolve(); 122 | } 123 | return repo.sync(appRegistry, registryDir); 124 | } 125 | 126 | module.exports = create; 127 | module.exports.syncAppRegistryRepo = syncAppRegistryRepo; 128 | // module.exports.getAppTypes = getAppTypes; 129 | -------------------------------------------------------------------------------- /core/dependents.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | const getDependents = _.memoize(elementById => { 4 | const dependents = {}; 5 | Object.values(elementById).forEach(ele => { 6 | if (ele.type === 'file' && ele.deps && ele.deps.length) { 7 | ele.deps.forEach(dep => { 8 | if (dep.type === 'file') { 9 | if (!dependents[dep.id]) dependents[dep.id] = []; 10 | dependents[dep.id].push(ele.id); 11 | } 12 | }); 13 | } 14 | }); 15 | return dependents; 16 | }); 17 | module.exports = { getDependents }; 18 | -------------------------------------------------------------------------------- /core/deps.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const traverse = require('@babel/traverse').default; 3 | const t = require('@babel/types'); 4 | const vio = require('./vio'); 5 | const ast = require('./ast'); 6 | const refactor = require('./refactor'); 7 | const plugin = require('./plugin'); 8 | 9 | const depsCache = {}; 10 | 11 | function getDeps(filePath, filesInPath = {}) { 12 | // Summary: 13 | // Get dependencies of a module 14 | // originalFilePath is used to avoid circle loop 15 | // if (filesInPath[filePath]) return []; //todo: not enough for complex circle dep loop 16 | 17 | if (depsCache[filePath] && depsCache[filePath].content === vio.getContent(filePath)) { 18 | return depsCache[filePath].deps; 19 | } 20 | 21 | const plugins = plugin.getPlugins('deps.getDeps'); 22 | let deps = null; 23 | if (plugins.length) { 24 | _.reverse(plugins.slice()).some(p => { 25 | deps = p.deps.getDeps(filePath); 26 | return deps; 27 | }); 28 | if (deps) return deps; 29 | } 30 | 31 | if (!/\.(jsx|js|ts|tsx)$/.test(filePath)) { 32 | //!filePath.endsWith('.js') && !filePath.endsWith('.jsx')) { 33 | return null; 34 | } 35 | 36 | if (!vio.fileExists(filePath)) return []; 37 | 38 | deps = []; 39 | 40 | const pushModuleSource = (moduleSource, args = {}) => { 41 | if (!moduleSource) return; 42 | if (!refactor.isLocalModule(moduleSource)) { 43 | deps.push({ 44 | id: moduleSource, 45 | type: 'npm', 46 | ...args, 47 | }); 48 | return; 49 | } 50 | let modulePath = refactor.resolveModulePath(filePath, moduleSource); 51 | // maybe modulePath is json/img or others. 52 | if (!vio.fileExists(modulePath)) { 53 | ['.js', '.jsx', '.ts', '.tsx'].some(ext => { 54 | if (vio.fileExists(modulePath + ext)) { 55 | modulePath += ext; 56 | return true; 57 | } 58 | return false; 59 | }); 60 | } 61 | deps.push({ id: modulePath, type: 'file', ...args }); 62 | }; 63 | const ast2 = ast.getAst(filePath); 64 | if (!ast2) { 65 | console.log('no ast: ', filePath); 66 | return []; // if syntax error, no deps returned. 67 | } 68 | try { 69 | traverse(ast2, { 70 | ExportNamedDeclaration(path) { 71 | const moduleSource = _.get(path, 'node.source.value'); 72 | const exported = _.get(path, 'node.specifiers').reduce((prev, specifier) => { 73 | prev[_.get(specifier, 'exported.name')] = _.get(specifier, 'local.name'); 74 | return prev; 75 | }, {}); 76 | pushModuleSource(moduleSource, { exported }); 77 | }, 78 | ExportAllDeclaration(path) { 79 | const moduleSource = _.get(path, 'node.source.value'); 80 | pushModuleSource(moduleSource); 81 | }, 82 | CallExpression(path) { 83 | const isRequire = _.get(path, 'node.callee.name') === 'require'; 84 | const isImport = _.get(path, 'node.callee.type') === 'Import'; 85 | 86 | if ((isRequire || isImport) && t.isStringLiteral(_.get(path, 'node.arguments[0]'))) { 87 | const moduleSource = _.get(path, 'node.arguments[0].value'); 88 | const args = {}; 89 | if (isRequire) args.isRequire = true; 90 | if (isImport) args.isImport = true; 91 | pushModuleSource(moduleSource, args); 92 | } 93 | }, 94 | ImportDeclaration(path) { 95 | const moduleSource = _.get(path, 'node.source.value'); 96 | const imported = []; 97 | let defaultImport = false; 98 | let nsImport = false; 99 | path.node.specifiers.forEach(specifier => { 100 | if (specifier.type === 'ImportNamespaceSpecifier') { 101 | // imported.push('*'); 102 | // TODO: try to analyze namespace import 103 | nsImport = true; 104 | } 105 | if (specifier.type === 'ImportSpecifier') { 106 | imported.push(specifier.imported.name); 107 | } 108 | if (specifier.type === 'ImportDefaultSpecifier') { 109 | defaultImport = true; 110 | } 111 | }); 112 | 113 | const args = {}; 114 | if (imported.length) args.imported = imported; 115 | if (nsImport) args.nsImport = true; 116 | args.defaultImport = defaultImport; 117 | pushModuleSource(moduleSource, args); 118 | }, 119 | }); 120 | } catch (e) { 121 | return []; 122 | } 123 | 124 | // Flatten deps 125 | // If a module use 'export ... from ...' then should direct to the real target module. 126 | // e.g. 127 | // currentModule: import { a, b } from './someModule'; 128 | // someModule: export { default as a, b } from './anotherModule'; 129 | // then: currentModule depends on anotherrModule 130 | const realDeps = deps.reduce((prev, dep) => { 131 | if (dep.imported && dep.imported.length && !filesInPath[dep.id] && dep.id !== filePath) { 132 | const depsOfDep = getDeps(dep.id, { ...filesInPath, [filePath]: true }); 133 | const imported = [...dep.imported]; 134 | if (depsOfDep) { 135 | dep.imported.forEach(importedName => { 136 | depsOfDep.forEach(theDep => { 137 | if (theDep.exported && theDep.exported[importedName]) { 138 | _.pull(imported, importedName); 139 | const dep2 = { id: theDep.id, type: theDep.type }; 140 | const realImported = theDep.exported[importedName]; 141 | if (realImported === 'default') dep2.defaultImport = true; 142 | else { 143 | dep2.imported = [realImported]; 144 | dep2.defaultImport = false; 145 | } 146 | prev.push(dep2); 147 | } 148 | }); 149 | }); 150 | } 151 | if (imported.length) { 152 | prev.push({ 153 | ...dep, 154 | imported, 155 | }); 156 | } else if (dep.defaultImport) { 157 | const dep2 = { ...dep }; 158 | delete dep2.imported; 159 | prev.push(dep2); 160 | } 161 | } else prev.push(dep); 162 | return prev; 163 | }, []); 164 | 165 | // Merge deps 166 | const byId = {}; 167 | realDeps.forEach(dep => { 168 | let dep2 = byId[dep.id]; 169 | if (!dep2) { 170 | byId[dep.id] = dep; 171 | dep2 = dep; 172 | } else { 173 | const merged = { 174 | id: dep.id, 175 | defaultImport: dep.defaultImport || dep2.defaultImport, 176 | imported: _.uniq([...(dep2.imported || []), ...(dep.imported || [])]), 177 | exported: { ...dep.exported, ...dep2.exported }, 178 | isImport: dep.isImport || dep2.isImport, 179 | isRequire: dep.isRequire || dep2.isRequire, 180 | type: dep.type, 181 | }; 182 | if (!merged.imported.length) delete merged.imported; 183 | if (!merged.isImport) delete merged.isImport; 184 | if (!merged.isRequire) delete merged.isRequire; 185 | if (!Object.keys(merged.exported).length) delete merged.exported; 186 | 187 | byId[dep.id] = merged; 188 | } 189 | }); 190 | 191 | return Object.values(byId); 192 | } 193 | 194 | module.exports = { 195 | getDeps, 196 | }; 197 | -------------------------------------------------------------------------------- /core/element.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const plugin = require('./plugin'); 3 | 4 | function execHooks(beforeAfter, action, type) { 5 | const methodName = _.camelCase(`${beforeAfter}-${action}-${type}`); 6 | // console.log('exec hooks: ', beforeAfter, action, type, methodName); 7 | const hooksPlugins = plugin.getPlugins(`hooks.${methodName}`); 8 | // console.log('hooks plugins: ', `hooks.${methodName}`, hooksPlugins.length); 9 | const args = _.toArray(arguments).slice(3); 10 | hooksPlugins.forEach(p => p.hooks[methodName](...args)); 11 | } 12 | 13 | function execBeforeHooks(...args) { 14 | args.unshift('before'); 15 | execHooks(...args); 16 | } 17 | 18 | function execAfterHooks(...args) { 19 | args.unshift('after'); 20 | execHooks(...args); 21 | } 22 | 23 | function add(type, name, args) { 24 | console.log(`Adding ${type}: `, name); 25 | execBeforeHooks('add', type, name, args); 26 | const thePlugin = _.last(plugin.getPlugins(`elements.${type}.add`)); 27 | if (!thePlugin) throw new Error(`Can't find a plugin which could add an element of type ${type}`); 28 | thePlugin.elements[type].add(name, args); 29 | execAfterHooks('add', type, name, args); 30 | } 31 | 32 | function move(type, source, target, args) { 33 | console.log(`Moving ${type}: `, source, target); 34 | execBeforeHooks('move', type, source, target, args); 35 | const thePlugin = _.last(plugin.getPlugins(`elements.${type}.move`)); 36 | if (!thePlugin) throw new Error(`Can't find a plugin which could move element of type ${type}`); 37 | thePlugin.elements[type].move(source, target, args); 38 | execAfterHooks('move', type, source, target, args); 39 | } 40 | 41 | function remove(type, name, args) { 42 | console.log(`Removing ${type}: `, name); 43 | execBeforeHooks('remove', type, name, args); 44 | const thePlugin = _.last(plugin.getPlugins(`elements.${type}.remove`)); 45 | if (!thePlugin) throw new Error(`Can't find a plugin which could remove an element of type ${type}`); 46 | thePlugin.elements[type].remove(name, args); 47 | execAfterHooks('remove', type, name, args); 48 | } 49 | 50 | function update(type, name, args) { 51 | // Placeholder for potential future usage 52 | console.log('updating element: ', type, name, args); 53 | } 54 | 55 | function byId(id) { 56 | return id; 57 | } 58 | 59 | module.exports = { 60 | add, 61 | move, 62 | remove, 63 | update, 64 | byId, 65 | }; 66 | -------------------------------------------------------------------------------- /core/files.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const path = require('path'); 3 | const fs = require('fs'); 4 | const paths = require('./paths'); 5 | const config = require('./config'); 6 | const deps = require('./deps'); 7 | const vio = require('./vio'); 8 | const chokidar = require('chokidar'); 9 | const EventEmitter = require('events'); 10 | const minimatch = require('minimatch'); 11 | // const logger = require('./logger'); 12 | 13 | const MAX_FILES = 4000; 14 | 15 | let cache = {}; 16 | let parentHash = {}; 17 | let allElementById = {}; 18 | const byId = id => allElementById[id]; 19 | const otherFiles = {}; // dynamically added files to the project, regardless of exclude configuration 20 | const files = new EventEmitter(); 21 | 22 | // const emitChange = () => { 23 | // files.emit('change'); 24 | // }; 25 | 26 | const emitChange = _.debounce(() => { 27 | files.emit('change'); 28 | }, 100); 29 | 30 | config.on('change', () => { 31 | // when config change, cache should be clear because include/exclude may changed. 32 | cache = {}; 33 | parentHash = {}; 34 | allElementById = {}; 35 | }); 36 | 37 | // let watcher = null; 38 | // function startWatch() { 39 | // if (!watcher) { 40 | 41 | // } 42 | // } 43 | 44 | // Means read whole project 45 | function readDir(dir, args = {}) { 46 | ensureWatch(); 47 | if (args.force) { 48 | cache = {}; 49 | parentHash = {}; 50 | allElementById = {}; 51 | } 52 | const prjRoot = paths.getProjectRoot(); 53 | 54 | dir = dir || prjRoot; 55 | if (!path.isAbsolute(dir)) dir = paths.map(dir); 56 | if (!fs.existsSync(dir)) { 57 | return { elements: [], elementById: {} }; 58 | } 59 | if (!cache[dir]) { 60 | cache[dir] = getDirElement(dir); 61 | } 62 | const elementById = {}; 63 | const dirEle = cache[dir]; 64 | const children = [...dirEle.children]; 65 | const rDir = dir.replace(prjRoot, ''); 66 | elementById[rDir] = dirEle; 67 | while (children.length) { 68 | // Get elementById 69 | const cid = children.pop(); 70 | const ele = byId(cid); 71 | elementById[cid] = ele; 72 | if (ele.children) children.push(...ele.children); 73 | } 74 | Object.keys(otherFiles).forEach(k => { 75 | if (elementById[k]) return; 76 | const fileEle = getFileElement(k); 77 | if (fileEle) elementById[k] = fileEle; 78 | }); 79 | // Always return a cloned object to avoid acidentally cache modify 80 | const res = JSON.parse(JSON.stringify({ elements: dirEle.children, elementById })); 81 | return res; 82 | } 83 | 84 | let watcher = null; 85 | function ensureWatch() { 86 | if (watcher) return; 87 | if (global.__REKIT_NO_WATCH) return; 88 | watcher = chokidar.watch(paths.getProjectRoot(), { 89 | persistent: true, 90 | ignored: /[/\\]node_modules[/\\]|[/\\]\.git[/\\]/, 91 | awaitWriteFinish: false, 92 | }); 93 | watcher.on('ready', () => { 94 | watcher.on('add', onAdd); 95 | watcher.on('change', onChange); 96 | watcher.on('unlink', onUnlink); 97 | watcher.on('addDir', onAddDir); 98 | watcher.on('unlinkDir', onUnlinkDir); 99 | }); 100 | } 101 | 102 | const setLastChangeTime = () => { 103 | files.lastChangeTime = Date.now(); 104 | }; 105 | 106 | function getExInclude() { 107 | const exclude = [ 108 | '**/node_modules', 109 | '**/node_modules/**', 110 | '**/.DS_Store', 111 | '**/.git', 112 | '**/.git/**', 113 | ...(config.getRekitConfig().exclude || []), 114 | ]; 115 | const include = config.getRekitConfig().include || []; 116 | return { include, exclude }; 117 | } 118 | 119 | function shouldShow(file) { 120 | if (path.isAbsolute(file)) file = paths.relativePath(file); 121 | const { include, exclude } = getExInclude(); 122 | return !exclude.some(p => minimatch(file, p)) || include.some(p => minimatch(file, p)); 123 | } 124 | 125 | function onAdd(file, args = {}) { 126 | if (!shouldShow(file) && !args.force) return; 127 | if (!fs.existsSync(file)) return; 128 | const rFile = paths.relativePath(file); 129 | allElementById[rFile] = getFileElement(file); 130 | 131 | const dir = path.dirname(rFile); 132 | if (byId(dir) && byId(dir).children) { 133 | const children = byId(dir).children; 134 | children.push(rFile); 135 | 136 | sortElements(byId(dir).children); 137 | } 138 | 139 | setLastChangeTime(); 140 | emitChange(); 141 | } 142 | function onUnlink(file) { 143 | if (!shouldShow(file)) return; 144 | // console.log('on unlink', file); 145 | const rFile = paths.relativePath(file); 146 | delete allElementById[rFile]; 147 | 148 | const dir = path.dirname(rFile); 149 | const dirEle = byId(dir); 150 | if (dirEle) { 151 | // If an element is in watching but not in project data 152 | // Or it's top element 153 | // Then its parent may not exist. 154 | _.pull(byId(dir).children, rFile); 155 | } 156 | 157 | setLastChangeTime(); 158 | emitChange(); 159 | } 160 | function onChange(file) { 161 | if (!shouldShow(file)) return; 162 | if (!fs.existsSync(file)) return; 163 | const prjRoot = paths.getProjectRoot(); 164 | const rFile = paths.relativePath(file); 165 | allElementById[rFile] = getFileElement(file); 166 | 167 | setLastChangeTime(); 168 | emitChange(); 169 | } 170 | function onAddDir(file) { 171 | if (!shouldShow(file)) return; 172 | if (!fs.existsSync(file)) return; 173 | const rFile = paths.relativePath(file); 174 | if (byId(rFile)) return; // already exists 175 | // suppose it's always empty, children will be added by other events 176 | allElementById[rFile] = { 177 | name: path.basename(file), 178 | type: 'folder', 179 | id: rFile, 180 | children: [], 181 | }; 182 | const dir = path.dirname(rFile); 183 | if (!byId(dir) || !byId(dir).children) { 184 | console.warn('files.onAddDir: dir not exists: ', file); 185 | return; 186 | } 187 | byId(dir).children.push(rFile); 188 | 189 | sortElements(byId(dir).children); 190 | setLastChangeTime(); 191 | emitChange(); 192 | } 193 | function onUnlinkDir(file) { 194 | onUnlink(file); 195 | } 196 | 197 | function getDirElement(dir, theElementById) { 198 | ensureWatch(); 199 | if (!path.isAbsolute(dir)) dir = paths.map(dir); 200 | let rDir = paths.relativePath(dir); // dir.replace(prjRoot, ''); 201 | if (!rDir) rDir = '.'; // root dir 202 | const dirEle = { 203 | name: path.basename(dir), 204 | type: 'folder', 205 | id: rDir, 206 | children: [], 207 | }; 208 | allElementById[rDir] = dirEle; 209 | if (theElementById) theElementById[rDir] = dirEle; 210 | 211 | fs.readdirSync(dir) 212 | .map(name => path.join(dir, name)) 213 | .filter(shouldShow) 214 | .forEach(file => { 215 | const rFile = paths.relativePath(file); 216 | dirEle.children.push(rFile); 217 | parentHash[rFile] = rDir; 218 | if (fs.statSync(file).isDirectory()) { 219 | getDirElement(file); 220 | } else { 221 | getFileElement(file, theElementById); 222 | } 223 | }); 224 | if (Object.keys(allElementById).length > MAX_FILES) { 225 | console.log( 226 | `Rekit does'nt support a project with too many files. The project contains ${ 227 | Object.keys(allElementById).length 228 | } files, the max size is ${MAX_FILES}. Consider exclude some in rekit.json.`, 229 | ); 230 | throw new Error('OVER_MAX_FILES'); 231 | } 232 | 233 | sortElements(dirEle.children); 234 | 235 | return dirEle; 236 | } 237 | 238 | function getFileElement(file, theElementById) { 239 | if (!path.isAbsolute(file)) file = paths.map(file); 240 | if (!vio.fileExists(file)) return null; 241 | const rFile = paths.relativePath(file); 242 | const ext = path.extname(file).replace('.', ''); 243 | const size = fs.statSync(file).size; 244 | const fileEle = { 245 | name: path.basename(file), 246 | type: 'file', 247 | ext, 248 | size, 249 | id: rFile, 250 | }; 251 | 252 | allElementById[rFile] = fileEle; 253 | if (theElementById) theElementById[rFile] = fileEle; 254 | const fileDeps = size < 50000 ? deps.getDeps(rFile) : null; 255 | if (fileDeps) { 256 | fileEle.views = [ 257 | { key: 'diagram', name: 'Diagram' }, 258 | { 259 | key: 'code', 260 | name: 'Code', 261 | target: rFile, 262 | isDefault: true, 263 | }, 264 | ]; 265 | fileEle.deps = fileDeps; 266 | } 267 | return fileEle; 268 | } 269 | 270 | function sortElements(elements) { 271 | elements.sort((a1, b1) => { 272 | const a = byId(a1); 273 | const b = byId(b1); 274 | if (!a || !b) { 275 | console.log('Error in sortElement: ', a1, b1); // should not happen? 276 | return 0; 277 | } 278 | if (a.children && !b.children) return -1; 279 | else if (!a.children && b.children) return 1; 280 | return a.name.toLowerCase().localeCompare(b.name.toLowerCase()); 281 | }); 282 | return elements; 283 | } 284 | 285 | function include(file) { 286 | if (path.isAbsolute(file)) file = paths.relativePath(file); 287 | if (!fs.existsSync(paths.map(file))) return; 288 | otherFiles[paths.normalize(file)] = true; 289 | onAdd(paths.map(file), { force: true }); 290 | } 291 | 292 | files.readDir = readDir; 293 | files.getDirElement = getDirElement; 294 | files.getFileElement = getFileElement; 295 | // files.setFileChanged = setFileChanged; 296 | files.include = include; 297 | module.exports = files; 298 | -------------------------------------------------------------------------------- /core/handleCommand.js: -------------------------------------------------------------------------------- 1 | const element = require('./element'); 2 | 3 | module.exports = function(args) { 4 | switch (args.commandName) { 5 | case 'add': 6 | case 'remove': 7 | element[args.commandName](args.type, args.name, args); 8 | break; 9 | case 'move': 10 | element.move(args.type, args.source, args.target, args); 11 | break; 12 | case 'update': 13 | element.update(args.type, args.name, args); 14 | break; 15 | default: 16 | console.error('Unknown command: ', args.commandName, args); 17 | throw new Error(`Unknown command: ${args.commandName}`); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /core/logger.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const { createLogger, transports, format } = require('winston'); 3 | const stringify = require('fast-safe-stringify'); 4 | const paths = require('./paths'); 5 | 6 | const SPLAT = Symbol.for('splat'); 7 | 8 | fs.ensureDirSync(paths.configFile('logs')); 9 | const myFormat = format.combine( 10 | format.timestamp({ 11 | format: 'YYYY-MM-DD hh:mm:ss.SSS', 12 | }), 13 | format.splat(), 14 | format.printf(info => { 15 | const splat = info[SPLAT]; 16 | const ss = []; 17 | if (splat) { 18 | splat.forEach(s => { 19 | ss.push(s instanceof Error ? s.stack || s.message : stringify(s)); 20 | }); 21 | } 22 | return `[${info.timestamp}] ${info.label ? '[' + info.label + '] ' : ''}[${info.level}] ${ 23 | info.message 24 | } ${ss.join(',')}`; 25 | }), 26 | ); 27 | 28 | const myTransports = [ 29 | new transports.File({ 30 | filename: paths.configFile(`logs/rekit.log`), 31 | maxsize: 1000000, 32 | maxFiles: 5, 33 | handleExceptions: true, 34 | }), 35 | new transports.Console({ 36 | handleExceptions: true, 37 | }), 38 | ]; 39 | 40 | const logger = createLogger({ 41 | level: 'info', 42 | exitOnError: false, 43 | format: myFormat, 44 | splat: true, 45 | transports: myTransports, 46 | }); 47 | 48 | module.exports = logger; 49 | -------------------------------------------------------------------------------- /core/paths.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const os = require('os'); 3 | const _ = require('lodash'); 4 | const fs = require('fs-extra'); 5 | 6 | function join(...args) { 7 | // A consistent and normalized version of path.join cross platforms 8 | return path.normalize(path.join(...args)).replace(/\\/g, '/'); 9 | } 10 | 11 | function normalize(p) { 12 | return path.normalize(p).replace(/\\/g, '/'); 13 | } 14 | 15 | let projectRoot; 16 | function setProjectRoot(prjRoot) { 17 | if (!path.isAbsolute(prjRoot)) prjRoot = path.join(process.cwd(), prjRoot); 18 | prjRoot = path.normalize(prjRoot).replace(/\\/g, '/'); 19 | projectRoot = /\/$/.test(prjRoot) ? prjRoot : prjRoot + '/'; 20 | } 21 | 22 | function getProjectRoot() { 23 | if (!projectRoot) { 24 | setProjectRoot(process.cwd()); 25 | } 26 | return projectRoot; 27 | } 28 | 29 | function map() { 30 | const args = [getProjectRoot()]; 31 | args.push.apply(args, arguments); 32 | return join.apply(null, args); 33 | } 34 | 35 | function relativePath(file) { 36 | if (!path.isAbsolute(file)) return file; 37 | return file.replace(/\\/g, '/').replace(getProjectRoot(), ''); 38 | } 39 | 40 | function relative(from, to) { 41 | return path.relative(from, to).replace(/\\/g, '/'); 42 | } 43 | 44 | // get the module source of 'to' from 'from' file. 45 | // e.g: import to from '../to'; 46 | function relativeModuleSource(from, to) { 47 | const p = join( 48 | relative(path.dirname(from), path.dirname(to)), 49 | path.basename(to).replace(/\.\w+$/, ''), 50 | ); 51 | 52 | if (!_.startsWith(p, '.')) return './' + p; 53 | return p; 54 | } 55 | 56 | function getFileId(filePath) { 57 | return filePath.replace(getProjectRoot()).replace(/^\/?/, ''); 58 | } 59 | 60 | const rekitDir = path.join(os.homedir(), '.rekit'); 61 | function configFile(file) { 62 | return path.join(rekitDir, file); 63 | } 64 | 65 | const configPath = configFile; 66 | fs.ensureDirSync(configPath('plugins')); // ensure plugins folder exists 67 | 68 | module.exports = { 69 | join, 70 | map, 71 | getFileId, 72 | setProjectRoot, 73 | getProjectRoot, 74 | relative, 75 | normalize, 76 | rekitDir, 77 | configFile, 78 | configPath, 79 | relativePath, 80 | relativeModuleSource, 81 | getLocalPluginRoot: () => map('tools/plugins'), 82 | }; 83 | -------------------------------------------------------------------------------- /core/refactor/array.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const traverse = require('@babel/traverse').default; 5 | const logger = require('../logger'); 6 | const vio = require('../vio'); 7 | const common = require('./common'); 8 | 9 | /** 10 | * Find the nearest char index before given index. skip white space strings 11 | * If not found, return -1 12 | * eg: nearestCharBefore(',', '1, 2, 3', 4) => 1 13 | * @param {string} char - Which char to find 14 | * @param {string} str - The string to to search. 15 | * @index {number} index - From which index start to find 16 | * @ 17 | **/ 18 | function nearestCharBefore(char, str, index) { 19 | // Find the nearest char index before given index. skip white space strings 20 | // If not found, return -1 21 | // eg: nearestCharBefore(',', '1, 2, 3', 4) => 1 22 | let i = index - 1; 23 | while (i >= 0) { 24 | if (str.charAt(i) === char) return i; 25 | if (!/\s/.test(str.charAt(i))) return -1; 26 | i -= 1; 27 | } 28 | return -1; 29 | } 30 | 31 | /** 32 | * Similar with nearestCharBefore, but find the char after the given index. 33 | * If not found, return -1 34 | * @param {string} char - Which char to find 35 | * @param {string} str - The string to to search. 36 | * @index {number} index - From which index start to find 37 | * @ 38 | **/ 39 | function nearestCharAfter(char, str, index) { 40 | // Find the nearest char index before given index. skip white space strings 41 | // If not found, return -1 42 | let i = index + 1; 43 | while (i < str.length) { 44 | if (str.charAt(i) === char) return i; 45 | if (!/\s/.test(str.charAt(i))) return -1; 46 | i += 1; 47 | } 48 | return -1; 49 | } 50 | 51 | /** 52 | * Add an element to an array definition. 53 | * @param {object} node - The ast node of the array definition. 54 | * @param {string} code - The code to append to the array. 55 | * @alias module:refactor.addToArrayByNode 56 | * @ 57 | **/ 58 | function addToArrayByNode(node, code) { 59 | // node: the arr expression node 60 | // code: added as the last element of the array 61 | 62 | const multilines = node.loc.start.line !== node.loc.end.line; 63 | let insertPos = node.start + 1; // insert after '[' 64 | 65 | if (node.elements.length) { 66 | const ele = _.last(node.elements); 67 | insertPos = ele.end; 68 | } 69 | 70 | let replacement; 71 | if (multilines) { 72 | const indent = _.repeat(' ', node.loc.end.column - 1); 73 | replacement = `\n${indent} ${code}`; 74 | if (node.elements.length) { 75 | replacement = `,${replacement}`; 76 | } else { 77 | replacement = `${replacement},`; 78 | } 79 | } else { 80 | replacement = code; 81 | if (node.elements.length > 0) { 82 | replacement = `, ${code}`; 83 | } 84 | } 85 | return [ 86 | { 87 | start: insertPos, 88 | end: insertPos, 89 | replacement, 90 | }, 91 | ]; 92 | } 93 | 94 | /** 95 | * Remove an element from an array definition. 96 | * @param {object} node - The ast node of the array definition. 97 | * @param {object} eleNode - The ast node to be removed. 98 | * @alias module:refactor.removeFromArrayByNode 99 | * @ 100 | **/ 101 | function removeFromArrayByNode(node, eleNode) { 102 | const elements = node.elements; 103 | 104 | if (!elements.includes(eleNode)) { 105 | logger.warn('Failed to find element when trying to remove element from array.'); 106 | return []; 107 | } 108 | 109 | if (!node._filePath) { 110 | throw new Error('No _filePath property found on node when removing element from array'); 111 | } 112 | 113 | const content = vio.getContent(node._filePath); 114 | 115 | let startPos = nearestCharBefore(',', content, eleNode.start); 116 | let isFirstElement = false; 117 | if (startPos < 0) { 118 | // it's the first element 119 | isFirstElement = true; 120 | startPos = node.start + 1; // start from the char just after the '[' 121 | } 122 | 123 | let endPos = eleNode.end; 124 | 125 | if (elements.length === 1 || isFirstElement) { 126 | // if the element is the only element, try to remove the trailing comma if exists 127 | const nextComma = nearestCharAfter(',', content, endPos - 1); 128 | if (nextComma >= 0) endPos = nextComma + 1; 129 | } 130 | 131 | return [ 132 | { 133 | start: startPos, 134 | end: endPos, 135 | replacement: '', 136 | }, 137 | ]; 138 | } 139 | 140 | /** 141 | * Add an identifier to the array of the name 'varName'. 142 | * It only finds the first matched array in the top scope of ast. 143 | * 144 | * @param {object|string} ast - The ast tree or the js file path to find the array. 145 | * @param {string} varName - The variable name of the array definition. 146 | * @param {string} identifierName - The identifier to append to array. 147 | * @alias module:refactor.addToArray 148 | * 149 | * @example 150 | * const refactor = require('rekit-core').refactor; 151 | * // code in ast: const arr = [a, b, c]; 152 | * refactor.addToArray(file, 'arr', 'd'); 153 | * // code changes to: const arr = [a, b, c, d]; 154 | **/ 155 | function addToArray(ast, varName, identifierName) { 156 | let changes = []; 157 | traverse(ast, { 158 | VariableDeclarator(path) { 159 | const node = path.node; 160 | if (_.get(node, 'id.name') !== varName || _.get(node, 'init.type') !== 'ArrayExpression') 161 | return; 162 | node.init._filePath = ast._filePath; 163 | changes = addToArrayByNode(node.init, identifierName); 164 | path.stop(); 165 | }, 166 | }); 167 | return changes; 168 | } 169 | 170 | /** 171 | * Remove an identifier from the array of the name 'varName'. 172 | * It only finds the first matched array in the global scope of ast. 173 | * 174 | * @param {object|string} ast - The ast tree or the js file path to find the array. 175 | * @param {string} varName - The variable name of the array definition. 176 | * @param {string} identifierName - The identifier to append to array. 177 | * @alias module:refactor.removeFromArray 178 | * 179 | * @example 180 | * const refactor = require('rekit-core').refactor; 181 | * // code in ast: const arr = [a, b, c]; 182 | * refactor.removeFromArray(file, 'arr', 'a'); 183 | * // code changes to: const arr = [b, c]; 184 | **/ 185 | function removeFromArray(ast, varName, identifierName) { 186 | let changes = []; 187 | traverse(ast, { 188 | VariableDeclarator(path) { 189 | const node = path.node; 190 | if (_.get(node, 'id.name') !== varName || _.get(node, 'init.type') !== 'ArrayExpression') 191 | return; 192 | node.init._filePath = ast._filePath; 193 | const toRemove = _.find(node.init.elements, ele => ele.name === identifierName); 194 | changes = removeFromArrayByNode(node.init, toRemove); 195 | path.stop(); 196 | }, 197 | }); 198 | return changes; 199 | } 200 | 201 | module.exports = { 202 | nearestCharBefore, // export only for test purpose 203 | nearestCharAfter, // export only for test purpose 204 | addToArrayByNode, 205 | removeFromArrayByNode, 206 | addToArray: common.acceptFilePathForAst(addToArray), 207 | removeFromArray: common.acceptFilePathForAst(removeFromArray), 208 | }; 209 | -------------------------------------------------------------------------------- /core/refactor/cls.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const traverse = require("@babel/traverse").default; 4 | const common = require("./common"); 5 | const identifier = require("./identifier"); 6 | 7 | /** 8 | * Rename a es6 class name in a module. Only rename the class definition and its reference in the module. 9 | * It will not find other files to rename reference. 10 | * @param {string} ast - Which module to rename class name. 11 | * @param {string} oldName - The old class name. 12 | * @index {number} newName - The new class name. 13 | * @alias module:refactor.renameClassName 14 | * @example 15 | * // class MyClass { 16 | * // ... 17 | * // } 18 | * const refactor = require('rekit-core').refactor; 19 | * refactor.renameClassName(file, 'MyClass', 'NewMyClass'); 20 | * // => class NewMyClass { 21 | * // => ... 22 | * // => } 23 | **/ 24 | function renameClassName(ast, oldName, newName) { 25 | let defNode = null; 26 | // Find the definition node of the class 27 | traverse(ast, { 28 | ClassDeclaration(path) { 29 | if (path.node.id && path.node.id.name === oldName) { 30 | defNode = path.node.id; 31 | } 32 | } 33 | }); 34 | 35 | if (defNode) { 36 | return identifier.renameIdentifier(ast, oldName, newName, defNode); 37 | } 38 | return []; 39 | } 40 | 41 | module.exports = { 42 | renameClassName: common.acceptFilePathForAst(renameClassName) 43 | }; 44 | -------------------------------------------------------------------------------- /core/refactor/common.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const _ = require('lodash'); 5 | const vio = require('../vio'); 6 | const ast = require('../ast'); 7 | const paths = require('../paths'); 8 | const config = require('../config'); 9 | // const utils = require('../../common/utils'); 10 | 11 | function updateSourceCode(code, changes) { 12 | // Summary: 13 | // This must be called before code is changed some places else rather than ast 14 | 15 | changes.sort((c1, c2) => c2.start - c1.start); 16 | // Remove same or overlapped changes 17 | const newChanges = _.reduce( 18 | changes, 19 | (cleanChanges, curr) => { 20 | const last = _.last(cleanChanges); 21 | 22 | if (!cleanChanges.length || last.start > curr.end) { 23 | cleanChanges.push(curr); 24 | } else if (last.start === last.end && last.end === curr.start && curr.start === curr.end) { 25 | // insert code at the same position, merge them 26 | last.replacement += curr.replacement; 27 | } 28 | return cleanChanges; 29 | }, 30 | [], 31 | ); 32 | 33 | const chars = code.split(''); 34 | newChanges.forEach(c => { 35 | // Special case: after the change, two empty lines occurs, should delete one line 36 | if ( 37 | c.replacement === '' && 38 | (c.start === 0 || chars[c.start - 1] === '\n') && 39 | chars[c.end] === '\n' 40 | ) { 41 | c.end += 1; 42 | } 43 | chars.splice(c.start, c.end - c.start, c.replacement); 44 | }); 45 | return chars.join(''); 46 | } 47 | 48 | function updateFile(filePath, changes) { 49 | // Summary: 50 | // Update the source file by changes. 51 | 52 | if (_.isFunction(changes)) { 53 | const ast1 = ast.getAst(filePath); 54 | // vio.assertAst(ast1, filePath); 55 | changes = changes(ast1); 56 | } 57 | let code = vio.getContent(filePath); 58 | code = updateSourceCode(code, changes); 59 | vio.save(filePath, code); 60 | } 61 | 62 | function getModuleResolverAlias() { 63 | const alias = config.getRekitConfig().moduleAlias || {}; 64 | return alias; 65 | } 66 | 67 | /** 68 | * Check if a module is local module. It will check alias defined by babel plugin module-resolver. 69 | * @param {string} modulePath - The module path. i.e.: import * from './abc'; './abc' is the module path. 70 | * @alias module:common.isLocalModule 71 | **/ 72 | function isLocalModule(modulePath) { 73 | // TODO: handle alias module path like src 74 | const alias = getModuleResolverAlias(); 75 | return /^\./.test(modulePath) || _.keys(alias).some(a => modulePath === a || _.startsWith(modulePath, a + '/')); 76 | } 77 | 78 | /** 79 | * Resolve the module path. 80 | * @param {string} relativeTo - Relative to which file to resolve. That is the file in which import the module. 81 | * @param {string} modulePath - The relative module path. 82 | * @alias module:common.resolveModulePath 83 | **/ 84 | function resolveModulePath(relativeToFile, modulePath) { 85 | if (!isLocalModule(modulePath)) { 86 | return modulePath; 87 | } 88 | 89 | const alias = getModuleResolverAlias(); 90 | const matched = _.find(_.keys(alias), k => _.startsWith(modulePath, k)); 91 | 92 | let res = null; 93 | if (matched) { 94 | const resolveTo = alias[matched]; 95 | const relativePath = modulePath.replace(new RegExp(`^${matched}`), '').replace(/^\//, ''); 96 | res = paths.map(resolveTo, relativePath); 97 | // res = utils.joinPath(utils.getProjectRoot(), resolveTo, relativePath); 98 | } else { 99 | res = paths.join(path.dirname(relativeToFile), modulePath); 100 | } 101 | 102 | let relPath = res.replace(paths.getProjectRoot(), '').replace(/^\/?/, ''); 103 | if (vio.dirExists(relPath)) { 104 | // if import from a folder, then resolve to index.js 105 | relPath = paths.join(relPath, 'index'); 106 | } 107 | return relPath; 108 | } 109 | 110 | function isSameModuleSource(s1, s2, contextFilePath) { 111 | return resolveModulePath(contextFilePath, s1) === resolveModulePath(contextFilePath, s2); 112 | } 113 | 114 | function acceptFilePathForAst(func) { 115 | // Summary: 116 | // Wrapper a function that accepts ast also accepts file path. 117 | // If it's file path, then update the file immediately. 118 | 119 | return function(file) { 120 | // eslint-disable-line 121 | let theAst = file; 122 | if (_.isString(file)) { 123 | theAst = ast.getAst(file, true); 124 | } 125 | const args = _.toArray(arguments); 126 | args[0] = theAst; 127 | 128 | const changes = func(...args); 129 | 130 | if (_.isString(file)) { 131 | updateFile(file, changes); 132 | } 133 | 134 | return changes; 135 | }; 136 | } 137 | 138 | function acceptFilePathForLines(func) { 139 | // Summary: 140 | // Wrapper a function that accepts lines also accepts file path. 141 | // If it's file path, then update the file immediately. 142 | 143 | return function(file) { 144 | // eslint-disable-line 145 | let lines = file; 146 | if (_.isString(file)) { 147 | lines = vio.getLines(file); 148 | } 149 | const args = _.toArray(arguments); 150 | args[0] = lines; 151 | 152 | func(...args); 153 | 154 | if (_.isString(file)) { 155 | vio.save(file, lines); 156 | } 157 | }; 158 | } 159 | 160 | module.exports = { 161 | updateSourceCode, 162 | updateFile, 163 | isLocalModule, 164 | isSameModuleSource, 165 | resolveModulePath, 166 | acceptFilePathForAst, 167 | acceptFilePathForLines, 168 | }; 169 | -------------------------------------------------------------------------------- /core/refactor/format.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const prettier = require('prettier'); 3 | const fs = require('fs'); 4 | const chokidar = require('chokidar'); 5 | const paths = require('../paths'); 6 | 7 | const PRETTIER_CONFIG_FILES = [ 8 | '.prettierrc', 9 | '.prettierrc.json', 10 | '.prettierrc.yaml', 11 | '.prettierrc.yml', 12 | '.prettierrc.js', 13 | 'package.json', 14 | 'prettier.config.js', 15 | '.editorconfig', 16 | ].map(f => paths.map(f)); 17 | 18 | const DEFAULT_PRETTIER_OPTIONS = { 19 | singleQuote: true, 20 | trailingComma: 'all', 21 | printWidth: 120, 22 | cursorOffset: 0, 23 | insertFinalNewline: true, 24 | editorconfig: true, 25 | }; 26 | 27 | let watcher; 28 | function startWatch() { 29 | if (!global.__REKIT_NO_WATCH) { 30 | watcher = chokidar.watch(PRETTIER_CONFIG_FILES, { 31 | persistent: true, 32 | awaitWriteFinish: true, 33 | }); 34 | 35 | const clearConfigCache = () => prettier.clearConfigCache(); 36 | watcher.on('all', clearConfigCache); 37 | } 38 | } 39 | 40 | function format(code, file, opts = {}) { 41 | if (!watcher) startWatch(); 42 | const filePath = path.isAbsolute(file) ? file : paths.map(file); 43 | if (!code) code = fs.readFileSync(filePath).toString(); 44 | 45 | let formatted = code; 46 | try { 47 | const options = prettier.resolveConfig.sync(filePath) || {}; 48 | const finalOptions = Object.assign( 49 | { filepath: filePath }, 50 | DEFAULT_PRETTIER_OPTIONS, 51 | options, 52 | opts, 53 | ); 54 | if (!finalOptions.insertFinalNewline) { 55 | finalOptions.rangeStart = 0; 56 | finalOptions.rangeEnd = code.length - 1; 57 | } 58 | formatted = prettier.formatWithCursor(code, finalOptions); 59 | } catch (err) { 60 | console.log('Failed to format code: ', err); 61 | } 62 | return formatted; 63 | } 64 | 65 | module.exports = format; 66 | -------------------------------------------------------------------------------- /core/refactor/func.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const traverse = require("@babel/traverse").default; 4 | const common = require("./common"); 5 | const identifier = require("./identifier"); 6 | 7 | /** 8 | * Rename a function name in a module. 9 | * @param {string} ast - Which module to rename function. 10 | * @param {string} oldName - The old function name. 11 | * @index {number} newName - The new function name. 12 | * @alias module:refactor.renameFunctionName 13 | * @example 14 | * // function MyFunc() { 15 | * // ... 16 | * // } 17 | * const refactor = require('rekit-core').refactor; 18 | * refactor.renameFunctionName(file, 'MyFunc', 'NewMyFunc'); 19 | * // => function NewMyFunc { 20 | * // => ... 21 | * // => } 22 | **/ 23 | function renameFunctionName(ast, oldName, newName) { 24 | // Summary: 25 | // Rename the name of the function first found. Usually used by 26 | // flat function definition file. 27 | 28 | let defNode = null; 29 | // Find the definition node of the class 30 | traverse(ast, { 31 | FunctionDeclaration(path) { 32 | if (path.node.id && path.node.id.name === oldName) { 33 | defNode = path.node.id; 34 | } 35 | } 36 | }); 37 | 38 | if (defNode) { 39 | return identifier.renameIdentifier(ast, oldName, newName, defNode); 40 | } 41 | return []; 42 | } 43 | 44 | module.exports = { 45 | renameFunctionName: common.acceptFilePathForAst(renameFunctionName) 46 | }; 47 | -------------------------------------------------------------------------------- /core/refactor/generate.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const generate = require('@babel/generator').default; 4 | const format = require('./format'); 5 | 6 | // Generate and format code based on prettier config from ast node. 7 | module.exports = (astNode, filePath) => { 8 | let code = generate(astNode, { comments: false }).code; 9 | code = format(code, filePath, { insertFinalNewline: false }).formatted; 10 | return code; 11 | }; 12 | -------------------------------------------------------------------------------- /core/refactor/identifier.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const traverse = require('@babel/traverse').default; 4 | const common = require('./common'); 5 | 6 | function getDefNode(name, scope) { 7 | // Summary: 8 | // Get the definition node for an identifier 9 | 10 | while (scope) { 11 | if (scope.bindings[name]) return scope.bindings[name].identifier; 12 | scope = scope.parent; 13 | } 14 | return null; 15 | } 16 | 17 | /** 18 | * Rename an top scope identifier in a module. If first finds the definition node of the given name. 19 | * Then rename all identifiers those refer to that definition node. 20 | * @param {string} ast - Which module to rename an identifier. 21 | * @param {string} oldName - The old identifier name. 22 | * @index {string} newName - The new identifier name. 23 | * @index {object} defNode - The definition node of the identifier. If not provided, then find the first definition in the module. 24 | * @alias module:refactor.renameIdentifier 25 | * @example 26 | * // import { m1 } from './some-module'; 27 | * // m1.doSomething(); 28 | * // function () { const m1 = 'abc'; } 29 | * const refactor = require('rekit-core').refactor; 30 | * refactor.renameIdentifier(file, 'm1', 'm2'); 31 | * // => import { m2 } from './some-module'; 32 | * // => m2.doSomething(); 33 | * // => function () { const m1 = 'abc'; } // m1 is not renamed. 34 | **/ 35 | function renameIdentifier(ast, oldName, newName, defNode) { 36 | // Summary: 37 | // Rename identifiers with oldName in ast 38 | const changes = []; 39 | if (!defNode) { 40 | let scope; 41 | traverse(ast, { 42 | Identifier(path) { 43 | if (path.node.name === oldName) { 44 | scope = path.scope; 45 | path.stop(); 46 | } 47 | }, 48 | }); 49 | if (!scope) return changes; 50 | defNode = getDefNode(oldName, scope); 51 | } 52 | 53 | function rename(path) { 54 | if ( 55 | path.node.name === oldName && 56 | path.key !== 'imported' && // it should NOT be imported specifier 57 | getDefNode(path.node.name, path.scope) === defNode 58 | ) { 59 | path.node.name = newName; 60 | changes.push({ 61 | start: path.node.start, 62 | end: path.node.end, 63 | replacement: newName, 64 | }); 65 | } 66 | } 67 | traverse(ast, { 68 | JSXIdentifier: rename, 69 | Identifier: rename, 70 | }); 71 | return changes; 72 | } 73 | 74 | module.exports = { 75 | renameIdentifier: common.acceptFilePathForAst(renameIdentifier), 76 | }; 77 | -------------------------------------------------------------------------------- /core/refactor/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Simple refactor for es6 code. 5 | * @module refactor 6 | **/ 7 | 8 | const common = require('./common'); 9 | const identifier = require('./identifier'); 10 | const array = require('./array'); 11 | const importExport = require('./importExport'); 12 | const object = require('./object'); 13 | const style = require('./style'); 14 | const string = require('./string'); 15 | const cls = require('./cls'); 16 | const func = require('./func'); 17 | const lines = require('./lines'); 18 | const format = require('./format'); 19 | const generate = require('./generate'); 20 | const updateRefs = require('./updateRefs'); 21 | 22 | module.exports = { 23 | // Common 24 | updateSourceCode: common.updateSourceCode, 25 | updateFile: common.updateFile, 26 | isLocalModule: common.isLocalModule, 27 | isSameModuleSource: common.isSameModuleSource, 28 | resolveModulePath: common.resolveModulePath, 29 | acceptFilePathForAst: common.acceptFilePathForAst, 30 | acceptFilePathForLines: common.acceptFilePathForLines, 31 | updateRefs, 32 | 33 | // Identifier 34 | renameIdentifier: identifier.renameIdentifier, 35 | 36 | // Class 37 | renameClassName: cls.renameClassName, 38 | 39 | // Function 40 | renameFunctionName: func.renameFunctionName, 41 | 42 | // Array 43 | addToArrayByNode: array.addToArrayByNode, 44 | removeFromArrayByNode: array.removeFromArrayByNode, 45 | addToArray: array.addToArray, 46 | removeFromArray: array.removeFromArray, 47 | 48 | // Import export 49 | addImportFrom: importExport.addImportFrom, 50 | addExportFrom: importExport.addExportFrom, 51 | 52 | renameImportSpecifier: importExport.renameImportSpecifier, 53 | renameImportAsSpecifier: importExport.renameImportAsSpecifier, 54 | renameExportSpecifier: importExport.renameExportSpecifier, 55 | 56 | removeImportSpecifier: importExport.removeImportSpecifier, 57 | removeImportBySource: importExport.removeImportBySource, 58 | 59 | renameModuleSource: importExport.renameModuleSource, 60 | 61 | // Object 62 | objExpToObj: object.objExpToObj, 63 | addObjectProperty: object.addObjectProperty, 64 | setObjectProperty: object.setObjectProperty, 65 | renameObjectProperty: object.renameObjectProperty, 66 | removeObjectProperty: object.removeObjectProperty, 67 | 68 | // Style 69 | renameCssClassName: style.renameCssClassName, 70 | addStyleImport: style.addStyleImport, 71 | removeStyleImport: style.removeStyleImport, 72 | renameStyleImport: style.renameStyleImport, 73 | renameCssClassInStyle: style.renameCssClassInStyle, 74 | 75 | // String 76 | renameStringLiteral: string.renameStringLiteral, 77 | replaceStringLiteral: string.replaceStringLiteral, 78 | 79 | // Lines 80 | lineIndex: lines.lineIndex, 81 | lastLineIndex: lines.lastLineIndex, 82 | removeLines: lines.removeLines, 83 | 84 | // Format 85 | format, 86 | 87 | // Generate 88 | generate, 89 | }; 90 | -------------------------------------------------------------------------------- /core/refactor/lines.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const common = require('./common'); 5 | 6 | function isStringMatch(str, match) { 7 | if (_.isString(match)) { 8 | return _.includes(str, match); 9 | } else if (_.isFunction(match)) { 10 | return match(str); 11 | } 12 | return match.test(str); 13 | } 14 | 15 | function lineIndex(lines, match, fromMatch) { 16 | if (fromMatch && !_.isNumber(fromMatch)) { 17 | fromMatch = lineIndex(lines, fromMatch); 18 | } 19 | if (_.isString(match)) { 20 | // Match string 21 | return _.findIndex(lines, l => l.indexOf(match) >= 0, fromMatch || 0); 22 | } else if (_.isFunction(match)) { 23 | // Callback 24 | return _.findIndex(lines, match); 25 | } 26 | // console.log('regular: ', _.findIndex(lines, l => match.test(l), fromMatch || 0)); 27 | // Regular expression 28 | return _.findIndex(lines, l => match.test(l), fromMatch || 0); 29 | } 30 | 31 | function lastLineIndex(lines, match) { 32 | if (_.isString(match)) { 33 | // String 34 | return _.findLastIndex(lines, l => l.indexOf(match) >= 0); 35 | } else if (_.isFunction(match)) { 36 | // Callback 37 | return _.findLastIndex(lines, match); 38 | } 39 | 40 | // Regular expression 41 | return _.findLastIndex(lines, l => match.test(l)); 42 | } 43 | 44 | function lineExists(lines, line) { 45 | return lines.indexOf(line) >= 0; 46 | } 47 | 48 | function removeLines(lines, str) { 49 | _.remove(lines, line => isStringMatch(line, str)); 50 | } 51 | 52 | module.exports = { 53 | lineIndex, 54 | lastLineIndex, 55 | lineExists, 56 | removeLines: common.acceptFilePathForLines(removeLines), 57 | }; 58 | -------------------------------------------------------------------------------- /core/refactor/object.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const traverse = require('@babel/traverse').default; 5 | const common = require('./common'); 6 | 7 | function objExpToObj(objExp) { 8 | // only for non-computed properties 9 | const obj = {}; 10 | objExp.properties.forEach(p => { 11 | if (p.computed) return; 12 | obj[p.key.name] = p; 13 | }); 14 | return obj; 15 | } 16 | 17 | // function nearestCharBefore(char, str, index) { 18 | // // Find the nearest char index before given index. skip white space strings 19 | // // If not found, return -1 20 | // // eg: nearestCharBefore(',', '1, 2, 3', 4) => 1 21 | // let i = index - 1; 22 | // while (i >= 0) { 23 | // if (str.charAt(i) === char) return i; 24 | // if (!/\s/.test(str.charAt(i))) return -1; 25 | // i -= 1; 26 | // } 27 | // return -1; 28 | // } 29 | 30 | function addObjectProperty(ast, varName, propName, propValue) { 31 | const changes = []; 32 | traverse(ast, { 33 | VariableDeclarator(path) { 34 | const node = path.node; 35 | if ( 36 | (varName && _.get(node, 'id.name') !== varName) || 37 | _.get(node, 'init.type') !== 'ObjectExpression' 38 | ) 39 | return; 40 | const props = _.get(node, 'init.properties'); 41 | 42 | const multilines = node.loc.start.line !== node.loc.end.line; 43 | // Check if it exists 44 | const targetPropNode = _.find( 45 | props, 46 | p => 47 | _.get(p, 'key.type') === 'Identifier' && _.get(p, 'key.name') === propName && !p.computed, 48 | ); 49 | 50 | let insertPos = node.init.start + 1; 51 | if (props.length) { 52 | insertPos = _.last(props).end; 53 | } 54 | const code = `${propName}: ${propValue}`; 55 | if (!targetPropNode) { 56 | let replacement; 57 | if (multilines) { 58 | const indent = _.repeat(' ', node.loc.end.column - 1); 59 | replacement = `\n${indent} ${code}`; 60 | if (props.length) { 61 | replacement = `,${replacement}`; 62 | } else { 63 | replacement = `${replacement},`; 64 | } 65 | } else { 66 | replacement = ` ${code} `; 67 | if (props.length > 0) { 68 | replacement = `, ${code}`; 69 | } 70 | } 71 | changes.push({ 72 | start: insertPos, 73 | end: insertPos, 74 | replacement, 75 | }); 76 | } else { 77 | console.warn(`Property name '${propName}' already exists for ${varName}.`); 78 | } 79 | }, 80 | }); 81 | return changes; 82 | } 83 | 84 | function setObjectProperty(ast, varName, propName, propValue) { 85 | const changes = []; 86 | traverse(ast, { 87 | VariableDeclarator(path) { 88 | const node = path.node; 89 | if ( 90 | (varName && _.get(node, 'id.name') !== varName) || 91 | _.get(node, 'init.type') !== 'ObjectExpression' 92 | ) 93 | return; 94 | const props = _.get(node, 'init.properties'); 95 | 96 | // Check if it exists 97 | const targetPropNode = _.find( 98 | props, 99 | p => 100 | _.get(p, 'key.type') === 'Identifier' && _.get(p, 'key.name') === propName && !p.computed, 101 | ); 102 | 103 | if (targetPropNode) { 104 | changes.push({ 105 | start: targetPropNode.value.start, 106 | end: targetPropNode.value.end, 107 | replacement: propValue, 108 | }); 109 | } 110 | }, 111 | }); 112 | return changes; 113 | } 114 | 115 | function renameObjectProperty(ast, varName, oldName, newName) { 116 | // Summary: 117 | // Rename the object property and only for non-computed identifier property 118 | // Return: 119 | // All changes needed. 120 | 121 | const changes = []; 122 | traverse(ast, { 123 | VariableDeclarator(path) { 124 | const node = path.node; 125 | if ( 126 | (varName && _.get(node, 'id.name') !== varName) || 127 | _.get(node, 'init.type') !== 'ObjectExpression' 128 | ) 129 | return; 130 | const props = _.get(node, 'init.properties'); 131 | 132 | // const multilines = node.loc.start.line !== node.loc.end.line; 133 | const targetPropNode = _.find( 134 | props, 135 | p => 136 | _.get(p, 'key.type') === 'Identifier' && _.get(p, 'key.name') === oldName && !p.computed, 137 | ); 138 | 139 | if (targetPropNode) { 140 | changes.push({ 141 | start: targetPropNode.key.start, 142 | end: targetPropNode.key.end, 143 | replacement: newName, 144 | }); 145 | } 146 | }, 147 | }); 148 | return changes; 149 | } 150 | 151 | function removeObjectProperty(ast, varName, propName) { 152 | const changes = []; 153 | traverse(ast, { 154 | VariableDeclarator(path) { 155 | const node = path.node; 156 | if ( 157 | (varName && _.get(node, 'id.name') !== varName) || 158 | _.get(node, 'init.type') !== 'ObjectExpression' 159 | ) 160 | return; 161 | const props = _.get(node, 'init.properties'); 162 | 163 | const multilines = node.loc.start.line !== node.loc.end.line; 164 | 165 | const targetPropNode = _.find( 166 | props, 167 | p => 168 | _.get(p, 'key.type') === 'Identifier' && _.get(p, 'key.name') === propName && !p.computed, 169 | ); 170 | 171 | if (targetPropNode) { 172 | const targetIndex = _.indexOf(props, targetPropNode); 173 | let startIndex; 174 | let endIndex; 175 | if (targetIndex > 0) { 176 | startIndex = props[targetIndex - 1].end; 177 | endIndex = targetPropNode.end; 178 | } else { 179 | startIndex = node.init.start + 1; 180 | endIndex = targetPropNode.end + (multilines || targetIndex < props.length - 1 ? 1 : 0); 181 | } 182 | changes.push({ 183 | start: startIndex, 184 | end: endIndex, 185 | replacement: '', 186 | }); 187 | } 188 | }, 189 | }); 190 | return changes; 191 | } 192 | 193 | module.exports = { 194 | objExpToObj, 195 | addObjectProperty: common.acceptFilePathForAst(addObjectProperty), 196 | setObjectProperty: common.acceptFilePathForAst(setObjectProperty), 197 | renameObjectProperty: common.acceptFilePathForAst(renameObjectProperty), 198 | removeObjectProperty: common.acceptFilePathForAst(removeObjectProperty), 199 | }; 200 | -------------------------------------------------------------------------------- /core/refactor/string.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const _ = require("lodash"); 4 | const traverse = require("@babel/traverse").default; 5 | const common = require("./common"); 6 | 7 | function renameStringLiteral(ast, oldName, newName) { 8 | // Summary: 9 | // Rename the string literal in ast 10 | // Return: 11 | // All changes needed. 12 | 13 | const changes = []; 14 | traverse(ast, { 15 | StringLiteral(path) { 16 | // Simple replace literal strings 17 | if (path.node.value === oldName) { 18 | changes.push({ 19 | start: path.node.start + 1, 20 | end: path.node.end - 1, 21 | replacement: newName 22 | }); 23 | } 24 | } 25 | }); 26 | return changes; 27 | } 28 | 29 | function replaceStringLiteral(ast, oldName, newName, fullMatch) { 30 | // Summary: 31 | // Replace the string literal in ast 32 | // Return: 33 | // All changes needed. 34 | 35 | if (typeof fullMatch === "undefined") fullMatch = true; 36 | 37 | const changes = []; 38 | traverse(ast, { 39 | StringLiteral(path) { 40 | // Simply replace literal strings 41 | if (fullMatch && path.node.value === oldName) { 42 | changes.push({ 43 | start: path.node.start + 1, 44 | end: path.node.end - 1, 45 | replacement: newName 46 | }); 47 | } else if (!fullMatch && _.includes(path.node.value, oldName)) { 48 | const i = path.node.value.indexOf(oldName); 49 | const start = path.node.start + 1 + i; 50 | const end = start + oldName.length; 51 | changes.push({ 52 | start, 53 | end, 54 | replacement: newName 55 | }); 56 | } 57 | } 58 | }); 59 | return changes; 60 | } 61 | 62 | module.exports = { 63 | renameStringLiteral: common.acceptFilePathForAst(renameStringLiteral), 64 | replaceStringLiteral: common.acceptFilePathForAst(replaceStringLiteral) 65 | }; 66 | -------------------------------------------------------------------------------- /core/refactor/style.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const traverse = require('@babel/traverse').default; 5 | const common = require('./common'); 6 | const linesManager = require('./lines'); 7 | 8 | function renameCssClassName(ast, oldName, newName) { 9 | // Summary: 10 | // Rename the css class name in a JSXAttribute 11 | // Return: 12 | // All changes needed. 13 | 14 | const changes = []; 15 | // Find the definition node of the className attribute 16 | // const reg = new RegExp(`(^| +)(${oldName})( +|$)`); 17 | traverse(ast, { 18 | StringLiteral(path) { 19 | // Simple replace literal strings 20 | const i = path.node.value.indexOf(oldName); 21 | if (i >= 0) { 22 | changes.push({ 23 | start: path.node.start + i + 1, 24 | end: path.node.start + i + oldName.length + 1, 25 | replacement: newName, 26 | }); 27 | } 28 | }, 29 | }); 30 | return changes; 31 | } 32 | 33 | function renameCssClassInStyle(lines, oldName, newName) { 34 | for (let i = 0; i < lines.length; i++) { 35 | lines[i] = lines[i].replace(`.${oldName}`, `.${newName}`); 36 | } 37 | } 38 | 39 | function addStyleImport(lines, moduleSource) { 40 | const i = linesManager.lastLineIndex(lines, '@import '); 41 | const line = `@import '${moduleSource}';`; 42 | if (!linesManager.lineExists(lines, line)) { 43 | lines.splice(i + 1, 0, line); 44 | } 45 | } 46 | 47 | function removeStyleImport(lines, moduleSource) { 48 | linesManager.removeLines( 49 | lines, 50 | new RegExp(`@import '${_.escapeRegExp(moduleSource)}(\.less|\.scss)?'`), 51 | ); 52 | } 53 | 54 | function renameStyleImport(lines, oldMoudleSource, newModuleSource) { 55 | const i = linesManager.lineIndex( 56 | lines, 57 | new RegExp(`@import '${_.escapeRegExp(oldMoudleSource)}(\.less|\.scss)?'`), 58 | ); 59 | lines[i] = `@import '${newModuleSource}';`; 60 | } 61 | 62 | module.exports = { 63 | renameCssClassName: common.acceptFilePathForAst(renameCssClassName), 64 | addStyleImport: common.acceptFilePathForLines(addStyleImport), 65 | removeStyleImport: common.acceptFilePathForLines(removeStyleImport), 66 | renameStyleImport: common.acceptFilePathForLines(renameStyleImport), 67 | renameCssClassInStyle: common.acceptFilePathForLines(renameCssClassInStyle), 68 | }; 69 | -------------------------------------------------------------------------------- /core/refactor/updateRefs.js: -------------------------------------------------------------------------------- 1 | // rename a module, should update all references 2 | const path = require('path'); 3 | 4 | function updateRefs(src, dest) { 5 | console.log('updateRefs: ', src, dest); 6 | const app = require('../app'); 7 | const refactor = require('./'); 8 | const { elementById } = app.getProjectData(); 9 | const dependents = require('../dependents').getDependents(elementById); 10 | const byId = id => elementById[id]; 11 | if (!byId(src)) throw new Error(`Src element not exist: ${src}`); 12 | if (byId(dest)) throw new Error(`Dest element already exists: ${dest}`); 13 | 14 | dependents[src] && 15 | dependents[src].forEach(id => { 16 | const ele = byId(id); 17 | // update module source to correct path 18 | refactor.renameModuleSource(ele.id, src, dest); 19 | 20 | // If name changed, update the import specifier 21 | const oldName = path.basename(src).replace(/\.[^.]+$/, ''); 22 | const newName = path.basename(dest).replace(/\.[^.]+$/, ''); 23 | console.log(oldName, newName); 24 | if (oldName !== newName) { 25 | // Renamed 26 | refactor.renameImportSpecifier(ele.id, oldName, newName, dest); 27 | } 28 | }); 29 | } 30 | 31 | module.exports = updateRefs; 32 | -------------------------------------------------------------------------------- /core/repo.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const fs = require('fs-extra'); 3 | const https = require('https'); 4 | const download = require('download-git-repo'); 5 | const logger = require('./logger'); 6 | 7 | // Clone a repo to a folder, if folder not exists, create it. 8 | function clone(gitRepo, destDir, errMsg) { 9 | fs.ensureDirSync(destDir); 10 | return new Promise((resolve, reject) => { 11 | const isDirect = /^https?:|^git@/.test(gitRepo); 12 | download(isDirect ? `direct:${gitRepo}` : gitRepo, destDir, { clone: isDirect }, err => { 13 | if (err) { 14 | console.log( 15 | errMsg || 16 | 'Failed to download the boilerplate. The project was not created. Please check and retry.', 17 | ); 18 | console.log(err); 19 | logger.info(err); 20 | reject('CLONE_REPO_FAILED'); 21 | return; 22 | } 23 | resolve(); 24 | }); 25 | }); 26 | } 27 | 28 | // gitRepo: owner/repo#branch , only supports github.com. 29 | // branch defaults to master 30 | function sync(gitRepo, destDir) { 31 | logger.info(`sync repo: ${gitRepo} to ${destDir}`) 32 | return new Promise((resolve, reject) => { 33 | const arr = gitRepo.split('/'); 34 | const owner = arr[0]; 35 | const arr2 = arr[1].split('#'); 36 | const repo = arr2[0]; 37 | const branch = arr2[1] || 'master'; 38 | // Get last commit to decide if it needs sync 39 | https 40 | .get( 41 | { 42 | hostname: 'api.github.com', 43 | path: `/repos/${owner}/${repo}/git/refs/heads/${branch}`, 44 | port: 443, 45 | headers: { 'User-Agent': 'rekit-core' }, 46 | }, 47 | resp => { 48 | let data = ''; 49 | 50 | // A chunk of data has been recieved. 51 | resp.on('data', chunk => { 52 | data += chunk; 53 | }); 54 | 55 | resp.on('end', () => { 56 | try { 57 | const ref = JSON.parse(data); 58 | const lastCommit = ref.object.sha; 59 | if (!fs.existsSync(path.join(destDir, lastCommit))) { 60 | // If a file with the name of commit id doesn't exist, means not synced. 61 | fs.removeSync(destDir); 62 | clone(gitRepo, destDir) 63 | .then(() => { 64 | fs.writeFileSync(path.join(destDir, lastCommit), ''); 65 | resolve(lastCommit); 66 | }) 67 | .catch(reject); 68 | } else { 69 | resolve(); 70 | } 71 | } catch (err) { 72 | reject(err); 73 | } 74 | }); 75 | }, 76 | ) 77 | .on('error', err => { 78 | logger.info('Failed to get last commit from: ' + gitRepo, err); 79 | reject('FAILED_CHECK_GIT_REPO_LATEST_COMMIT'); 80 | }); 81 | }); 82 | } 83 | 84 | module.exports = { 85 | clone, 86 | sync, 87 | }; 88 | -------------------------------------------------------------------------------- /core/template.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Template manager. A simple wrapper of lodash template for using vio internally. 5 | * @module 6 | **/ 7 | 8 | const _ = require('lodash'); 9 | const fs = require('fs'); 10 | const path = require('path'); 11 | const vio = require('./vio'); 12 | const plugin = require('./plugin'); 13 | const paths = require('./paths'); 14 | const config = require('./config'); 15 | 16 | // Make sure it works in template 17 | _.pascalCase = _.flow( 18 | _.camelCase, 19 | _.upperFirst, 20 | ); 21 | _.upperSnakeCase = _.flow( 22 | _.snakeCase, 23 | _.toUpper, 24 | ); 25 | 26 | function getTemplatePath(templateFile, args) { 27 | // Get real template path when generate a file from template 28 | // If templateFile is absolute path, use it directly 29 | // Otherwise it provides general customizable template discovery mechanism in below prioirty 30 | // 1. args.context.templateFiles[templateFile] 31 | // 2. [rekit.json:templateDir || /rekit-templates]//templateFile 32 | // 3. /templates/templateFile 33 | 34 | if (path.isAbsolute(templateFile)) return templateFile; 35 | if (args.context && args.context.templateFiles && args.context.templateFiles[templateFile]) { 36 | templateFile = args.context.templateFiles[templateFile]; // customize which template file is used 37 | } 38 | const [pluginName, tplFile] = templateFile.split(':'); 39 | if (!tplFile) { 40 | // no plugin specified, should look for template in args.cwd 41 | if (!args.cwd) throw new Error('No args.cwd for template ' + templateFile); 42 | return path.join(args.cwd, 'templates', templateFile); 43 | } 44 | 45 | const p = plugin.getPlugin(pluginName); 46 | if (!p) throw new Error('Unknown template file: ' + templateFile + ' because plugin not found.'); 47 | if (!p.root) throw new Error('No physical folder of the plugin: ' + pluginName); 48 | const tplDir1 = path.join(p.root, 'core/templates'); 49 | const tplDir2 = path.join(p.root, 'templates'); // for built-in core plugins 50 | const pluginTplDir = fs.existsSync(tplDir1) ? tplDir1 : tplDir2; 51 | let customTplDir = config.getRekitConfig().templateDir; 52 | if (customTplDir && !path.isAbsolute(customTplDir)) 53 | customTplDir = path.join(paths.map(customTplDir), pluginName); 54 | if (!customTplDir) customTplDir = paths.map('rekit-templates/' + pluginName); 55 | 56 | let realTplFile; 57 | [customTplDir, pluginTplDir].some(d => { 58 | // First find user customized template, then find plugin template 59 | const f = path.join(d, tplFile); 60 | if (fs.existsSync(f)) { 61 | realTplFile = f; 62 | return true; 63 | } 64 | return false; 65 | }); 66 | if (!realTplFile) throw new Error('Template file not found: ' + templateFile); 67 | return realTplFile; 68 | } 69 | 70 | /** 71 | * Process the template file and save the result to virtual IO. 72 | * If the target file has existed, throw fatal error and exit. 73 | * 74 | * @param {string} targetPath - The path to save the result. 75 | * @param {Object} args - The path to save the result. 76 | * @param {string} args.template - The template string to process. 77 | * @param {string} args.templateFile - If no 'template' defined, read the template file to process. One of template and templateFile should be provided. 78 | * @param {Object} args.context - The context to process the template. 79 | * @alias module:template.generate 80 | * 81 | * @example 82 | * const template = require('rekit-core').template; 83 | * 84 | * const tpl = 'hello ${user}!'; 85 | * template.generate('path/to/result.txt', { template: tpl, context: { user: 'Nate' } }); 86 | * 87 | * // Result => create a file 'path/to/result.txt' which contains text: 'hello Nate!' 88 | * // NOTE the result is only in vio, you need to call vio.flush() to write to disk. 89 | **/ 90 | function generate(targetPath, args) { 91 | if (!args.template && !args.templateFile) { 92 | throw new Error('No template or templateFile provided.'); 93 | } 94 | if (args.throwIfExists && vio.fileExists(targetPath)) { 95 | throw new Error('File already exists: ' + targetPath); 96 | } 97 | const tpl = args.template || vio.getContent(getTemplatePath(args.templateFile, args)); 98 | const compiled = _.template(tpl, args.templateOptions || {}); 99 | const result = compiled(args.context || {}); 100 | 101 | vio.save(targetPath, result); 102 | } 103 | 104 | module.exports = { 105 | generate, 106 | }; 107 | -------------------------------------------------------------------------------- /core/ua.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/core/ua.js -------------------------------------------------------------------------------- /core/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs-extra'); 2 | const paths = require('./paths'); 3 | const logger = require('./logger'); 4 | 5 | module.exports = { 6 | fatal(code, msg) { 7 | console.log('Fatal error: ', code, msg); 8 | const err = new Error(msg); 9 | err.code = code; 10 | throw err; 11 | }, 12 | isDirectory(file) { 13 | return fs.statSync(file).isDirectory(file); 14 | }, 15 | useYarn() { 16 | return fs.existsSync(paths.map('yarn.lock')); 17 | }, 18 | addNodePath(p) { 19 | // Add a path to NODE_PATH to find node_modules 20 | const delimiter = process.platform === 'win32' ? ';' : ':'; 21 | 22 | const current = process.env.NODE_PATH; 23 | if (current) process.env.NODE_PATH = current + delimiter + p; 24 | else process.env.NODE_PATH = p; 25 | require('module').Module._initPaths(); 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const app = require('./core/app'); 3 | const element = require('./core/element'); 4 | const plugin = require('./core/plugin'); 5 | const paths = require('./core/paths'); 6 | const files = require('./core/files'); 7 | const vio = require('./core/vio'); 8 | const template = require('./core/template'); 9 | const config = require('./core/config'); 10 | const ast = require('./core/ast'); 11 | const refactor = require('./core/refactor'); 12 | const deps = require('./core/deps'); 13 | const dependents = require('./core/dependents'); 14 | const handleCommand = require('./core/handleCommand'); 15 | const create = require('./core/create'); 16 | const utils = require('./core/utils'); 17 | const logger = require('./core/logger'); 18 | 19 | _.pascalCase = _.flow( 20 | _.camelCase, 21 | _.upperFirst 22 | ); 23 | _.upperSnakeCase = _.flow( 24 | _.snakeCase, 25 | _.toUpper 26 | ); 27 | 28 | global.rekit = { 29 | core: { 30 | app, 31 | paths, 32 | files, 33 | plugin, 34 | element, 35 | vio, 36 | template, 37 | config, 38 | refactor, 39 | ast, 40 | deps, 41 | dependents, 42 | handleCommand, 43 | create, 44 | utils, 45 | logger, 46 | }, 47 | }; 48 | 49 | module.exports = global.rekit; 50 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rekit-core", 3 | "version": "3.0.2", 4 | "description": "Rekit core functions.", 5 | "author": "Nate Wang ", 6 | "license": "MIT", 7 | "homepage": "https://github.com/rekit/rekit-core", 8 | "main": "index.js", 9 | "keywords": [ 10 | "rekit", 11 | "rekit-core", 12 | "react", 13 | "redux", 14 | "react-router" 15 | ], 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/rekit/rekit-core" 19 | }, 20 | "bugs": { 21 | "url": "https://github.com/rekit/rekit-core/issues" 22 | }, 23 | "scripts": { 24 | "test": "node ./scripts/runTests.js", 25 | "jsdoc:build": "jsdoc core -r -c .jsdoc.json --verbose", 26 | "codecov": "codecov" 27 | }, 28 | "nyc": { 29 | "include": [ 30 | "core/**/*.js" 31 | ], 32 | "reporter": [ 33 | "lcov", 34 | "text-summary" 35 | ], 36 | "cache": false, 37 | "sourceMap": false, 38 | "instrument": true 39 | }, 40 | "files": [ 41 | "core", 42 | "plugins", 43 | "index.js", 44 | "package.json" 45 | ], 46 | "dependencies": { 47 | "@babel/generator": "^7.4.0", 48 | "@babel/parser": "^7.4.3", 49 | "@babel/plugin-proposal-optional-chaining": "^7.13.12", 50 | "@babel/traverse": "^7.4.3", 51 | "@babel/types": "^7.4.0", 52 | "chalk": "^2.4.2", 53 | "chokidar": "^3.5.1", 54 | "diff": "^4.0.1", 55 | "download-git-repo": "^1.1.0", 56 | "download-npm-package": "^3.1.12", 57 | "fast-safe-stringify": "^2.0.6", 58 | "fs-extra": "^7.0.1", 59 | "lodash": "^4.17.11", 60 | "minimatch": "^3.0.4", 61 | "prettier": "^1.16.4", 62 | "winston": "^3.2.1" 63 | }, 64 | "devDependencies": { 65 | "chai": "^4.2.0", 66 | "codecov": "^3.2.0", 67 | "eslint": "^5.15.3", 68 | "eslint-config-node": "^4.0.0", 69 | "mocha": "^6.0.2", 70 | "nyc": "^13.3.0" 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /plugins/common-core/app.js: -------------------------------------------------------------------------------- 1 | const { files, paths } = rekit.core; 2 | 3 | function getProjectData(args = {}) { 4 | return files.readDir(paths.getProjectRoot(), { force: args.force }); 5 | } 6 | 7 | module.exports = { 8 | getProjectData, 9 | }; 10 | -------------------------------------------------------------------------------- /plugins/common-core/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'common-core', 3 | app: require('./app'), 4 | isAppPlugin: true, 5 | appType: 'common', 6 | }; 7 | -------------------------------------------------------------------------------- /plugins/common-core/readme.md: -------------------------------------------------------------------------------- 1 | This plugin is for managing the project in original folder structure excluding node_modules .xxx folders. 2 | It reads all files under the project except node_modules and .git files. -------------------------------------------------------------------------------- /plugins/rekit-plugin/app.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | 3 | const getParentPlugin = () => rekit.core.plugin.getPlugin('rekit-react'); 4 | function processProjectData(prjData, args) { 5 | const pp = getParentPlugin(); 6 | if (!pp) throw new Error('Plugin not found: rekit-react'); 7 | pp.app.processProjectData(prjData, args); 8 | const { elements, elementById } = prjData; 9 | 10 | const { files, vio } = rekit.core; 11 | 12 | if (vio.dirExists('core')) { 13 | const id = 'v:_plugin-ext-core'; 14 | const res = files.readDir('core'); 15 | Object.assign(elementById, res.elementById); 16 | elementById[id] = { 17 | id, 18 | name: 'Ext Core', 19 | target: 'core', 20 | type: 'folder-alias', 21 | icon: 'core', 22 | children: res.elements, 23 | }; 24 | _.pull(elements, 'core'); 25 | elements.splice(1, 0, id); 26 | } 27 | 28 | if (elementById['src/ext']) { 29 | const id = 'v:_plugin-ext-ui'; 30 | // For rekit plugin project, there's a ext virtual folder points to src/ext 31 | elementById[id] = { 32 | id, 33 | name: 'Ext UI', 34 | target: 'src/ext', 35 | type: 'folder-alias', 36 | icon: 'ui', 37 | iconColor: '#CDDC39', 38 | children: elementById['src/ext'].children, 39 | }; 40 | elements.splice(2, 0, id); 41 | _.pull(elementById['v:_src-misc'].children, 'src/ext'); 42 | } 43 | } 44 | function getFileProps(...args) { 45 | return getParentPlugin().app.getFileProps(...args); 46 | } 47 | 48 | module.exports = { 49 | processProjectData, 50 | getFileProps, 51 | }; 52 | -------------------------------------------------------------------------------- /plugins/rekit-plugin/appType.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "rekit-plugin", 3 | "name": "Rekit Plugin", 4 | "repo": "rekit/boilerplate-rekit-plugin#master", 5 | "description": "A Rekit plugin project." 6 | } 7 | -------------------------------------------------------------------------------- /plugins/rekit-plugin/hooks.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const utils = require('../rekit-react/utils'); 3 | 4 | const getParentPlugin = () => rekit.core.plugin.getPlugin('rekit-react'); 5 | 6 | module.exports = { 7 | /* Add prefix to state mapping for connected react component 8 | * e.g. 9 | * function mapStateToProps(state) { 10 | * return { 11 | * home: state.pluginMyPlugin.home 12 | * }; 13 | * } 14 | */ 15 | afterAddComponent(elePath, args) { 16 | const pp = getParentPlugin(); 17 | // because this inherit rekit-react plugin, so need manually call inherited hooks 18 | if (_.has(pp, 'hooks.afterAddComponent')) pp.hooks.afterAddComponent(elePath, args); 19 | 20 | if (args.connect) { 21 | const { vio } = rekit.core; 22 | const ele = utils.parseElePath(elePath, 'component'); 23 | const { lastLineIndex } = rekit.core.refactor; 24 | const match = ` ${_.camelCase(ele.feature)}: state.${_.camelCase(ele.feature)}`; 25 | const lines = vio.getLines(ele.modulePath); 26 | const i = lastLineIndex(lines, match); 27 | lines[i] = ` ${_.camelCase(ele.feature)}: state.${_.camelCase( 28 | 'plugin-'+rekit.core.config.getAppName().replace(/^rekit-plugin-/, ''), 29 | )}.${_.camelCase(ele.feature)}`; 30 | } 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /plugins/rekit-plugin/index.js: -------------------------------------------------------------------------------- 1 | const app = require('./app'); 2 | 3 | module.exports = { 4 | name: 'rekit-plugin', 5 | appType: 'rekit-plugin', 6 | inherit: 'rekit-react', 7 | app, 8 | hooks: require('./hooks'), 9 | initialize() { 10 | // inherited from rekit-react 11 | this.prefix.setPrefix(rekit.core.config.getAppName().replace(/^rekit-plugin-/, '')); 12 | }, 13 | }; 14 | -------------------------------------------------------------------------------- /plugins/rekit-plugin/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/plugins/rekit-plugin/logo.png -------------------------------------------------------------------------------- /plugins/rekit-react/appType.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "rekit-react", 3 | "name": "Rekit React", 4 | "repo": "rekit/boilerplate-rekit-react#master", 5 | "description": "Feature oriented SPA application with React, Redux and React Router.", 6 | "args": [ 7 | { 8 | "id": "css", 9 | "label": "CSS Transpiler", 10 | "type": "string", 11 | "widget": "radio-group", 12 | "initialValue": "less", 13 | "options": [["less", "Less"], ["scss", "Sass"]] 14 | }, 15 | { 16 | "id": "examples", 17 | "label": "Examples", 18 | "tooltip": "Whether to include examples code in the project.", 19 | "widget": "checkbox", 20 | "initialValue": true 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /plugins/rekit-react/cli.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | defineArgs({ addCmd }) { 3 | addCmd.addArgument(['--connect', '-c'], { 4 | help: 'Whether to connect to the Redux store. Only used for component.', 5 | action: 'storeTrue', 6 | }); 7 | 8 | addCmd.addArgument(['--url-path', '-u'], { 9 | help: 'The url path added to react router config. Only used for page/component.', 10 | dest: 'urlPath', 11 | }); 12 | 13 | addCmd.addArgument(['--async', '-a'], { 14 | help: 'Whether the action is async using redux-thunk.', 15 | action: 'storeTrue', 16 | }); 17 | 18 | addCmd.addArgument(['--type', '-t'], { 19 | help: 'Component type, functional or class component.', 20 | dest: 'componentType', 21 | choices: ['functional', 'class'], 22 | defaultValue: 'functional', 23 | }); 24 | 25 | addCmd.addArgument(['--hooks'], { 26 | help: 'If functional component, which hooks to include.', 27 | dest: 'hooks', 28 | defaultValue: [], 29 | }); 30 | 31 | addCmd.addArgument(['--selector'], { 32 | help: 'In useAction, which store value to select.', 33 | dest: 'selector', 34 | defaultValue: [], 35 | }); 36 | 37 | addCmd.addArgument(['--allow-auto-effect'], { 38 | help: 'Whether to allow auto effect when use the hook version of action', 39 | dest: 'allowAutoEffect', 40 | action: 'storeTrue', 41 | defaultValue: false, 42 | }); 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /plugins/rekit-react/colors.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/plugins/rekit-react/colors.js -------------------------------------------------------------------------------- /plugins/rekit-react/component.js: -------------------------------------------------------------------------------- 1 | /* eslint no-lonely-if:0 */ 2 | const _ = require('lodash'); 3 | const entry = require('./entry'); 4 | const route = require('./route'); 5 | const style = require('./style'); 6 | const utils = require('./utils'); 7 | const prefix = require('./prefix'); 8 | 9 | const { vio, template, refactor } = rekit.core; 10 | const { parseElePath } = utils; 11 | 12 | // Add a component 13 | // elePath format: home/MyComponent, home/subFolder/MyComponent 14 | function add(elePath, args) { 15 | const { connect, urlPath, componentType } = args; 16 | const ele = parseElePath(elePath, 'component'); 17 | let tplFile; 18 | if (connect) { 19 | if (componentType === 'functional') { 20 | tplFile = 'rekit-react:FuncComponent.js.tpl'; 21 | } else { 22 | tplFile = 'rekit-react:ConnectedComponent.js.tpl'; 23 | } 24 | } else { 25 | if (componentType === 'functional') { 26 | tplFile = 'rekit-react:FuncComponent.js.tpl'; 27 | } else { 28 | tplFile = 'rekit-react:Component.js.tpl'; 29 | } 30 | } 31 | let hooks = args.hooks || []; 32 | if (typeof hooks === 'string') { 33 | hooks = hooks.split(',').map(s => s.trim()); 34 | } 35 | // const tplFile = connect ? 'ConnectedComponent.js.tpl' : 'Component.js.tpl'; 36 | if (vio.fileExists(ele.modulePath)) { 37 | throw new Error(`Failed to add component: target file already exsited: ${ele.modulePath}`); 38 | } 39 | if (vio.fileExists(ele.stylePath)) { 40 | throw new Error(`Failed to add component: target file already exsited: ${ele.stylePath}`); 41 | } 42 | const pre = prefix.getPrefix() ? _.kebabCase(prefix.getPrefix()) + '_' : ''; 43 | template.generate(ele.modulePath, { 44 | templateFile: tplFile, 45 | cwd: __dirname, 46 | context: { ...args, ele, prefix: pre, hooks, componentType }, 47 | }); 48 | 49 | style.add(ele, args); 50 | entry.addToIndex(ele, args); 51 | if (urlPath) { 52 | route.add(ele.path, args); 53 | } 54 | } 55 | 56 | function remove(elePath, args) { 57 | // Remove component module 58 | const ele = parseElePath(elePath, 'component'); 59 | vio.del(ele.modulePath); 60 | 61 | style.remove(ele, args); 62 | entry.removeFromIndex(ele, args); 63 | route.remove(ele.path, args); 64 | } 65 | 66 | function move(source, target, args) { 67 | const sourceEle = parseElePath(source, 'component'); 68 | const targetEle = parseElePath(target, 'component'); 69 | vio.move(sourceEle.modulePath, targetEle.modulePath); 70 | const pre = prefix.getPrefix() ? _.kebabCase(prefix.getPrefix()) + '_' : ''; 71 | const oldCssClass = `${pre}${sourceEle.feature}-${_.kebabCase(sourceEle.name)}`; 72 | const newCssClass = `${pre}${targetEle.feature}-${_.kebabCase(targetEle.name)}`; 73 | 74 | refactor.updateFile(targetEle.modulePath, ast => 75 | [].concat( 76 | refactor.renameClassName(ast, sourceEle.name, targetEle.name), 77 | refactor.renameFunctionName(ast, sourceEle.name, targetEle.name), 78 | refactor.renameCssClassName(ast, oldCssClass, newCssClass), 79 | ), 80 | ); 81 | 82 | if (sourceEle.feature === targetEle.feature) { 83 | entry.renameInIndex(sourceEle.feature, sourceEle.name, targetEle.name); 84 | } else { 85 | entry.removeFromIndex(sourceEle); 86 | entry.addToIndex(targetEle); 87 | } 88 | 89 | style.move(sourceEle, targetEle, args); 90 | route.move(source, target, args); 91 | } 92 | 93 | module.exports = { 94 | add, 95 | remove, 96 | move, 97 | }; 98 | -------------------------------------------------------------------------------- /plugins/rekit-react/constant.js: -------------------------------------------------------------------------------- 1 | const { vio, refactor } = rekit.core; 2 | 3 | function add(feature, name) { 4 | const targetPath = `src/features/${feature}/redux/constants.js`; 5 | const lines = vio.getLines(targetPath); 6 | const i = refactor.lastLineIndex(lines, /^export /); 7 | const code = `export const ${name} = '${name}';`; 8 | if (!lines.includes(code)) { 9 | lines.splice(i + 1, 0, code); 10 | vio.save(targetPath, lines); 11 | } 12 | } 13 | 14 | function rename(feature, oldName, newName) { 15 | const targetPath = `src/features/${feature}/redux/constants.js`; 16 | const lines = vio.getLines(targetPath); 17 | const i = refactor.lineIndex(lines, `export const ${oldName} = '${oldName}';`); 18 | if (i >= 0) { 19 | lines[i] = `export const ${newName} = '${newName}';`; 20 | } 21 | 22 | vio.save(targetPath, lines); 23 | } 24 | 25 | function remove(feature, name) { 26 | const targetPath = `src/features/${feature}/redux/constants.js`; 27 | refactor.removeLines(targetPath, `export const ${name} = '${name}';`); 28 | } 29 | 30 | module.exports = { 31 | add, 32 | remove, 33 | rename, 34 | }; 35 | -------------------------------------------------------------------------------- /plugins/rekit-react/feature.js: -------------------------------------------------------------------------------- 1 | const _ = require('lodash'); 2 | const path = require('path'); 3 | const traverse = require('@babel/traverse').default; 4 | 5 | const entry = require('./entry'); 6 | const utils = require('./utils'); 7 | const constant = require('./constant'); 8 | 9 | const { vio, refactor, config, template, ast } = rekit.core; 10 | const { getTplPath, upperSnakeCase } = utils; 11 | 12 | function add(feature, args = {}) { 13 | feature = _.kebabCase(feature); 14 | const targetDir = `src/features/${feature}`; 15 | if (vio.dirExists(targetDir)) throw new Error('Feature folder already existed: ' + targetDir); 16 | 17 | vio.mkdir(targetDir); 18 | vio.mkdir(`${targetDir}/redux`); 19 | vio.mkdir(`tests/features/${feature}`); 20 | vio.mkdir(`tests/features/${feature}/redux`); 21 | 22 | // Create files from template 23 | [ 24 | 'index.js', 25 | 'route.js', 26 | 'style.' + config.getRekitConfig().css, 27 | 'redux/actions.js', 28 | 'redux/hooks.js', 29 | 'redux/reducer.js', 30 | 'redux/constants.js', 31 | 'redux/initialState.js', 32 | ].forEach(fileName => { 33 | template.generate(`src/features/${feature}/${fileName}`, { 34 | templateFile: 'rekit-react:' + fileName + '.tpl', 35 | cwd: __dirname, 36 | context: { feature, ...args }, 37 | }); 38 | }); 39 | 40 | // Create wrapper reducer for the feature 41 | // template.generate(utils.joinPath(utils.getProjectRoot(), `tests/features/${name}/redux/reducer.test.js`), { 42 | // templateFile: 'redux/reducer.test.js', 43 | // context: { feature: name } 44 | // }); 45 | 46 | entry.addToRootReducer(feature); 47 | entry.addToRouteConfig(feature); 48 | entry.addToRootStyle(feature); 49 | } 50 | 51 | function move(oldName, newName) { 52 | // assert.notEmpty(oldName); 53 | // assert.notEmpty(newName); 54 | // assert.featureExist(oldName); 55 | // assert.featureNotExist(newName); 56 | 57 | oldName = _.kebabCase(oldName); 58 | newName = _.kebabCase(newName); 59 | 60 | // const prjRoot = paths.getProjectRoot(); 61 | 62 | // Move feature folder 63 | const oldFolder = `src/features/${oldName}`; //paths.map('src/features', oldName); 64 | const newFolder = `src/features/${newName}`; 65 | vio.moveDir(oldFolder, newFolder); 66 | console.log('vio.exist: ', `${newFolder}/route.js`, vio.fileExists(`${newFolder}/route.js`)); 67 | 68 | // Update common/routeConfig 69 | entry.renameInRouteConfig(oldName, newName); 70 | 71 | // Update common/rootReducer 72 | entry.renameInRootReducer(oldName, newName); 73 | 74 | // Update styles/index.less 75 | entry.renameInRootStyle(oldName, newName); 76 | 77 | // Update feature/route.js for path if they bind to feature name 78 | refactor.replaceStringLiteral(`src/features/${newName}/route.js`, oldName, newName); // Rename path 79 | 80 | // Try to rename css class names for components 81 | const folder = `src/features/${newName}`; // utils.joinPath(prjRoot, 'src/features', newName); 82 | vio 83 | .ls(folder) 84 | // It simply assumes component file name is pascal case 85 | .filter(f => /^[A-Z]/.test(path.basename(f))) 86 | .forEach(filePath => { 87 | const moduleName = path.basename(filePath).split('.')[0]; 88 | 89 | if (/\.jsx?$/.test(filePath)) { 90 | // For components, update the css class name inside 91 | refactor.updateFile(filePath, ast => 92 | [].concat( 93 | refactor.replaceStringLiteral( 94 | ast, 95 | `${oldName}-${_.kebabCase(moduleName)}`, 96 | `${newName}-${_.kebabCase(moduleName)}`, 97 | false, 98 | ), // rename css class name 99 | ), 100 | ); 101 | } else if (/\.less$|\.s?css$/.test(filePath)) { 102 | // For style update 103 | let lines = vio.getLines(filePath); 104 | const oldCssClass = `${oldName}-${_.kebabCase(moduleName)}`; 105 | const newCssClass = `${newName}-${_.kebabCase(moduleName)}`; 106 | 107 | lines = lines.map(line => line.replace(`.${oldCssClass}`, `.${newCssClass}`)); 108 | vio.save(filePath, lines); 109 | } 110 | }); 111 | 112 | // Rename action constants 113 | const constantsFile = `src/features/${newName}/redux/constants.js`; 114 | const constants = []; 115 | const ast2 = ast.getAst(constantsFile, true); 116 | // vio.assertAst(ast, constantsFile); 117 | traverse(ast2, { 118 | VariableDeclarator(p) { 119 | const name = _.get(p, 'node.id.name'); 120 | if ( 121 | name && 122 | _.startsWith(name, `${upperSnakeCase(oldName)}_`) && 123 | name === _.get(p, 'node.init.value') 124 | ) { 125 | constants.push(name); 126 | } 127 | }, 128 | }); 129 | 130 | constants.forEach(name => { 131 | const oldConstant = name; 132 | const newConstant = name.replace( 133 | new RegExp(`^${upperSnakeCase(oldName)}`), 134 | upperSnakeCase(newName), 135 | ); 136 | constant.rename(newName, oldConstant, newConstant); 137 | }); 138 | 139 | // Rename actions 140 | vio 141 | .ls(`src/features/${newName}/redux`) 142 | // It simply assumes component file name is pascal case 143 | .forEach(filePath => { 144 | if (/\.js$/.test(filePath)) { 145 | refactor.updateFile(filePath, ast => { 146 | let changes = []; 147 | constants.forEach(name => { 148 | const oldConstant = name; 149 | const newConstant = name.replace( 150 | new RegExp(`^${upperSnakeCase(oldName)}`), 151 | upperSnakeCase(newName), 152 | ); 153 | changes = changes.concat(refactor.renameImportSpecifier(ast, oldConstant, newConstant)); 154 | }); 155 | return changes; 156 | }); 157 | } 158 | }); 159 | } 160 | 161 | function remove(feature) { 162 | feature = _.kebabCase(feature); 163 | vio.del(`src/features/${feature}`); 164 | vio.del(`tests/features/${feature}`); 165 | 166 | entry.removeFromRootReducer(feature); 167 | entry.removeFromRouteConfig(feature); 168 | entry.removeFromRootStyle(feature); 169 | } 170 | 171 | module.exports = { 172 | add, 173 | remove, 174 | move, 175 | }; 176 | -------------------------------------------------------------------------------- /plugins/rekit-react/hooks.js: -------------------------------------------------------------------------------- 1 | const test = require('./test'); 2 | 3 | module.exports = { 4 | afterAddComponent(name, args) { 5 | test.add('component', name, args); 6 | }, 7 | afterRemoveComponent(name, args) { 8 | test.remove('component', name, args); 9 | }, 10 | afterMoveComponent(source, target) { 11 | test.move('component', source, target); 12 | }, 13 | afterAddAction(name, args) { 14 | test.add('action', name, args); 15 | }, 16 | afterRemoveAction(name, args) { 17 | test.remove('action', name, args); 18 | }, 19 | afterMoveAction(source, target) { 20 | test.move('action', source, target); 21 | }, 22 | }; 23 | -------------------------------------------------------------------------------- /plugins/rekit-react/index.js: -------------------------------------------------------------------------------- 1 | const app = require('./app'); 2 | const feature = require('./feature'); 3 | const component = require('./component'); 4 | const action = require('./action'); 5 | const hooks = require('./hooks'); 6 | 7 | module.exports = { 8 | name: 'rekit-react', 9 | appType: 'rekit-react', 10 | app, 11 | hooks, 12 | prefix: require('./prefix'), 13 | cli: require('./cli'), 14 | elements: { 15 | feature, 16 | component, 17 | action, 18 | }, 19 | }; 20 | -------------------------------------------------------------------------------- /plugins/rekit-react/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/plugins/rekit-react/logo.png -------------------------------------------------------------------------------- /plugins/rekit-react/prefix.js: -------------------------------------------------------------------------------- 1 | let prefix = null; 2 | 3 | module.exports = { 4 | getPrefix() { 5 | return prefix; 6 | }, 7 | setPrefix(p) { 8 | prefix = p; 9 | return p; 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /plugins/rekit-react/route.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const traverse = require('@babel/traverse').default; 5 | const utils = require('./utils'); 6 | 7 | const { ast, refactor, vio } = rekit.core; 8 | const { parseElePath } = utils; 9 | 10 | function getChildRoutesNode(ast1) { 11 | let arrNode = null; 12 | traverse(ast1, { 13 | ObjectProperty(path) { 14 | const node = path.node; 15 | if ( 16 | _.get(node, 'key.name') === 'childRoutes' && 17 | _.get(node, 'value.type') === 'ArrayExpression' 18 | ) { 19 | arrNode = node.value; 20 | arrNode._filePath = ast1._filePath; 21 | path.stop(); 22 | } 23 | }, 24 | }); 25 | return arrNode; 26 | } 27 | // Add component to a route.js under a feature. 28 | // It imports all component from index.js 29 | function add(elePath, args) { 30 | if (!args || !args.urlPath) return; 31 | const ele = parseElePath(elePath, 'component'); 32 | const routePath = `src/features/${ele.feature}/route.js`; 33 | if (!vio.fileExists(routePath)) { 34 | throw new Error(`route.add failed: file not found ${routePath}`); 35 | } 36 | 37 | const { urlPath } = args; 38 | refactor.addImportFrom(routePath, './', '', ele.name); 39 | 40 | const ast1 = ast.getAst(routePath, true); 41 | const arrNode = getChildRoutesNode(ast1); 42 | if (arrNode) { 43 | const rule = `{ path: '${urlPath}', component: ${ele.name}${ 44 | args.isIndex ? ', isIndex: true' : '' 45 | } }`; 46 | const changes = refactor.addToArrayByNode(arrNode, rule); 47 | const code = refactor.updateSourceCode(vio.getContent(routePath), changes); 48 | vio.save(routePath, code); 49 | } else { 50 | throw new Error( 51 | `You are adding a route rule, but can't find childRoutes property in '${routePath}', please check.` 52 | ); 53 | } 54 | } 55 | 56 | function remove(elePath) { 57 | const ele = parseElePath(elePath, 'component'); 58 | const routePath = `src/features/${ele.feature}/route.js`; 59 | if (!vio.fileExists(routePath)) { 60 | throw new Error(`route.remove failed: file not found ${routePath}`); 61 | } 62 | 63 | refactor.removeImportSpecifier(routePath, ele.name); 64 | 65 | const ast1 = ast.getAst(routePath, true); 66 | const arrNode = getChildRoutesNode(ast1); 67 | if (arrNode) { 68 | let changes = []; 69 | arrNode.elements 70 | .filter(element => 71 | _.find( 72 | element.properties, 73 | p => _.get(p, 'key.name') === 'component' && _.get(p, 'value.name') === ele.name 74 | ) 75 | ) 76 | .forEach(element => { 77 | changes = changes.concat(refactor.removeFromArrayByNode(arrNode, element)); 78 | }); 79 | const code = refactor.updateSourceCode(vio.getContent(routePath), changes); 80 | vio.save(routePath, code); 81 | } else { 82 | utils.fatal( 83 | `You are removing a route rule, but can't find childRoutes property in '${routePath}', please check.` 84 | ); 85 | } 86 | } 87 | 88 | function move(source, target) { 89 | const sourceEle = parseElePath(source, 'component'); 90 | const targetEle = parseElePath(target, 'component'); 91 | 92 | if (sourceEle.feature === targetEle.feature) { 93 | // If in the same feature, rename imported component name 94 | const oldName = sourceEle.name; 95 | const newName = targetEle.name; 96 | const targetRoutePath = `src/features/${targetEle.feature}/route.js`; 97 | 98 | refactor.updateFile(targetRoutePath, ast => 99 | [].concat( 100 | refactor.renameImportSpecifier(ast, oldName, newName), 101 | refactor.renameStringLiteral(ast, _.kebabCase(oldName), _.kebabCase(newName)) // Rename path roughly 102 | ) 103 | ); 104 | } else { 105 | add(target); 106 | remove(source); 107 | } 108 | } 109 | 110 | module.exports = { 111 | add, 112 | remove, 113 | move, 114 | }; 115 | -------------------------------------------------------------------------------- /plugins/rekit-react/style.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * Style manager. It manage style files for components. Usually used with component manage. 5 | * @module 6 | **/ 7 | const _ = require('lodash'); 8 | const entry = require('./entry'); 9 | const prefix = require('./prefix'); 10 | 11 | const { vio, template } = rekit.core; 12 | 13 | function add(ele, args) { 14 | // Create style file for a component 15 | // const ele = utils.parseElePath(elePath, 'style'); 16 | const pre = prefix.getPrefix() ? _.kebabCase(prefix.getPrefix()) + '_' : ''; 17 | template.generate( 18 | ele.stylePath, 19 | Object.assign({}, args, { 20 | templateFile: 'rekit-react:Component.less.tpl', 21 | cwd: __dirname, 22 | context: { 23 | ele, 24 | prefix: pre, 25 | ...args, 26 | }, 27 | }), 28 | ); 29 | 30 | entry.addToStyle(ele); 31 | } 32 | 33 | function remove(ele) { 34 | // Remove style file of a component 35 | // const ele = utils.parseElePath(elePath, 'style'); 36 | vio.del(ele.stylePath); 37 | entry.removeFromStyle(ele); 38 | } 39 | 40 | function move(sourceEle, targetEle) { 41 | // 1. Move File.less to the destination 42 | // 2. Rename css class name 43 | // 3. Update references in the style.less 44 | 45 | vio.move(sourceEle.stylePath, targetEle.stylePath); 46 | const pre = prefix.getPrefix() ? _.kebabCase(prefix.getPrefix()) + '_' : ''; 47 | 48 | let lines = vio.getLines(targetEle.stylePath); 49 | const oldCssClass = `${pre}${_.kebabCase(sourceEle.feature)}-${_.kebabCase(sourceEle.name)}`; 50 | const newCssClass = `${pre}${_.kebabCase(targetEle.feature)}-${_.kebabCase(targetEle.name)}`; 51 | 52 | lines = lines.map(line => line.replace(`.${oldCssClass}`, `.${newCssClass}`)); 53 | vio.save(targetEle.stylePath, lines); 54 | 55 | if (sourceEle.feature === targetEle.feature) { 56 | entry.renameInStyle(sourceEle.feature, sourceEle.name, targetEle.name); 57 | } else { 58 | entry.removeFromStyle(sourceEle); 59 | entry.addToStyle(targetEle); 60 | } 61 | } 62 | 63 | module.exports = { 64 | add, 65 | move, 66 | remove, 67 | }; 68 | -------------------------------------------------------------------------------- /plugins/rekit-react/templates/Component.js.tpl: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | 3 | export default class ${ele.name} extends Component { 4 | static propTypes = { 5 | 6 | }; 7 | 8 | render() { 9 | return ( 10 |
11 | Component content: ${ele.path} 12 |
13 | ); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /plugins/rekit-react/templates/Component.less.tpl: -------------------------------------------------------------------------------- 1 | @import '../../styles/mixins'; 2 | 3 | .${prefix}${_.kebabCase(ele.path)} { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /plugins/rekit-react/templates/Component.test.js.tpl: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { ${ele.name} } from '../../../src/features/${_.kebabCase(ele.feature)}'; 4 | 5 | it('renders node with correct class name', () => { 6 | const renderedComponent = shallow(<${ele.name} />); 7 | expect(renderedComponent.find('.${_.kebabCase(ele.feature)}-${_.kebabCase(ele.name)}').length).toBe(1); 8 | }); 9 | -------------------------------------------------------------------------------- /plugins/rekit-react/templates/Component.test.jsx.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/plugins/rekit-react/templates/Component.test.jsx.tpl -------------------------------------------------------------------------------- /plugins/rekit-react/templates/ConnectedComponent.js.tpl: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import { bindActionCreators } from 'redux'; 4 | import { connect } from 'react-redux'; 5 | import * as actions from './redux/actions'; 6 | 7 | export class ${ele.name} extends Component { 8 | static propTypes = { 9 | ${_.camelCase(ele.feature)}: PropTypes.object.isRequired, 10 | actions: PropTypes.object.isRequired, 11 | }; 12 | 13 | render() { 14 | return ( 15 |
16 | Page Content: ${ele.path} 17 |
18 | ); 19 | } 20 | } 21 | 22 | /* istanbul ignore next */ 23 | function mapStateToProps(state) { 24 | return { 25 | ${_.camelCase(ele.feature)}: state.${_.camelCase(ele.feature)}, 26 | }; 27 | } 28 | 29 | /* istanbul ignore next */ 30 | function mapDispatchToProps(dispatch) { 31 | return { 32 | actions: bindActionCreators({ ...actions }, dispatch) 33 | }; 34 | } 35 | 36 | export default connect( 37 | mapStateToProps, 38 | mapDispatchToProps 39 | )(${ele.name}); 40 | -------------------------------------------------------------------------------- /plugins/rekit-react/templates/ConnectedComponent.test.js.tpl: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { shallow } from 'enzyme'; 3 | import { ${ele.name} } from '../../../src/features/${ele.feature}/${ele.name}'; 4 | 5 | describe('${ele.feature}/${ele.name}', () => { 6 | it('renders node with correct class name', () => { 7 | const props = { 8 | ${_.camelCase(ele.feature)}: {}, 9 | actions: {}, 10 | }; 11 | const renderedComponent = shallow( 12 | <${ele.name} {...props} /> 13 | ); 14 | 15 | expect( 16 | renderedComponent.find('.${_.kebabCase(ele.feature)}-${_.kebabCase(ele.name)}').length 17 | ).toBe(1); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /plugins/rekit-react/templates/FuncComponent.js.tpl: -------------------------------------------------------------------------------- 1 | import React<% if(hooks.length) { print(', { ' + hooks.join(', ') + ' }'); } %> from 'react';<%if (connect) {%> 2 | import {} from './redux/hooks';<%}%> 3 | 4 | export default function ${ele.name}() { 5 | return ( 6 |
7 | Component content: ${ele.path} 8 |
9 | ); 10 | }; 11 | -------------------------------------------------------------------------------- /plugins/rekit-react/templates/index.js.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/plugins/rekit-react/templates/index.js.tpl -------------------------------------------------------------------------------- /plugins/rekit-react/templates/redux/action.js.tpl: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { useDispatch<% if(selector.length > 1) print(', useSelector, shallowEqual')%><% if(selector.length === 1) print(', useSelector')%> } from 'react-redux'; 3 | import { 4 | ${actionType}, 5 | } from './constants'; 6 | 7 | export function ${ele.name}() { 8 | return { 9 | type: ${actionType}, 10 | }; 11 | } 12 | 13 | export function use${_.pascalCase(ele.name)}() { 14 | const dispatch = useDispatch();<% if(selector.length > 1) { %> 15 | const { <%= selector.join(', ')%> } = useSelector(state => ({<% selector.forEach(p => print('\r\n ' + p + ': state.' + ele.feature + '.' + p + ',')) %> 16 | }), shallowEqual);<% } %><% if(selector.length === 1) { %> 17 | const ${selector[0]} = useSelector(state => state.${_.camelCase(ele.feature)}.${selector[0]});<% } %> 18 | const boundAction = useCallback((...params) => dispatch(${ele.name}(...params)), [dispatch]); 19 | return { <% selector.forEach(p => print(p + ', ')) %>${ele.name}: boundAction }; 20 | } 21 | 22 | export function reducer(state, action) { 23 | switch (action.type) { 24 | case ${actionType}: 25 | return { 26 | ...state, 27 | }; 28 | 29 | default: 30 | return state; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /plugins/rekit-react/templates/redux/action.test.js.tpl: -------------------------------------------------------------------------------- 1 | import { 2 | ${actionType}, 3 | } from '../../../../src/features/${ele.feature}/redux/constants'; 4 | 5 | import { 6 | ${ele.name}, 7 | reducer, 8 | } from '../../../../src/features/${ele.feature}/redux/${ele.name}'; 9 | 10 | describe('${ele.feature}/redux/${ele.name}', () => { 11 | it('returns correct action by ${ele.name}', () => { 12 | expect(${ele.name}()).toHaveProperty('type', ${actionType}); 13 | }); 14 | 15 | it('handles action type ${actionType} correctly', () => { 16 | const prevState = {}; 17 | const state = reducer( 18 | prevState, 19 | { type: ${actionType} } 20 | ); 21 | // Should be immutable 22 | expect(state).not.toBe(prevState); 23 | 24 | // TODO: use real case expected value instead of {}. 25 | const expectedState = {}; 26 | expect(state).toEqual(expectedState); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /plugins/rekit-react/templates/redux/action.test.ts.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/plugins/rekit-react/templates/redux/action.test.ts.tpl -------------------------------------------------------------------------------- /plugins/rekit-react/templates/redux/action.ts.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/plugins/rekit-react/templates/redux/action.ts.tpl -------------------------------------------------------------------------------- /plugins/rekit-react/templates/redux/actions.js.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/plugins/rekit-react/templates/redux/actions.js.tpl -------------------------------------------------------------------------------- /plugins/rekit-react/templates/redux/asyncAction.js.tpl: -------------------------------------------------------------------------------- 1 | import { useEffect, useCallback } from 'react'; 2 | import { useDispatch, useSelector, shallowEqual } from 'react-redux'; 3 | import { 4 | ${actionTypes.begin}, 5 | ${actionTypes.success}, 6 | ${actionTypes.failure}, 7 | ${actionTypes.dismissError}, 8 | } from './constants'; 9 | 10 | export function ${ele.name}(args = {}) { 11 | return (dispatch) => { // optionally you can have getState as the second argument 12 | dispatch({ 13 | type: ${actionTypes.begin}, 14 | }); 15 | 16 | const promise = new Promise((resolve, reject) => { 17 | const doRequest = args.error ? Promise.reject(new Error()) : Promise.resolve(); 18 | doRequest.then( 19 | (res) => { 20 | dispatch({ 21 | type: ${actionTypes.success}, 22 | data: res, 23 | }); 24 | resolve(res); 25 | }, 26 | // Use rejectHandler as the second argument so that render errors won't be caught. 27 | (err) => { 28 | dispatch({ 29 | type: ${actionTypes.failure}, 30 | data: { error: err }, 31 | }); 32 | reject(err); 33 | }, 34 | ); 35 | }); 36 | 37 | return promise; 38 | }; 39 | } 40 | 41 | export function dismiss${utils.pascalCase(ele.name)}Error() { 42 | return { 43 | type: ${actionTypes.dismissError}, 44 | }; 45 | } 46 | 47 | export function use${utils.pascalCase(ele.name)}(${allowAutoEffect ? 'params' : ''}) { 48 | const dispatch = useDispatch(); 49 | 50 | const { <% _.forEach(selector, p => print(p + ', ')) %>${ele.name}Pending, ${ele.name}Error } = useSelector( 51 | state => ({<%_.forEach(selector, p => print('\r\n ' + p + ': state.' + _.camelCase(ele.feature) + '.' + p + ',')) %> 52 | ${ele.name}Pending: state.${_.camelCase(ele.feature)}.${ele.name}Pending, 53 | ${ele.name}Error: state.${_.camelCase(ele.feature)}.${ele.name}Error, 54 | }), 55 | shallowEqual, 56 | ); 57 | 58 | const boundAction = useCallback((...args) => { 59 | return dispatch(${ele.name}(...args)); 60 | }, [dispatch]);<% if (allowAutoEffect) { %> 61 | 62 | useEffect(() => { 63 | if (params) boundAction(...(params || [])); 64 | }, [...(params || []), boundAction]); // eslint-disable-line<% } %> 65 | 66 | const boundDismissError = useCallback(() => { 67 | return dispatch(dismiss${utils.pascalCase(ele.name)}Error()); 68 | }, [dispatch]); 69 | 70 | return {<% selector.forEach(p => print('\r\n ' + p + ','))%> 71 | ${ele.name}: boundAction, 72 | ${ele.name}Pending, 73 | ${ele.name}Error, 74 | dismiss${utils.pascalCase(ele.name)}Error: boundDismissError, 75 | }; 76 | } 77 | 78 | export function reducer(state, action) { 79 | switch (action.type) { 80 | case ${actionTypes.begin}: 81 | // Just after a request is sent 82 | return { 83 | ...state, 84 | ${ele.name}Pending: true, 85 | ${ele.name}Error: null, 86 | }; 87 | 88 | case ${actionTypes.success}: 89 | // The request is success 90 | return { 91 | ...state, 92 | ${ele.name}Pending: false, 93 | ${ele.name}Error: null, 94 | }; 95 | 96 | case ${actionTypes.failure}: 97 | // The request is failed 98 | return { 99 | ...state, 100 | ${ele.name}Pending: false, 101 | ${ele.name}Error: action.data.error, 102 | }; 103 | 104 | case ${actionTypes.dismissError}: 105 | // Dismiss the request failure error 106 | return { 107 | ...state, 108 | ${ele.name}Error: null, 109 | }; 110 | 111 | default: 112 | return state; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /plugins/rekit-react/templates/redux/asyncAction.test.js.tpl: -------------------------------------------------------------------------------- 1 | import configureMockStore from 'redux-mock-store'; 2 | import thunk from 'redux-thunk'; 3 | import nock from 'nock'; 4 | 5 | import { 6 | ${asyncActionTypes.begin}, 7 | ${asyncActionTypes.success}, 8 | ${asyncActionTypes.failure}, 9 | ${asyncActionTypes.dismissError}, 10 | } from '../../../../src/features/${ele.feature}/redux/constants'; 11 | 12 | import { 13 | ${ele.name}, 14 | dismiss${_.pascalCase(ele.name)}Error, 15 | reducer, 16 | } from '../../../../src/features/${ele.feature}/redux/${ele.name}'; 17 | 18 | const middlewares = [thunk]; 19 | const mockStore = configureMockStore(middlewares); 20 | 21 | describe('${ele.feature}/redux/${ele.name}', () => { 22 | afterEach(() => { 23 | nock.cleanAll(); 24 | }); 25 | 26 | it('dispatches success action when ${ele.name} succeeds', () => { 27 | const store = mockStore({}); 28 | 29 | return store.dispatch(${ele.name}()) 30 | .then(() => { 31 | const actions = store.getActions(); 32 | expect(actions[0]).toHaveProperty('type', ${asyncActionTypes.begin}); 33 | expect(actions[1]).toHaveProperty('type', ${asyncActionTypes.success}); 34 | }); 35 | }); 36 | 37 | it('dispatches failure action when ${ele.name} fails', () => { 38 | const store = mockStore({}); 39 | 40 | return store.dispatch(${ele.name}({ error: true })) 41 | .catch(() => { 42 | const actions = store.getActions(); 43 | expect(actions[0]).toHaveProperty('type', ${asyncActionTypes.begin}); 44 | expect(actions[1]).toHaveProperty('type', ${asyncActionTypes.failure}); 45 | expect(actions[1]).toHaveProperty('data.error', expect.anything()); 46 | }); 47 | }); 48 | 49 | it('returns correct action by dismiss${_.pascalCase(ele.name)}Error', () => { 50 | const expectedAction = { 51 | type: ${asyncActionTypes.dismissError}, 52 | }; 53 | expect(dismiss${_.pascalCase(ele.name)}Error()).toEqual(expectedAction); 54 | }); 55 | 56 | it('handles action type ${asyncActionTypes.begin} correctly', () => { 57 | const prevState = { ${ele.name}Pending: false }; 58 | const state = reducer( 59 | prevState, 60 | { type: ${asyncActionTypes.begin} } 61 | ); 62 | expect(state).not.toBe(prevState); // should be immutable 63 | expect(state.${ele.name}Pending).toBe(true); 64 | }); 65 | 66 | it('handles action type ${asyncActionTypes.success} correctly', () => { 67 | const prevState = { ${ele.name}Pending: true }; 68 | const state = reducer( 69 | prevState, 70 | { type: ${asyncActionTypes.success}, data: {} } 71 | ); 72 | expect(state).not.toBe(prevState); // should be immutable 73 | expect(state.${ele.name}Pending).toBe(false); 74 | }); 75 | 76 | it('handles action type ${asyncActionTypes.failure} correctly', () => { 77 | const prevState = { ${ele.name}Pending: true }; 78 | const state = reducer( 79 | prevState, 80 | { type: ${asyncActionTypes.failure}, data: { error: new Error('some error') } } 81 | ); 82 | expect(state).not.toBe(prevState); // should be immutable 83 | expect(state.${ele.name}Pending).toBe(false); 84 | expect(state.${ele.name}Error).toEqual(expect.anything()); 85 | }); 86 | 87 | it('handles action type ${asyncActionTypes.dismissError} correctly', () => { 88 | const prevState = { ${ele.name}Error: new Error('some error') }; 89 | const state = reducer( 90 | prevState, 91 | { type: ${asyncActionTypes.dismissError} } 92 | ); 93 | expect(state).not.toBe(prevState); // should be immutable 94 | expect(state.${ele.name}Error).toBe(null); 95 | }); 96 | }); 97 | 98 | -------------------------------------------------------------------------------- /plugins/rekit-react/templates/redux/asyncAction.ts.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/plugins/rekit-react/templates/redux/asyncAction.ts.tpl -------------------------------------------------------------------------------- /plugins/rekit-react/templates/redux/constants.js.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/plugins/rekit-react/templates/redux/constants.js.tpl -------------------------------------------------------------------------------- /plugins/rekit-react/templates/redux/constants.ts.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/plugins/rekit-react/templates/redux/constants.ts.tpl -------------------------------------------------------------------------------- /plugins/rekit-react/templates/redux/hooks.js.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/plugins/rekit-react/templates/redux/hooks.js.tpl -------------------------------------------------------------------------------- /plugins/rekit-react/templates/redux/initialState.js.tpl: -------------------------------------------------------------------------------- 1 | // Initial state is the place you define all initial values for the Redux store of the feature. 2 | // In the 'standard' way, initialState is defined in reducers: http://redux.js.org/docs/basics/Reducers.html 3 | // But when application grows, there will be multiple reducers files, it's not intuitive what data is managed by the whole store. 4 | // So Rekit extracts the initial state definition into a separate module so that you can have 5 | // a quick view about what data is used for the feature, at any time. 6 | 7 | // NOTE: initialState constant is necessary so that Rekit could auto add initial state when creating async actions. 8 | const initialState = { 9 | }; 10 | 11 | export default initialState; 12 | -------------------------------------------------------------------------------- /plugins/rekit-react/templates/redux/initialState.ts.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/plugins/rekit-react/templates/redux/initialState.ts.tpl -------------------------------------------------------------------------------- /plugins/rekit-react/templates/redux/reducer.js.tpl: -------------------------------------------------------------------------------- 1 | // This is the root reducer of the feature. It is used for: 2 | // 1. Load reducers from each action in the feature and process them one by one. 3 | // Note that this part of code is mainly maintained by Rekit, you usually don't need to edit them. 4 | // 2. Write cross-topic reducers. If a reducer is not bound to some specific action. 5 | // Then it could be written here. 6 | // Learn more from the introduction of this approach: 7 | // https://medium.com/@nate_wang/a-new-approach-for-managing-redux-actions-91c26ce8b5da. 8 | 9 | import initialState from './initialState'; 10 | 11 | const reducers = [ 12 | ]; 13 | 14 | export default function reducer(state = initialState, action) { 15 | let newState; 16 | switch (action.type) { 17 | // Handle cross-topic actions here 18 | default: 19 | newState = state; 20 | break; 21 | } 22 | return reducers.reduce((s, r) => r(s, action), newState); 23 | } 24 | -------------------------------------------------------------------------------- /plugins/rekit-react/templates/redux/reducer.test.js.tpl: -------------------------------------------------------------------------------- 1 | import reducer from '../../../../src/features/${_.kebabCase(feature)}/redux/reducer'; 2 | 3 | describe('${_.kebabCase(feature)}/redux/reducer', () => { 4 | it('does nothing if no matched action', () => { 5 | const prevState = {}; 6 | const state = reducer( 7 | prevState, 8 | { type: '__UNKNOWN_ACTION_TYPE__' } 9 | ); 10 | expect(state).toBe(prevState); 11 | }); 12 | 13 | // TODO: add global reducer test if needed. 14 | }); 15 | -------------------------------------------------------------------------------- /plugins/rekit-react/templates/redux/reducer.test.ts.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/plugins/rekit-react/templates/redux/reducer.test.ts.tpl -------------------------------------------------------------------------------- /plugins/rekit-react/templates/redux/reducer.ts.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/plugins/rekit-react/templates/redux/reducer.ts.tpl -------------------------------------------------------------------------------- /plugins/rekit-react/templates/route.js.tpl: -------------------------------------------------------------------------------- 1 | // This is the JSON way to define React Router rules in a Rekit app. 2 | // Learn more from: http://rekit.js.org/docs/routing.html 3 | 4 | import { 5 | } from './'; 6 | 7 | export default { 8 | path: '${_.kebabCase(feature)}', 9 | childRoutes: [ 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /plugins/rekit-react/templates/style.less.tpl: -------------------------------------------------------------------------------- 1 | @import '../../styles/mixins'; 2 | -------------------------------------------------------------------------------- /plugins/rekit-react/templates/style.scss.tpl: -------------------------------------------------------------------------------- 1 | @import '../../styles/mixins'; 2 | -------------------------------------------------------------------------------- /plugins/rekit-react/templates/ts/Component.tsx.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/plugins/rekit-react/templates/ts/Component.tsx.tpl -------------------------------------------------------------------------------- /plugins/rekit-react/templates/ts/ConnectedComponent.tsx.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/plugins/rekit-react/templates/ts/ConnectedComponent.tsx.tpl -------------------------------------------------------------------------------- /plugins/rekit-react/templates/ts/ConnectedCopmonent.test.tsx.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/plugins/rekit-react/templates/ts/ConnectedCopmonent.test.tsx.tpl -------------------------------------------------------------------------------- /plugins/rekit-react/templates/ts/index.ts.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/plugins/rekit-react/templates/ts/index.ts.tpl -------------------------------------------------------------------------------- /plugins/rekit-react/templates/ts/route.ts.tpl: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/plugins/rekit-react/templates/ts/route.ts.tpl -------------------------------------------------------------------------------- /plugins/rekit-react/tests/async-action.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const expect = require('chai').expect; 5 | const helpers = require('./helpers'); 6 | const core = require('../core'); 7 | 8 | const vio = core.vio; 9 | const utils = core.utils; 10 | 11 | const expectFile = helpers.expectFile; 12 | const expectFiles = helpers.expectFiles; 13 | const expectNoFile = helpers.expectNoFile; 14 | const expectNoFiles = helpers.expectNoFiles; 15 | const expectLines = helpers.expectLines; 16 | const expectNoLines = helpers.expectNoLines; 17 | const TEST_FEATURE_NAME = helpers.TEST_FEATURE_NAME; 18 | 19 | const mapFeatureFile = _.partial(utils.mapFeatureFile, TEST_FEATURE_NAME); 20 | const mapTestFile = _.partial(utils.mapTestFile, TEST_FEATURE_NAME); 21 | 22 | describe('cli: async action tests', function() { // eslint-disable-line 23 | this.timeout(10000); 24 | 25 | before(() => { 26 | vio.reset(); 27 | core.addFeature(TEST_FEATURE_NAME); 28 | }); 29 | 30 | it('throw error when no args to add async action', () => { 31 | expect(core.addAsyncAction).to.throw(Error); 32 | }); 33 | 34 | it('throw error when no args to remove async action', () => { 35 | expect(core.removeAction).to.throw(Error); 36 | }); 37 | 38 | it('add async action', () => { 39 | core.addAsyncAction(TEST_FEATURE_NAME, 'async-action'); 40 | const actionTypes = utils.getAsyncActionTypes(TEST_FEATURE_NAME, 'async-action'); 41 | expectLines(mapFeatureFile('redux/constants.js'), [ 42 | `export const ${actionTypes.begin} = '${actionTypes.begin}';`, 43 | `export const ${actionTypes.success} = '${actionTypes.success}';`, 44 | `export const ${actionTypes.failure} = '${actionTypes.failure}';`, 45 | `export const ${actionTypes.dismissError} = '${actionTypes.dismissError}';`, 46 | ]); 47 | expectLines(mapFeatureFile('redux/actions.js'), [ 48 | 'export { asyncAction, dismissAsyncActionError } from \'./asyncAction\';', 49 | ]); 50 | expectFile(mapFeatureFile('redux/asyncAction.js')); 51 | expectFiles([ 52 | 'redux/asyncAction.test.js', 53 | ].map(mapTestFile)); 54 | }); 55 | 56 | it('remove async action', () => { 57 | core.removeAsyncAction(TEST_FEATURE_NAME, 'async-action'); 58 | const actionTypes = utils.getAsyncActionTypes(TEST_FEATURE_NAME, 'async-action'); 59 | expectNoLines(mapFeatureFile('redux/constants.js'), [ 60 | `export const ${actionTypes.begin} = '${actionTypes.begin}';`, 61 | `export const ${actionTypes.success} = '${actionTypes.success}';`, 62 | `export const ${actionTypes.failure} = '${actionTypes.failure}';`, 63 | `export const ${actionTypes.dismissError} = '${actionTypes.dismissError}';`, 64 | ]); 65 | expectNoLines(mapFeatureFile('redux/actions.js'), [ 66 | 'asyncAction', 67 | ]); 68 | expectNoFile(mapFeatureFile('redux/asyncAction.js')); 69 | expectNoFiles([ 70 | 'redux/asyncAction.test.js', 71 | ].map(mapTestFile)); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /plugins/rekit-react/tests/component.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const expect = require('chai').expect; 5 | const helpers = require('./helpers'); 6 | const core = require('../core'); 7 | 8 | const vio = core.vio; 9 | const utils = core.utils; 10 | 11 | const expectFiles = helpers.expectFiles; 12 | const expectNoFiles = helpers.expectNoFiles; 13 | const expectLines = helpers.expectLines; 14 | const expectNoLines = helpers.expectNoLines; 15 | const TEST_FEATURE_NAME = helpers.TEST_FEATURE_NAME; 16 | const mapFeatureFile = _.partial(utils.mapFeatureFile, TEST_FEATURE_NAME); 17 | const mapTestFile = _.partial(utils.mapTestFile, TEST_FEATURE_NAME); 18 | 19 | describe('component', function() { // eslint-disable-line 20 | before(() => { 21 | vio.reset(); 22 | core.addFeature(TEST_FEATURE_NAME); 23 | }); 24 | 25 | it('throw error when no args to add component', () => { 26 | expect(core.addComponent).to.throw(Error); 27 | }); 28 | 29 | it('throw error when no args to remove component', () => { 30 | expect(core.removeComponent).to.throw(Error); 31 | }); 32 | 33 | it('add component', () => { 34 | core.addComponent(TEST_FEATURE_NAME, 'test-component'); 35 | expectFiles([ 36 | 'TestComponent.js', 37 | 'TestComponent.less', 38 | ].map(mapFeatureFile)); 39 | expectLines(mapFeatureFile('style.less'), [ 40 | '@import \'./TestComponent\';' 41 | ]); 42 | expectLines(mapFeatureFile('index.js'), [ 43 | 'export { default as TestComponent } from \'./TestComponent\';', 44 | ]); 45 | expectFiles([ 46 | 'TestComponent.test.js', 47 | ].map(mapTestFile)); 48 | }); 49 | 50 | it('throw error when component already exists', () => { 51 | expect(core.addComponent.bind(core, TEST_FEATURE_NAME, 'test-component')).to.throw(); 52 | }); 53 | 54 | it('add component connected to redux store', () => { 55 | core.addComponent(TEST_FEATURE_NAME, 'redux-component', { connect: true }); 56 | expectLines(mapFeatureFile('ReduxComponent.js'), [ 57 | "import { connect } from 'react-redux';", 58 | ]); 59 | }); 60 | it('add component used in a route rule', () => { 61 | core.addComponent(TEST_FEATURE_NAME, 'route-component', { urlPath: '$auto' }); 62 | expectLines(mapFeatureFile('route.js'), [ 63 | " RouteComponent,", 64 | " { path: 'route-component', name: 'Route component', component: RouteComponent },", 65 | ]); 66 | core.addComponent(TEST_FEATURE_NAME, 'route-component-2', { urlPath: 'my-url' }); 67 | expectLines(mapFeatureFile('route.js'), [ 68 | " { path: 'my-url', name: 'Route component 2', component: RouteComponent2 },", 69 | ]); 70 | }); 71 | 72 | it('rename component', () => { 73 | const source = { feature: TEST_FEATURE_NAME, name: 'test-component' }; 74 | const target = { feature: TEST_FEATURE_NAME, name: 'renamed-component' }; 75 | core.moveComponent(source, target); 76 | 77 | expectNoFiles([ 78 | 'TestComponent.js', 79 | 'TestComponent.less', 80 | ].map(mapFeatureFile)); 81 | expectNoLines(mapFeatureFile('style.less'), [ 82 | 'TestComponent.less' 83 | ]); 84 | expectNoLines(mapFeatureFile('index.js'), [ 85 | 'TestComponent', 86 | ]); 87 | expectNoFiles([ 88 | 'TestComponent.test.js', 89 | ].map(mapTestFile)); 90 | 91 | expectFiles([ 92 | 'RenamedComponent.js', 93 | 'RenamedComponent.less', 94 | ].map(mapFeatureFile)); 95 | expectLines(mapFeatureFile('style.less'), [ 96 | '@import \'./RenamedComponent\';' 97 | ]); 98 | expectLines(mapFeatureFile('index.js'), [ 99 | 'export { default as RenamedComponent } from \'./RenamedComponent\';', 100 | ]); 101 | expectFiles([ 102 | 'RenamedComponent.test.js', 103 | ].map(mapTestFile)); 104 | 105 | // css class name 106 | expectNoLines(mapFeatureFile('RenamedComponent.less'), [ 107 | `${TEST_FEATURE_NAME}-test-component`, 108 | ]); 109 | expectNoLines(mapFeatureFile('RenamedComponent.js'), [ 110 | `${TEST_FEATURE_NAME}-test-component`, 111 | ]); 112 | expectLines(mapFeatureFile('RenamedComponent.less'), [ 113 | `.${TEST_FEATURE_NAME}-renamed-component {`, 114 | ]); 115 | expectLines(mapFeatureFile('RenamedComponent.js'), [ 116 | `
`, 117 | ]); 118 | }); 119 | 120 | it('move component to a different feature', () => { 121 | const TEST_FEATURE_NAME_2 = `${TEST_FEATURE_NAME}-2`; 122 | core.addFeature(TEST_FEATURE_NAME_2); 123 | core.addComponent(TEST_FEATURE_NAME, 'test-component-2'); 124 | 125 | const source = { feature: TEST_FEATURE_NAME, name: 'test-component-2' }; 126 | const target = { feature: TEST_FEATURE_NAME_2, name: 'renamed-component-2' }; 127 | core.moveComponent(source, target); 128 | 129 | expectNoFiles([ 130 | 'TestComponent2.js', 131 | 'TestComponent2.less', 132 | ].map(mapFeatureFile)); 133 | expectNoLines(mapFeatureFile('style.less'), [ 134 | 'TestComponent2.less' 135 | ]); 136 | expectNoLines(mapFeatureFile('index.js'), [ 137 | 'TestComponent2', 138 | ]); 139 | expectNoFiles([ 140 | 'TestComponent2.test.js', 141 | ].map(mapTestFile)); 142 | 143 | const mapFeatureFile2 = _.partial(utils.mapFeatureFile, TEST_FEATURE_NAME_2); 144 | const mapTestFile2 = _.partial(utils.mapTestFile, TEST_FEATURE_NAME_2); 145 | expectFiles([ 146 | 'RenamedComponent2.js', 147 | 'RenamedComponent2.less', 148 | ].map(mapFeatureFile2)); 149 | expectLines(mapFeatureFile2('style.less'), [ 150 | '@import \'./RenamedComponent2\';' 151 | ]); 152 | expectLines(mapFeatureFile2('index.js'), [ 153 | 'export { default as RenamedComponent2 } from \'./RenamedComponent2\';', 154 | ]); 155 | expectFiles([ 156 | 'RenamedComponent2.test.js', 157 | ].map(mapTestFile2)); 158 | 159 | // css class name 160 | expectNoLines(mapFeatureFile2('RenamedComponent2.less'), [ 161 | `${TEST_FEATURE_NAME_2}-test-component-2`, 162 | ]); 163 | expectNoLines(mapFeatureFile2('RenamedComponent2.js'), [ 164 | `${TEST_FEATURE_NAME_2}-test-component-2`, 165 | ]); 166 | expectLines(mapFeatureFile2('RenamedComponent2.less'), [ 167 | `.${TEST_FEATURE_NAME_2}-renamed-component-2 {`, 168 | ]); 169 | expectLines(mapFeatureFile2('RenamedComponent2.js'), [ 170 | `
`, 171 | ]); 172 | }); 173 | 174 | it('remove component', () => { 175 | core.removeComponent(TEST_FEATURE_NAME, 'test-component'); 176 | expectNoFiles([ 177 | 'TestComponent.js', 178 | 'TestComponent.less', 179 | ].map(mapFeatureFile)); 180 | expectNoLines(mapFeatureFile('style.less'), [ 181 | '@import \'./TestComponent.less\';' 182 | ]); 183 | expectNoLines(mapFeatureFile('index.js'), [ 184 | 'import TestComponent from \'./TestComponent\';', 185 | ' TestComponent,', 186 | ]); 187 | expectNoFiles([ 188 | 'TestComponent.test.js', 189 | ].map(mapTestFile)); 190 | }); 191 | }); 192 | -------------------------------------------------------------------------------- /plugins/rekit-react/tests/constant.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const helpers = require('./helpers'); 5 | const core = require('../core'); 6 | 7 | const vio = core.vio; 8 | const utils = core.utils; 9 | const constant = core.constant; 10 | 11 | const expectLines = helpers.expectLines; 12 | const expectNoLines = helpers.expectNoLines; 13 | const TEST_FEATURE_NAME = helpers.TEST_FEATURE_NAME; 14 | 15 | const mapReduxFile = _.partial(utils.mapReduxFile, TEST_FEATURE_NAME); 16 | 17 | describe('constant', function() { // eslint-disable-line 18 | const targetPath = mapReduxFile('constants'); 19 | before(() => { 20 | vio.reset(); 21 | core.addFeature(TEST_FEATURE_NAME); 22 | }); 23 | 24 | it('add constant adds constant at end', () => { 25 | constant.add(TEST_FEATURE_NAME, 'CONST_1'); 26 | expectLines(targetPath, [ 27 | "export const CONST_1 = 'CONST_1';", 28 | ]); 29 | }); 30 | 31 | it('rename constant rename constant name and value', () => { 32 | constant.rename(TEST_FEATURE_NAME, 'CONST_1', 'NEW_CONST_1'); 33 | expectNoLines(targetPath, [ 34 | "export const CONST_1 = 'CONST_1';", 35 | ]); 36 | expectLines(targetPath, [ 37 | "export const NEW_CONST_1 = 'NEW_CONST_1';", 38 | ]); 39 | }); 40 | 41 | it('remove constant removes constant line', () => { 42 | constant.remove(TEST_FEATURE_NAME, 'NEW_CONST_1'); 43 | expectNoLines(targetPath, [ 44 | "export const NEW_CONST_1 = 'NEW_CONST_1';", 45 | ]); 46 | }); 47 | }); 48 | -------------------------------------------------------------------------------- /plugins/rekit-react/tests/feature.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const _ = require('lodash'); 5 | const helpers = require('./helpers'); 6 | const core = require('../core'); 7 | 8 | const vio = core.vio; 9 | const utils = core.utils; 10 | 11 | const expectFiles = helpers.expectFiles; 12 | const expectNoFile = helpers.expectNoFile; 13 | const expectNoFiles = helpers.expectNoFiles; 14 | const expectLines = helpers.expectLines; 15 | const expectNoLines = helpers.expectNoLines; 16 | const TEST_FEATURE_NAME = helpers.TEST_FEATURE_NAME; 17 | const TEST_FEATURE_NAME_2 = helpers.TEST_FEATURE_NAME_2; 18 | const CAMEL_TEST_FEATURE_NAME = _.camelCase(TEST_FEATURE_NAME); 19 | const CAMEL_TEST_FEATURE_NAME_2 = _.camelCase(TEST_FEATURE_NAME_2); 20 | 21 | const mapFeatureFile = _.partial(utils.mapFeatureFile, TEST_FEATURE_NAME); 22 | const mapFeatureFile2 = _.partial(utils.mapFeatureFile, TEST_FEATURE_NAME_2); 23 | const mapTestFile = _.partial(utils.mapTestFile, TEST_FEATURE_NAME); 24 | const mapTestFile2 = _.partial(utils.mapTestFile, TEST_FEATURE_NAME_2); 25 | const mapSrcFile = utils.mapSrcFile; 26 | 27 | describe('feature', function() { // eslint-disable-line 28 | 29 | before(() => { 30 | // To reset test env 31 | vio.reset(); 32 | }); 33 | 34 | it('throw error when no args to add feature', () => { 35 | expect(core.addFeature).to.throw(Error); 36 | }); 37 | 38 | it('add feature', () => { 39 | core.addFeature(TEST_FEATURE_NAME); 40 | expectFiles([ 41 | 'redux/actions.js', 42 | 'redux/constants.js', 43 | 'redux/reducer.js', 44 | 'redux/initialState.js', 45 | 'index.js', 46 | 'route.js', 47 | 'DefaultPage.js', 48 | 'DefaultPage.less', 49 | 'style.less', 50 | ].map(mapFeatureFile)); 51 | 52 | expectLines(mapSrcFile('common/rootReducer.js'), [ 53 | `import ${CAMEL_TEST_FEATURE_NAME}Reducer from '../features/${TEST_FEATURE_NAME}/redux/reducer';`, 54 | ` ${CAMEL_TEST_FEATURE_NAME}: ${CAMEL_TEST_FEATURE_NAME}Reducer,`, 55 | ]); 56 | expectLines(mapSrcFile('common/routeConfig.js'), [ 57 | `import ${CAMEL_TEST_FEATURE_NAME}Route from '../features/${TEST_FEATURE_NAME}/route';`, 58 | ` ${CAMEL_TEST_FEATURE_NAME}Route,`, 59 | ]); 60 | expectLines(mapSrcFile('styles/index.less'), [ 61 | `@import '../features/${TEST_FEATURE_NAME}/style';`, 62 | ]); 63 | expectFiles([ 64 | 'redux/reducer.test.js', 65 | ].map(mapTestFile)); 66 | }); 67 | 68 | it('rename feature', () => { 69 | core.moveFeature(TEST_FEATURE_NAME, TEST_FEATURE_NAME_2); 70 | expectNoLines(mapSrcFile('common/rootReducer.js'), [ 71 | CAMEL_TEST_FEATURE_NAME, 72 | ]); 73 | expectNoLines(mapSrcFile('common/routeConfig.js'), [ 74 | TEST_FEATURE_NAME, 75 | ]); 76 | expectNoLines(mapSrcFile('styles/index.less'), [ 77 | TEST_FEATURE_NAME, 78 | ]); 79 | 80 | expectLines(mapSrcFile('common/rootReducer.js'), [ 81 | `import ${CAMEL_TEST_FEATURE_NAME_2}Reducer from '../features/${TEST_FEATURE_NAME_2}/redux/reducer';`, 82 | ` ${CAMEL_TEST_FEATURE_NAME_2}: ${CAMEL_TEST_FEATURE_NAME_2}Reducer,`, 83 | ]); 84 | expectLines(mapSrcFile('common/routeConfig.js'), [ 85 | `import ${CAMEL_TEST_FEATURE_NAME_2}Route from '../features/${TEST_FEATURE_NAME_2}/route';`, 86 | ` ${CAMEL_TEST_FEATURE_NAME_2}Route,`, 87 | ]); 88 | expectLines(mapSrcFile('styles/index.less'), [ 89 | `@import '../features/${TEST_FEATURE_NAME_2}/style';`, 90 | ]); 91 | 92 | expectLines(mapFeatureFile2('DefaultPage.js'), [ 93 | `
`, 94 | ]); 95 | expectLines(mapFeatureFile2('DefaultPage.less'), [ 96 | `.${TEST_FEATURE_NAME_2}-default-page {`, 97 | ]); 98 | 99 | // verify test files 100 | expectLines(mapTestFile2('DefaultPage.test.js'), [ 101 | `import { DefaultPage } from 'src/features/${TEST_FEATURE_NAME_2}/DefaultPage';`, 102 | `describe('${TEST_FEATURE_NAME_2}/DefaultPage', () => {`, 103 | ` renderedComponent.find('.${TEST_FEATURE_NAME_2}-default-page').getElement()`, 104 | ]); 105 | 106 | expectLines(mapTestFile2('redux/reducer.test.js'), [ 107 | `import reducer from 'src/features/${TEST_FEATURE_NAME_2}/redux/reducer';`, 108 | ]); 109 | }); 110 | 111 | it('remove feature', () => { 112 | core.removeFeature(TEST_FEATURE_NAME_2); 113 | expectNoFile(mapFeatureFile('')); 114 | expectNoFile(mapTestFile('')); 115 | expectNoLines(mapSrcFile('common/rootReducer.js'), [ 116 | CAMEL_TEST_FEATURE_NAME_2, 117 | ]); 118 | expectNoLines(mapSrcFile('common/routeConfig.js'), [ 119 | `${TEST_FEATURE_NAME_2}Route`, 120 | ]); 121 | expectNoLines(mapSrcFile('styles/index.less'), [ 122 | `@import '../features/${TEST_FEATURE_NAME_2}/style';`, 123 | ]); 124 | }); 125 | }); 126 | -------------------------------------------------------------------------------- /plugins/rekit-react/tests/route.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const helpers = require('./helpers'); 5 | const core = require('../core'); 6 | 7 | const vio = core.vio; 8 | const utils = core.utils; 9 | const route = core.route; 10 | 11 | const expectLines = helpers.expectLines; 12 | const expectNoLines = helpers.expectNoLines; 13 | const TEST_FEATURE_NAME = helpers.TEST_FEATURE_NAME; 14 | const TEST_FEATURE_NAME_2 = helpers.TEST_FEATURE_NAME_2; 15 | 16 | const mapFeatureFile = _.partial(utils.mapFeatureFile, TEST_FEATURE_NAME); 17 | const mapFeatureFile2 = _.partial(utils.mapFeatureFile, TEST_FEATURE_NAME_2); 18 | 19 | describe('route', function() { // eslint-disable-line 20 | const targetPath = mapFeatureFile('route.js'); 21 | const targetPath2 = mapFeatureFile2('route.js'); 22 | before(() => { 23 | vio.reset(); 24 | core.addFeature(TEST_FEATURE_NAME); 25 | core.addFeature(TEST_FEATURE_NAME_2); 26 | }); 27 | 28 | it('add route for a component', () => { 29 | route.add(TEST_FEATURE_NAME, 'test-component'); 30 | expectLines(targetPath, [ 31 | " TestComponent,", 32 | " { path: 'test-component', name: 'Test component', component: TestComponent },", 33 | ]); 34 | }); 35 | 36 | it('remove route for a component', () => { 37 | route.remove(TEST_FEATURE_NAME, 'test-component'); 38 | expectNoLines(targetPath, [ 39 | "TestComponent", 40 | "component: TestComponent },", 41 | ]); 42 | }); 43 | 44 | it('add route for a component with custom url path', () => { 45 | route.add(TEST_FEATURE_NAME, 'test-component-2', { urlPath: 'my-url' }); 46 | expectLines(targetPath, [ 47 | " TestComponent2,", 48 | " { path: 'my-url', name: 'Test component 2', component: TestComponent2 },", 49 | ]); 50 | }); 51 | 52 | it('rename route for a component', () => { 53 | route.add(TEST_FEATURE_NAME, 'to-rename'); 54 | const source = { feature: TEST_FEATURE_NAME, name: 'to-rename' }; 55 | const target = { feature: TEST_FEATURE_NAME, name: 'renamed-component' }; 56 | route.move(source, target); 57 | expectNoLines(targetPath, [ 58 | "ToRename", 59 | ]); 60 | expectLines(targetPath, [ 61 | " RenamedComponent,", 62 | " { path: 'renamed-component', name: 'Renamed component', component: RenamedComponent },", 63 | ]); 64 | }); 65 | 66 | it('move route to a different feature', () => { 67 | const source = { feature: TEST_FEATURE_NAME, name: 'test-component-2' }; 68 | const target = { feature: TEST_FEATURE_NAME_2, name: 'renamed-component-2' }; 69 | route.move(source, target); 70 | expectNoLines(targetPath, [ 71 | "TestComponent2", 72 | ]); 73 | expectLines(targetPath2, [ 74 | " RenamedComponent2,", 75 | " { path: 'my-url', name: 'Renamed component 2', component: RenamedComponent2 },", 76 | ]); 77 | }); 78 | 79 | it('move route to a differnt feature should brings path and name together', () => { 80 | route.add(TEST_FEATURE_NAME, 'to-move'); 81 | const source = { feature: TEST_FEATURE_NAME, name: 'to-move' }; 82 | const target = { feature: TEST_FEATURE_NAME_2, name: 'to-move-done' }; 83 | // simulate manually edit route 84 | vio.put(targetPath, vio.getContent(targetPath) 85 | .replace("path: 'to-move'", "path: '/to/move'") 86 | .replace("name: 'To move'", "name: 'A new name'") 87 | ); 88 | route.move(source, target); 89 | expectLines(targetPath2, [ 90 | " ToMoveDone,", 91 | " { path: '/to/move', name: 'A new name', component: ToMoveDone },", 92 | ]); 93 | }); 94 | 95 | it('remove route for a component', () => { 96 | route.remove(TEST_FEATURE_NAME, 'renamed-component'); 97 | expectNoLines(targetPath, [ 98 | "RenamedComponent", 99 | "component: RenamedComponent },", 100 | ]); 101 | }); 102 | }); 103 | -------------------------------------------------------------------------------- /plugins/rekit-react/tools/craToRekit.js: -------------------------------------------------------------------------------- 1 | // Convert a create-react-app to Rekit. 2 | // This tool is only used for generating react-rekit-boilerplate 3 | 4 | // 1. Create react app 5 | // 2. Eject 6 | // 3. Update index.js, webpack config 7 | // 4. Creating folders/files 8 | // 5. 9 | 10 | const fs = require('fs-extra'); 11 | const path = require('path'); 12 | const spawn = require('child_process').spawn; 13 | const _ = require('lodash'); 14 | 15 | const srcPrjDir = path.join(__dirname, '../../../../../../rekit-boilerplate-cra'); 16 | const destPrjDir = path.join(__dirname, '../../../../../../cra3'); 17 | 18 | function createApp(dir) {} 19 | 20 | function convertApp(destPrjDir) { 21 | console.log('Converting project to Rekit: ', destPrjDir); 22 | // Copy files 23 | [ 24 | 'src/features', 25 | 'src/common', 26 | 'src/styles', 27 | 'src/images', 28 | 'src/Root.js', 29 | 'tests', 30 | 'postCreate.js', 31 | 'babel.config.js', 32 | // '.babelrc', 33 | '.prettierrc', 34 | ].forEach(file => { 35 | const absSrcPath = path.join(srcPrjDir, file); 36 | const absDestPath = path.join(destPrjDir, file); 37 | if (fs.existsSync(absDestPath)) { 38 | console.warn('Dest file exists: ', absDestPath); 39 | return; 40 | } 41 | if (fs.existsSync(absSrcPath)) { 42 | console.log('Creating file(s): ', absDestPath); 43 | fs.copy(absSrcPath, absDestPath); 44 | } else { 45 | console.warn('Src file(s) does not exist: ', absSrcPath); 46 | } 47 | }); 48 | 49 | // Update index.js 50 | // 1. Enable react-hot-loader 51 | 52 | // Update config/webpack.config.js 53 | // 1. Enable react-hot-loader 54 | 55 | 56 | // Install deps 57 | console.log('Installing dependencies...'); 58 | const destDeps = Object.keys(require(path.join(destPrjDir, 'package.json')).dependencies); 59 | const srcDeps = Object.keys(require(path.join(srcPrjDir, 'package.json')).dependencies); 60 | const diffDeps = _.difference(srcDeps, destDeps); 61 | console.log('diff deps: ', diffDeps); 62 | // const args = [ 63 | // 'add', 64 | // 'axios', 65 | // 'less', 66 | // 'less-loader', 67 | // 'nock', 68 | // 'redux', 69 | // 'redux-logger', 70 | // 'redux-thunk', 71 | // 'react-router-dom', 72 | // 'react-redux', 73 | // '--dev', 74 | // ]; 75 | // spawn('yarn', args, { 76 | // cwd: destPrjDir, 77 | // stdio: 'pipe', 78 | // }); 79 | } 80 | 81 | if (!fs.existsSync(destPrjDir)) createApp(destPrjDir); 82 | 83 | if (fs.existsSync(destPrjDir)) convertApp(destPrjDir); 84 | -------------------------------------------------------------------------------- /plugins/rekit-react/utils.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const path = require('path'); 3 | const _ = require('lodash'); 4 | const prefix = require('./prefix'); 5 | 6 | const { config, paths } = rekit.core; 7 | 8 | const pascalCase = _.flow( 9 | _.camelCase, 10 | _.upperFirst, 11 | ); 12 | 13 | const upperSnakeCase = _.flow( 14 | _.snakeCase, 15 | _.toUpper, 16 | ); 17 | 18 | // Parse and normalize an element paths, names 19 | function parseElePath(elePath, type = 'component') { 20 | const arr = elePath.split('/'); 21 | const feature = _.kebabCase(arr.shift()); 22 | let name = arr.pop(); 23 | if (type === 'action') name = _.camelCase(name); 24 | else if (type === 'component') name = pascalCase(name); 25 | else throw new Error('Unknown element type: ' + type + ' of ' + elePath); 26 | elePath = [feature, ...arr, name].join('/'); 27 | 28 | const ele = { 29 | name, 30 | path: elePath, 31 | feature, 32 | }; 33 | 34 | if (type === 'component') { 35 | ele.modulePath = `src/features/${elePath}.js`; 36 | ele.testPath = `tests/features/${elePath}.test.js`; 37 | ele.stylePath = `src/features/${elePath}.${config.getRekitConfig().css}`; 38 | } else if (type === 'action') { 39 | ele.modulePath = `src/features/${feature}/redux/${name}.js`; 40 | ele.testPath = `tests/features/${feature}/redux/${name}.test.js`; 41 | } 42 | return ele; 43 | } 44 | 45 | /** 46 | * Get action type constant for a sync action. It uses UPPER_SNAKE_CASE and combines feature name and action name. 47 | * @param {string} feature - The feature name of the action. 48 | * @param {string} action - The action name. 49 | * 50 | * @example 51 | * const utils = require('rekit-core').utils; 52 | * utils.getActionType('home', 'doSomething'); 53 | * // => HOME_DO_SOMETHING 54 | **/ 55 | function getActionType(feature, action) { 56 | const pre = prefix.getPrefix() ? upperSnakeCase(prefix.getPrefix()) + '$' : ''; 57 | return `${pre}${upperSnakeCase(feature)}_${upperSnakeCase(action)}`; 58 | } 59 | 60 | /** 61 | * Get action type constants for an async action. It uses UPPER_SNAKE_CASE and combines feature name and action name. 62 | * @param {string} feature - The feature name of the action. 63 | * @param {string} action - The action name. 64 | * 65 | * @example 66 | * const utils = require('rekit-core').utils; 67 | * utils.getAsyncActionTypes('home', 'doAsync'); 68 | * // => 69 | * // { 70 | * // doAsyncBegin: 'HOME_DO_ASYNC_BEGIN', 71 | * // doAsyncSuccess: 'HOME_DO_ASYNC_SUCCESS', 72 | * // doAsyncFailure: 'HOME_DO_ASYNC_FAILURE', 73 | * // doAsyncDismissError: 'HOME_DO_ASYNC_DISMISS_ERROR', 74 | * // } 75 | **/ 76 | function getAsyncActionTypes(feature, action) { 77 | const f = upperSnakeCase(feature); 78 | const a = upperSnakeCase(action); 79 | const pre = prefix.getPrefix() ? upperSnakeCase(prefix.getPrefix()) + '$' : ''; 80 | return { 81 | normal: getActionType(feature, action), 82 | begin: `${pre}${f}_${a}_BEGIN`, 83 | success: `${pre}${f}_${a}_SUCCESS`, 84 | failure: `${pre}${f}_${a}_FAILURE`, 85 | dismissError: `${pre}${f}_${a}_DISMISS_ERROR`, 86 | }; 87 | } 88 | 89 | // Get the template file path 90 | function getTplPath(tpl) { 91 | const tplFile = path.join(__dirname, './templates', tpl); 92 | const customTplDir = 93 | _.get(config.getRekitConfig(), 'rekitReact.templateDir') || 94 | path.join(paths.map('.rekit-react/templates')); 95 | const customTplFile = path.join(customTplDir, tpl); 96 | return fs.existsSync(customTplFile) ? customTplFile : tplFile; 97 | } 98 | 99 | module.exports = { 100 | parseElePath, 101 | getActionType, 102 | getAsyncActionTypes, 103 | getTplPath, 104 | pascalCase, 105 | upperSnakeCase, 106 | }; 107 | -------------------------------------------------------------------------------- /rekit.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [], 3 | "exclude": ["tmp", ".nyc_output", ".*", "package-lock.json", "coverage"], 4 | "appType": "common", 5 | "diagram": { 6 | "include": [], 7 | "exclude": [] 8 | }, 9 | "astFolders": [] 10 | } 11 | -------------------------------------------------------------------------------- /scripts/copyTo.js: -------------------------------------------------------------------------------- 1 | /* This script is used to copy SDK pkg to a plugin project to test if SDK works. */ 2 | 3 | const fs = require('fs-extra'); 4 | const path = require('path'); 5 | 6 | // build sdk first 7 | 8 | let destPrj = process.argv[2]; 9 | if (!path.isAbsolute(destPrj)) destPrj = path.join(process.cwd(), destPrj); 10 | 11 | const dest = path.join(destPrj, 'node_modules/rekit-core'); 12 | 13 | if (!fs.existsSync(dest)) { 14 | throw new Error('Dest folder not exists: ' + dest); 15 | } 16 | const src = path.join(__dirname, '..'); 17 | 18 | fs.copySync(src, dest, { 19 | filter(srcFile) { 20 | return !/node_modules|package\.json|\.git/.test(srcFile);//src.includes('node_modules'); 21 | } 22 | }) 23 | console.log('Done. SDK copied to: ', dest); -------------------------------------------------------------------------------- /scripts/runTests.js: -------------------------------------------------------------------------------- 1 | /* 2 | Summary: 3 | Run specific tests 4 | Usage examples: 5 | - node runTests.js // run all tests 6 | - node runTests.js refactor.test.js // run refactor tests 7 | */ 8 | 9 | 'use strict'; 10 | 11 | const path = require('path'); 12 | const { spawn } = require('child_process'); 13 | 14 | const prjRoot = path.join(__dirname, '../'); 15 | 16 | let testFile = process.argv[2]; 17 | let needReport = false; 18 | if (!testFile) { 19 | needReport = true; 20 | testFile = path.join(prjRoot, 'tests/**/*.test.js'); 21 | } else { 22 | testFile = path.join(prjRoot, 'tests', testFile); 23 | } 24 | console.log('Running tests: ', testFile.replace(prjRoot, ''), '...'); 25 | 26 | const env = Object.create(process.env); 27 | env.NODE_ENV = 'test'; 28 | 29 | const params = [ 30 | 'mocha', 31 | '--require', 32 | 'tests/before-all.js', 33 | `${testFile}`, 34 | // "--exit" 35 | ]; 36 | 37 | if (needReport) { 38 | params.splice(0, 0, 'nyc', '--report-dir=coverage'); 39 | } 40 | const opts = { 41 | cwd: prjRoot, 42 | stdio: 'inherit', 43 | env, 44 | }; 45 | spawn('npx', params, opts); 46 | 47 | if (needReport) { 48 | console.log('Report: ', path.join(prjRoot, 'coverage/lcov-report/index.html')); 49 | } 50 | -------------------------------------------------------------------------------- /tests/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "func-names": 0, 4 | "prefer-arrow-callback": 0, 5 | "no-unused-expressions": 0, 6 | "object-curly-newline": 0, 7 | "import/no-extraneous-dependencies": 0 8 | }, 9 | "globals": { 10 | "before": true, 11 | "after": true, 12 | "it": true, 13 | "describe": true, 14 | "beforeEach": true, 15 | "afterEach": true 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/before-all.js: -------------------------------------------------------------------------------- 1 | global.__REKIT_NO_WATCH = true; 2 | 3 | const paths = require('../core/paths'); 4 | const logger = require('../core/logger'); 5 | require('../'); 6 | 7 | paths.setProjectRoot(paths.join(__dirname, './test-prj')); 8 | logger.configure({ silent: true }); 9 | -------------------------------------------------------------------------------- /tests/deps.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | 5 | const paths = require('../core/paths'); 6 | const deps = require('../core/deps'); 7 | const vio = require('../core/vio'); 8 | 9 | const moduleA = 'src/moduleA.js'; 10 | const moduleB = 'src/moduleB.js'; 11 | const moduleC = 'src/moduleC.js'; 12 | const moduleD = 'src/moduleD.js'; 13 | const moduleIndex = 'src/index.js'; 14 | 15 | describe('deps', function() { 16 | const pkgJsonPath = paths.map('package.json'); 17 | before(() => { 18 | vio.reset(); 19 | 20 | require.cache[pkgJsonPath] = { 21 | id: pkgJsonPath, 22 | filename: pkgJsonPath, 23 | loaded: true, 24 | exports: { 25 | rekit: { 26 | 'module-alias': { 27 | app2: './src', 28 | }, 29 | }, 30 | babel: { 31 | plugins: [ 32 | [ 33 | 'module-resolver', 34 | { 35 | alias: { 36 | app: './src', 37 | }, 38 | }, 39 | ], 40 | ], 41 | }, 42 | }, 43 | }; 44 | }); 45 | after(() => { 46 | delete require.cache[pkgJsonPath]; 47 | }); 48 | 49 | beforeEach(() => { 50 | vio.reset(); 51 | vio.put(moduleA, ''); 52 | vio.put(moduleB, ''); 53 | vio.put(moduleC, ''); 54 | vio.put(moduleD, ''); 55 | vio.put('src/moduleFolder/index.js', ''); 56 | vio.put(moduleIndex, ''); 57 | }); 58 | 59 | it('handle normal import', () => { 60 | vio.put( 61 | moduleA, 62 | ` 63 | import B from './moduleB'; 64 | import { C } from './moduleC'; 65 | import D, { D1 } from './moduleD'; 66 | `, 67 | ); 68 | const resDeps = deps.getDeps(moduleA); 69 | expect(resDeps).to.deep.equal([ 70 | { id: moduleB, defaultImport: true, type: 'file' }, 71 | { id: moduleC, defaultImport: false, type: 'file', imported: ['C'] }, 72 | { id: moduleD, defaultImport: true, type: 'file', imported: ['D1'] }, 73 | ]); 74 | }); 75 | 76 | it('get npm deps', () => { 77 | vio.put(moduleA, `import React, { Component } from 'react';`); 78 | expect(deps.getDeps(moduleA)).to.deep.equal([ 79 | { 80 | id: 'react', 81 | type: 'npm', 82 | imported: ['Component'], 83 | defaultImport: true, 84 | }, 85 | ]); 86 | }); 87 | 88 | it('handle export from', () => { 89 | vio.put(moduleIndex, `export { default as A, A1, A2 as AA2 } from './moduleA'; `); 90 | expect(deps.getDeps(moduleIndex)).to.deep.equal([ 91 | { 92 | id: moduleA, 93 | exported: { A: 'default', A1: 'A1', AA2: 'A2' }, 94 | type: 'file', 95 | }, 96 | ]); 97 | }); 98 | 99 | it('handle require', () => { 100 | vio.put(moduleA, `require('./moduleB');`); 101 | expect(deps.getDeps(moduleA)).to.deep.equal([ 102 | { 103 | id: moduleB, 104 | type: 'file', 105 | isRequire: true, 106 | }, 107 | ]); 108 | }); 109 | 110 | it('handle import', () => { 111 | vio.put(moduleA, `import('./moduleB');`); 112 | expect(deps.getDeps(moduleA)).to.deep.equal([ 113 | { 114 | id: moduleB, 115 | type: 'file', 116 | isImport: true, 117 | }, 118 | ]); 119 | }); 120 | 121 | it('merges import from', () => { 122 | vio.put( 123 | moduleA, 124 | ` 125 | import { B1 } from './moduleB'; 126 | import { B2 } from './moduleB';`, 127 | ); 128 | expect(deps.getDeps(moduleA)).to.deep.equal([ 129 | { 130 | id: moduleB, 131 | type: 'file', 132 | imported: ['B1', 'B2'], 133 | defaultImport: false, 134 | }, 135 | ]); 136 | }); 137 | 138 | it('handle namespace import', () => { 139 | vio.put(moduleA, `import * as ns from './';`); 140 | expect(deps.getDeps(moduleA)).to.deep.equal([ 141 | { 142 | id: moduleIndex, 143 | type: 'file', 144 | defaultImport: false, 145 | nsImport: true, 146 | }, 147 | ]); 148 | }); 149 | 150 | it('handle module alias', () => { 151 | vio.put(moduleA, `import B from 'app/moduleB';`); 152 | expect(deps.getDeps(moduleA)).to.deep.equal([ 153 | { 154 | id: moduleB, 155 | type: 'file', 156 | defaultImport: true, 157 | }, 158 | ]); 159 | }); 160 | 161 | it(`computes deps via index entry`, () => { 162 | vio.put(moduleA, `import foo, { B1, BB2, C } from './index';`); 163 | vio.put(moduleB, `import { A, D } from './index';`); 164 | vio.put( 165 | moduleIndex, 166 | ` 167 | export { default as A } from './moduleA'; 168 | export { default as B, B1, B2 as BB2 } from './moduleB'; 169 | export { default as C } from './moduleC'; 170 | `, 171 | ); 172 | expect(deps.getDeps(moduleA)).to.deep.equal([ 173 | { id: moduleB, defaultImport: false, imported: ['B1', 'B2'], type: 'file' }, 174 | { id: moduleC, defaultImport: true, type: 'file' }, 175 | { id: moduleIndex, defaultImport: true, type: 'file' }, 176 | ]); 177 | 178 | expect(deps.getDeps(moduleB)).to.deep.equal([ 179 | { id: moduleA, defaultImport: true, type: 'file' }, 180 | { id: moduleIndex, defaultImport: false, imported: ['D'], type: 'file' }, 181 | ]); 182 | }); 183 | 184 | it(`import from a folder resolves to index.js`, () => { 185 | vio.mkdir('src/moduleFolder'); 186 | vio.put(moduleA, `import F from './moduleFolder';`); 187 | expect(deps.getDeps(moduleA)).to.deep.equal([ 188 | { 189 | id: 'src/moduleFolder/index.js', 190 | type: 'file', 191 | defaultImport: true, 192 | }, 193 | ]); 194 | }); 195 | }); 196 | -------------------------------------------------------------------------------- /tests/helpers.js: -------------------------------------------------------------------------------- 1 | /* eslint strict: 0, no-unused-expressions: 0 */ 2 | "use strict"; 3 | 4 | const path = require("path"); 5 | const expect = require("chai").expect; 6 | const _ = require("lodash"); 7 | // const utils = require('../core/utils'); 8 | const vio = require("../core/vio"); 9 | 10 | // const TEST_FEATURE_NAME = 'a-feature'; 11 | // const TEST_FEATURE_NAME_2 = 'another-feature'; 12 | 13 | // For testing, use a fake project root 14 | // paths.setProjectRoot(path.join(__dirname, './test-prj')); 15 | // logger.setSilent(true); 16 | 17 | // function mapFile(file) { 18 | // return path.join(__dirname, '../../src', file); 19 | // } 20 | 21 | // function mapFeatureFile(file) { 22 | // return path.join(__dirname, '../../src/features', TEST_FEATURE_NAME, file); 23 | // } 24 | 25 | // function mapTestFile(file) { 26 | // return path.join(__dirname, '../../tests', file); 27 | // } 28 | 29 | // function mapFeatureTestFile(file) { 30 | // return path.join(__dirname, '../../tests/features', TEST_FEATURE_NAME, file); 31 | // } 32 | 33 | // function pureExec(cmd) { 34 | // return shell.exec(cmd, { silent: true }); 35 | // } 36 | 37 | // function exec(cmd) { 38 | // expect(pureExec(cmd).code).to.equal(0); 39 | // } 40 | 41 | // function pureExecTool(script, args) { 42 | // return shell.exec(`"${process.execPath}" "${path.join(__dirname, '../../tools/cli', script)}" ${args || ''}`, { 43 | // silent: true, 44 | // }); 45 | // } 46 | 47 | // function execTool(script, args) { 48 | // expect(pureExecTool(script, args).code).to.equal(0); 49 | // } 50 | 51 | // function expectError(cmd) { 52 | // expect(pureExec(cmd).code).to.equal(1); 53 | // } 54 | 55 | function expectFile(file) { 56 | expect(file).to.satisfy(vio.fileExists); 57 | } 58 | 59 | function expectFiles(files) { 60 | files.forEach(expectFile); 61 | } 62 | 63 | function expectNoFile(file) { 64 | expect(file).to.satisfy(vio.fileNotExists); 65 | } 66 | 67 | function expectNoFiles(files) { 68 | files.forEach(expectNoFile); 69 | } 70 | 71 | function getLines(file) { 72 | return vio.getLines(file); 73 | } 74 | 75 | function expectLine(file, line) { 76 | const lines = getLines(file); 77 | expect(line).to.be.oneOf(lines); 78 | } 79 | 80 | function expectLines(file, lines) { 81 | lines.forEach(line => expectLine(file, line)); 82 | } 83 | 84 | function expectNoLine(file, line) { 85 | const lines = getLines(file); 86 | expect(_.find(lines, l => l.indexOf(line) >= 0)).to.not.exist; 87 | } 88 | 89 | function expectNoLines(file, lines) { 90 | lines.forEach(line => expectNoLine(file, line)); 91 | } 92 | 93 | function escapeRegExp(s) { 94 | return s.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); 95 | } 96 | 97 | module.exports = { 98 | // exec, 99 | // execTool, 100 | // pureExecTool, 101 | // pureExec, 102 | // expectError, 103 | expectFile, 104 | expectFiles, 105 | expectNoFile, 106 | expectNoFiles, 107 | getLines, 108 | expectLine, 109 | expectNoLine, 110 | expectLines, 111 | expectNoLines, 112 | // TEST_FEATURE_NAME, 113 | // TEST_FEATURE_NAME_2, 114 | escapeRegExp 115 | }; 116 | -------------------------------------------------------------------------------- /tests/npm.js: -------------------------------------------------------------------------------- 1 | // Test npm scripts work 2 | // const shell = require('shelljs'); 3 | 4 | // const featureName = 'npm-test-feature'; 5 | // shell.exec(`npm run add:feature ${featureName}`); 6 | // shell.exec(`npm run add:page ${featureName}/my-page`); 7 | // shell.exec(`npm run add:component ${featureName}/my-component`); 8 | // shell.exec(`npm run add:action ${featureName}/my-action`); 9 | // shell.exec(`npm run add:async-action ${featureName}/my-async-action`); 10 | 11 | // shell.exec(`npm run rm:page ${featureName}/my-page`); 12 | // shell.exec(`npm run rm:component ${featureName}/my-component`); 13 | // shell.exec(`npm run rm:action ${featureName}/my-action`); 14 | // shell.exec(`npm run rm:async-action ${featureName}/my-async-action`); 15 | // shell.exec(`npm run rm:feature ${featureName}`); 16 | -------------------------------------------------------------------------------- /tests/paths.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const paths = require('../core/paths'); 5 | 6 | describe('paths tests', function() { 7 | before(() => {}); 8 | 9 | describe('relativeModuleSource', () => { 10 | it('it works', () => { 11 | const p1 = paths.relativeModuleSource('/a/b.js', '/a/c.js'); 12 | expect(p1).to.equal('./c'); 13 | const p2 = paths.relativeModuleSource('a/b.js', 'a/c.js'); 14 | expect(p2).to.equal('./c'); 15 | const p3 = paths.relativeModuleSource('./a/b.js', './a/c.js'); 16 | expect(p3).to.equal('./c'); 17 | const p4 = paths.relativeModuleSource('a/b/c.js', 'a/c/d.js'); 18 | expect(p4).to.equal('../c/d'); 19 | const p5 = paths.relativeModuleSource('a/b.js', 'c.js'); 20 | expect(p5).to.equal('../c'); 21 | }); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /tests/refactor/array.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const traverse = require('@babel/traverse').default; 4 | const vio = require('../../core/vio'); 5 | const ast = require('../../core/ast'); 6 | const refactor = require('../../core/refactor'); 7 | const helpers = require('../helpers'); 8 | 9 | const V_FILE = 'vio-temp-file.js'; 10 | 11 | const expectLines = helpers.expectLines; 12 | const expectNoLines = helpers.expectNoLines; 13 | 14 | describe('refactor array tests', function() { 15 | // eslint-disable-line 16 | before(() => { 17 | vio.reset(); 18 | }); 19 | 20 | const CODE = `\ 21 | const arr1 = []; 22 | const arr2 = [a, b, c]; 23 | const arr3 = [ 24 | ]; 25 | const arr4 = [ 26 | { p1: 1, p2: 2 }, 27 | ]; 28 | const arr5 = [ 29 | 5 30 | ]; 31 | const arr6 = [ 32 | a, 33 | { 34 | abc: 1, 35 | }, 36 | { 37 | def: 2, 38 | }, 39 | ]; 40 | const arr7 = [ 41 | abc, 42 | def, 43 | ghi, 44 | ]; 45 | `; 46 | 47 | it(`addToArrayByNode`, () => { 48 | vio.put(V_FILE, CODE); 49 | const ast1 = ast.getAst(V_FILE); 50 | const arrs = {}; 51 | traverse(ast1, { 52 | VariableDeclarator(path) { 53 | const node = path.node; 54 | node.init._filePath = ast1._filePath; 55 | arrs[node.id.name] = node.init; 56 | }, 57 | }); 58 | 59 | const changes = [].concat( 60 | refactor.addToArrayByNode(arrs.arr1, '1'), 61 | refactor.addToArrayByNode(arrs.arr2, '1'), 62 | refactor.addToArrayByNode(arrs.arr3, '1'), 63 | refactor.addToArrayByNode(arrs.arr4, '{ p: 1 }'), 64 | refactor.addToArrayByNode(arrs.arr5, '6'), 65 | ); 66 | 67 | const code = refactor.updateSourceCode(vio.getContent(V_FILE), changes); 68 | vio.put(V_FILE, code); 69 | 70 | expectLines(V_FILE, [ 71 | 'const arr1 = [1];', 72 | 'const arr2 = [a, b, c, 1];', 73 | 'const arr3 = [', 74 | ' 1,', 75 | '];', 76 | 'const arr4 = [', 77 | ' { p1: 1, p2: 2 },', 78 | ' { p: 1 },', 79 | '];', 80 | 'const arr5 = [', 81 | ' 5,', 82 | ' 6', 83 | '];', 84 | ]); 85 | }); 86 | 87 | it(`removeFromArrayByNode`, () => { 88 | const ast1 = ast.getAst(V_FILE); 89 | const arrs = {}; 90 | traverse(ast1, { 91 | VariableDeclarator(path) { 92 | const node = path.node; 93 | node.init._filePath = ast1._filePath; 94 | arrs[node.id.name] = node.init; 95 | }, 96 | }); 97 | 98 | const changes = [].concat( 99 | refactor.removeFromArrayByNode(arrs.arr1, arrs.arr1.elements[0]), 100 | refactor.removeFromArrayByNode(arrs.arr2, arrs.arr2.elements[2]), 101 | refactor.removeFromArrayByNode(arrs.arr3, arrs.arr3.elements[0]), 102 | refactor.removeFromArrayByNode(arrs.arr4, arrs.arr4.elements[1]), 103 | refactor.removeFromArrayByNode(arrs.arr5, arrs.arr5.elements[1]), 104 | refactor.removeFromArrayByNode(arrs.arr6, arrs.arr6.elements[1]), 105 | ); 106 | 107 | const code = refactor.updateSourceCode(vio.getContent(V_FILE), changes); 108 | vio.put(V_FILE, code); 109 | expectLines(V_FILE, [ 110 | 'const arr1 = [];', 111 | 'const arr2 = [a, b, 1];', 112 | 'const arr3 = [', 113 | '];', 114 | 'const arr4 = [', 115 | ' { p1: 1, p2: 2 },', 116 | '];', 117 | 'const arr5 = [', 118 | ' 5', 119 | '];', 120 | ]); 121 | 122 | expectNoLines(V_FILE, [' 1,', ' { p: 1 },', ' 6', ' abc: 1']); 123 | }); 124 | 125 | it('addToArray', () => { 126 | refactor.addToArray(V_FILE, 'arr1', 'x'); 127 | refactor.addToArray(V_FILE, 'arr2', 'y'); 128 | refactor.addToArray(V_FILE, 'arr5', 'z'); 129 | expectLines(V_FILE, ['const arr1 = [x];', 'const arr2 = [a, b, 1, y];', ' 5,', ' z']); 130 | }); 131 | it('removeFromArray', () => { 132 | refactor.removeFromArray(V_FILE, 'arr1', 'x'); 133 | refactor.removeFromArray(V_FILE, 'arr2', 'y'); 134 | refactor.removeFromArray(V_FILE, 'arr5', 'z'); 135 | refactor.removeFromArray(V_FILE, 'arr7', 'abc'); 136 | expectNoLines(V_FILE, ['const arr7 = [,']); 137 | expectLines(V_FILE, ['const arr1 = [];', 'const arr2 = [a, b, 1];', ' 5']); 138 | }); 139 | }); 140 | -------------------------------------------------------------------------------- /tests/refactor/cls.test.js: -------------------------------------------------------------------------------- 1 | /* eslint quotes: 0 */ 2 | 'use strict'; 3 | 4 | const vio = require('../../core/vio'); 5 | const refactor = require('../../core/refactor/cls'); 6 | const helpers = require('../helpers'); 7 | 8 | const expectLines = helpers.expectLines; 9 | const V_FILE = 'vio-temp-file.js'; 10 | 11 | describe('renameClassName', function() { 12 | before(() => { 13 | vio.reset(); 14 | }); 15 | const CODE = `\ 16 | import React, { PureComponent } from 'react'; 17 | 18 | export class Hello extends PureComponent { 19 | render() { 20 | return ( 21 |

22 | Welcome to your Rekit project! 23 |

24 | ); 25 | } 26 | } 27 | 28 | export default Hello; 29 | `; 30 | it('rename es6 class name', () => { 31 | vio.put(V_FILE, CODE); 32 | refactor.renameClassName(V_FILE, 'Hello', 'NewHello'); 33 | expectLines(V_FILE, [ 34 | 'export class NewHello extends PureComponent {', 35 | 'export default NewHello;', 36 | ]); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/refactor/common.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const paths = require('../../core/paths'); 5 | const vio = require('../../core/vio'); 6 | const refactorCommon = require('../../core/refactor/common'); 7 | 8 | describe('refactor common tests', function() { 9 | // eslint-disable-line 10 | 11 | before(() => { 12 | vio.reset(); 13 | }); 14 | 15 | it('isLocalModule', () => { 16 | expect(refactorCommon.isLocalModule('../f-2/redux/actions')).to.be.true; 17 | expect(refactorCommon.isLocalModule('src/common/routeConfig')).to.be.true; 18 | expect(refactorCommon.isLocalModule('src2/common/routeConfig')).to.be.false; 19 | expect(refactorCommon.isLocalModule('react')).to.be.false; 20 | }); 21 | 22 | it('resolveModulePath', () => { 23 | const file = paths.map('src/features/f-1/Test.js'); 24 | const p1 = refactorCommon.resolveModulePath(file, '../f-2/redux/actions'); 25 | const p2 = refactorCommon.resolveModulePath(file, 'src/common/routeConfig'); 26 | const p22 = refactorCommon.resolveModulePath(file, 'src/common/routeConfig.js'); 27 | const p3 = refactorCommon.resolveModulePath(file, './M1'); 28 | const p4 = refactorCommon.resolveModulePath(file, 'react'); 29 | const p5 = refactorCommon.resolveModulePath(file, 'src/common'); 30 | expect(p1).to.equal('src/features/f-2/redux/actions'); 31 | expect(p2).to.equal('src/common/routeConfig'); 32 | expect(p22).to.equal('src/common/routeConfig.js'); 33 | expect(p3).to.equal('src/features/f-1/M1'); 34 | expect(p4).to.equal('react'); 35 | expect(p5).to.equal('src/common/index'); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tests/refactor/format.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const expect = require('chai').expect; 3 | const vio = require('../../core/vio'); 4 | const format = require('../../core/refactor/format'); 5 | 6 | describe('format code', function() { 7 | // eslint-disable-line 8 | before(() => { 9 | vio.reset(); 10 | }); 11 | 12 | const CODE = `var a="1";`; 13 | 14 | it(`rename the first matched variable`, () => { 15 | const newCode = format(CODE, __filename, { insertFinalNewline: false }).formatted; 16 | expect(newCode).to.equal("var a = '1';"); 17 | const newCode2 = format(CODE, __filename, { singleQuote: false }).formatted; 18 | expect(newCode2).to.equal('var a = "1";\n'); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /tests/refactor/generate.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const expect = require('chai').expect; 3 | const vio = require('../../core/vio'); 4 | const ast = require('../../core/ast'); 5 | const generate = require('../../core/refactor/generate'); 6 | 7 | const V_FILE = 'vio-temp-file.js'; 8 | 9 | describe('generate code', function() { 10 | // eslint-disable-line 11 | before(() => { 12 | vio.reset(); 13 | }); 14 | 15 | const CODE = `var a="1";`; 16 | 17 | it(`rename the first matched variable`, () => { 18 | vio.put(V_FILE, CODE); 19 | const newCode = generate(ast.getAst(V_FILE), V_FILE); 20 | expect(newCode).to.equal("var a = '1';"); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /tests/refactor/identifier.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const vio = require('../../core/vio'); 4 | const refactor = require('../../core/refactor'); 5 | const helpers = require('../helpers'); 6 | 7 | const V_FILE = 'vio-temp-file.js'; 8 | 9 | const expectLines = helpers.expectLines; 10 | 11 | describe('rename a variable', function() { 12 | // eslint-disable-line 13 | before(() => { 14 | vio.reset(); 15 | }); 16 | 17 | const CODE = `\ 18 | const arr1 = []; 19 | function abc() { 20 | let v1 = 1; 21 | } 22 | `; 23 | 24 | it(`rename the first matched variable`, () => { 25 | vio.put(V_FILE, CODE); 26 | refactor.renameIdentifier(V_FILE, 'arr1', 'arr2'); 27 | refactor.renameIdentifier(V_FILE, 'v1', 'v2'); 28 | 29 | expectLines(V_FILE, ['const arr2 = [];', ' let v2 = 1;']); 30 | }); 31 | }); 32 | -------------------------------------------------------------------------------- /tests/refactor/importExport.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const vio = require('../../core/vio'); 4 | const importExport = require('../../core/refactor/importExport'); 5 | const helpers = require('../helpers'); 6 | 7 | const V_FILE = 'vio-temp-file.js'; 8 | 9 | const expectLines = helpers.expectLines; 10 | const expectNoLines = helpers.expectNoLines; 11 | 12 | describe('importExport', function() { 13 | // eslint-disable-line 14 | before(() => { 15 | vio.reset(); 16 | }); 17 | 18 | const CODE0 = `\ 19 | // Test 20 | import { /*abc */DefaultPage } from './'; // abc 21 | `; 22 | 23 | it(`add import from`, () => { 24 | vio.put(V_FILE, CODE0); 25 | importExport.addImportFrom(V_FILE, './', '', 'ModuleName'); 26 | 27 | expectLines(V_FILE, [`import { DefaultPage, ModuleName } from './'; // abc`]); 28 | }); 29 | }); 30 | 31 | describe('addImportFrom', () => { 32 | const CODE = `\ 33 | import A from './A'; 34 | import { C, D, Z } from './D'; 35 | import { E } from './E'; 36 | import F from './F'; 37 | import { 38 | G1, 39 | G2, 40 | G3, 41 | } from './G'; 42 | import { 43 | } from './'; 44 | 45 | const otherCode = 1; 46 | `; 47 | 48 | it('should add import line when no module source exist', () => { 49 | vio.put(V_FILE, CODE); 50 | importExport.addImportFrom(V_FILE, './K', 'K'); 51 | importExport.addImportFrom(V_FILE, './L', 'L', 'L1'); 52 | importExport.addImportFrom(V_FILE, './M', '', 'M1'); 53 | importExport.addImportFrom(V_FILE, './N', 'N', ['N1', 'N2']); 54 | importExport.addImportFrom(V_FILE, './G', '', ['G4', 'G5']); 55 | importExport.addImportFrom(V_FILE, './', '', ['H1']); 56 | importExport.addImportFrom(V_FILE, './A', null, null, 'all'); 57 | importExport.addImportFrom(V_FILE, './X', null, null, 'AllX'); 58 | importExport.addImportFrom(V_FILE, './Y', 'Y'); 59 | importExport.addImportFrom(V_FILE, './Z', 'Z'); 60 | 61 | expectLines(V_FILE, [ 62 | "import K from './K';", 63 | "import Y from './Y';", 64 | "import Z from './Z';", 65 | "import L, { L1 } from './L';", 66 | "import { M1 } from './M';", 67 | "import N, { N1, N2 } from './N';", 68 | "import { G1, G2, G3, G4, G5 } from './G';", 69 | "import A, * as all from './A';", 70 | "import * as AllX from './X';", 71 | ]); 72 | }); 73 | 74 | it('should add import when empty', () => { 75 | const code = ` 76 | import { 77 | } from './'; 78 | 79 | export default { 80 | path: 'rekit-test-feature', 81 | name: 'Rekit test feature', 82 | childRoutes: [ 83 | { path: 'default-page', name: 'Default page', component: DefaultPage, isIndex: true }, 84 | ], 85 | }; 86 | `; 87 | vio.put(V_FILE, code); 88 | importExport.addImportFrom(V_FILE, './', '', 'A'); 89 | expectLines(V_FILE, ["import { A } from './';"]); 90 | }); 91 | 92 | it('should add import specifier(s) when module exist', () => { 93 | vio.put(V_FILE, CODE); 94 | importExport.addImportFrom(V_FILE, './A', 'AA', 'A1'); 95 | importExport.addImportFrom(V_FILE, './D', 'W', 'Y'); 96 | importExport.addImportFrom(V_FILE, './E', '', ['E', 'E1']); 97 | importExport.addImportFrom(V_FILE, './F', 'F'); 98 | expectLines(V_FILE, [ 99 | "import A, { A1 } from './A';", 100 | "import W, { C, D, Z, Y } from './D';", 101 | "import { E, E1 } from './E';", 102 | ]); 103 | }); 104 | }); 105 | 106 | describe('addExportFrom', () => { 107 | const CODE = `\ 108 | export { default as A } from './A'; 109 | export { C, D, Z } from './D'; 110 | export { E } from './E'; 111 | export { default as F } from './F'; 112 | 113 | const otherCode = 1; 114 | `; 115 | it('should add export line when no module source exist', () => { 116 | vio.put(V_FILE, CODE); 117 | importExport.addExportFrom(V_FILE, './K', 'K'); 118 | importExport.addExportFrom(V_FILE, './L', 'L', 'L1'); 119 | importExport.addExportFrom(V_FILE, './M', '', 'M1'); 120 | importExport.addExportFrom(V_FILE, './N', 'N', ['N1', 'N2']); 121 | 122 | expectLines(V_FILE, [ 123 | "export { default as K } from './K';", 124 | "export { default as L, L1 } from './L';", 125 | "export { M1 } from './M';", 126 | "export { default as N, N1, N2 } from './N';", 127 | ]); 128 | }); 129 | 130 | it('should add export specifier(s) when module exist', () => { 131 | vio.put(V_FILE, CODE); 132 | importExport.addExportFrom(V_FILE, './A', 'AA', 'A1'); 133 | importExport.addExportFrom(V_FILE, './D', 'W', 'Y'); 134 | importExport.addExportFrom(V_FILE, './E', '', ['E', 'E1']); 135 | importExport.addExportFrom(V_FILE, './F', 'F'); 136 | 137 | expectLines(V_FILE, [ 138 | "export { default as A, A1 } from './A';", 139 | "export { default as W, C, D, Z, Y } from './D';", 140 | "export { E, E1 } from './E';", 141 | ]); 142 | }); 143 | }); 144 | 145 | describe('renameImportSpecifier', () => { 146 | const CODE = `\ 147 | import A from './A'; 148 | import { C, D, Z as ZZ } from './D'; 149 | import { E } from './E'; 150 | import { E as EE } from './EE'; 151 | import F from './F'; 152 | import * as AllX from './X'; 153 | import { 154 | G1, 155 | G2, 156 | G3, 157 | } from './G'; 158 | const a = A; 159 | const d = D; 160 | const e = E; 161 | `; 162 | it('should rename imported specifiers correctly', () => { 163 | vio.put(V_FILE, CODE); 164 | importExport.renameImportSpecifier(V_FILE, 'A', 'A1'); 165 | importExport.renameImportSpecifier(V_FILE, 'D', 'D1'); 166 | importExport.renameImportSpecifier(V_FILE, 'Z', 'Z1'); 167 | importExport.renameImportSpecifier(V_FILE, 'E', 'E1'); 168 | importExport.renameImportSpecifier(V_FILE, 'G1', 'GG1'); 169 | importExport.renameImportSpecifier(V_FILE, 'AllX', 'X'); 170 | expectLines(V_FILE, [ 171 | "import A1 from './A';", 172 | "import { C, D1, Z1 as ZZ } from './D';", 173 | "import { E1 } from './E';", 174 | "import { E1 as EE } from './EE';", 175 | "import * as X from './X';", 176 | ' GG1,', 177 | 'const a = A1;', 178 | 'const d = D1;', 179 | ]); 180 | }); 181 | 182 | it('should rename imported specifiers correctly with specified module source', () => { 183 | vio.put(V_FILE, CODE); 184 | importExport.renameImportSpecifier(V_FILE, 'E', 'E1', './E'); 185 | importExport.renameImportSpecifier(V_FILE, 'E', 'E2', './EE'); 186 | 187 | expectLines(V_FILE, [ 188 | "import { E1 } from './E';", 189 | "import { E2 as EE } from './EE';", 190 | 'const e = E1;', 191 | ]); 192 | }); 193 | }); 194 | 195 | describe('renameExportSpecifier', () => { 196 | const CODE = `\ 197 | export { default as A } from './A'; 198 | export { C, D, Z } from './D'; 199 | export { E } from './E'; 200 | export { default as F } from './F'; 201 | `; 202 | it('renames export specifier when module source not specified', () => { 203 | vio.put(V_FILE, CODE); 204 | importExport.renameExportSpecifier(V_FILE, 'A', 'A1'); 205 | importExport.renameExportSpecifier(V_FILE, 'D', 'D1'); 206 | expectLines(V_FILE, [ 207 | "export { default as A1 } from './A';", 208 | "export { C, D1, Z } from './D';", 209 | ]); 210 | }); 211 | 212 | it('renames export specifier when module source is specified', () => { 213 | vio.put(V_FILE, CODE); 214 | importExport.renameExportSpecifier(V_FILE, 'A', 'A1', './A'); 215 | importExport.renameExportSpecifier(V_FILE, 'E', 'E1', './C'); 216 | expectLines(V_FILE, ["export { default as A1 } from './A';", "export { E } from './E';"]); 217 | }); 218 | }); 219 | 220 | describe('removeImportSpecifier', () => { 221 | const CODE = `\ 222 | import A from './A'; 223 | import { C, D, Z } from './D'; 224 | import { E } from './E'; 225 | import F from './F'; 226 | import * as AllX from './X'; 227 | import { 228 | G1, 229 | G2, 230 | } from './G'; 231 | `; 232 | it('should remove give import specifier', () => { 233 | vio.put(V_FILE, CODE); 234 | importExport.removeImportSpecifier(V_FILE, ['E', 'D', 'G1', 'AllX']); 235 | expectLines(V_FILE, ["import { C, Z } from './D';", "import { G2 } from './G';"]); 236 | expectNoLines(V_FILE, [ 237 | "import { E } from './E';", 238 | "import * as AllX from './X';", 239 | "import { G1, G2 } from './G';", 240 | ]); 241 | }); 242 | }); 243 | 244 | const CODE_3 = `\ 245 | import A from './A'; 246 | import { C, D, Z } from './D'; 247 | import { E } from './E'; 248 | import F from './F'; 249 | import { 250 | G1, 251 | G2, 252 | } from './G'; 253 | `; 254 | describe('removeNamedExport', () => { 255 | it('should remove give export specifier', () => { 256 | vio.put(V_FILE, CODE_3); 257 | importExport.removeImportSpecifier(V_FILE, ['E', 'D']); 258 | expectLines(V_FILE, ["import { C, Z } from './D';"]); 259 | expectNoLines(V_FILE, ["import { E } from './E';"]); 260 | }); 261 | }); 262 | 263 | describe('removeImportBySource', () => { 264 | it('should remove import statement by given source', () => { 265 | vio.put(V_FILE, CODE_3); 266 | importExport.removeImportBySource(V_FILE, './A'); 267 | importExport.removeImportBySource(V_FILE, './D'); 268 | expectNoLines(V_FILE, ["import A from './A';", "import { C, D, Z } from './D';"]); 269 | }); 270 | }); 271 | -------------------------------------------------------------------------------- /tests/refactor/object.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const vio = require('../../core/vio'); 4 | const refactor = require('../../core/refactor'); 5 | const expect = require('chai').expect; 6 | const { expectLines, expectNoLines } = require('../helpers'); 7 | const V_FILE = 'vio-temp-file.js'; 8 | 9 | describe('object tests', function() { 10 | // eslint-disable-line 11 | before(() => { 12 | vio.reset(); 13 | }); 14 | 15 | it(`addObjectProperty`, () => { 16 | const CODE1 = `\ 17 | const initialState = { 18 | apiSchema: [] 19 | };`; 20 | vio.put(V_FILE, CODE1); 21 | refactor.addObjectProperty(V_FILE, 'initialState', 'doFetchPending', false); 22 | expect(vio.getContent(V_FILE)).to.equal(`\ 23 | const initialState = { 24 | apiSchema: [], 25 | doFetchPending: false 26 | };`); 27 | const CODE2 = `\ 28 | const initialState = { 29 | };`; 30 | vio.put(V_FILE, CODE2); 31 | refactor.addObjectProperty(V_FILE, 'initialState', 'doFetchPending', false); 32 | expect(vio.getContent(V_FILE)).to.equal(`\ 33 | const initialState = { 34 | doFetchPending: false, 35 | };`); 36 | 37 | const CODE3 = `const initialState = {};`; 38 | vio.put(V_FILE, CODE3); 39 | refactor.addObjectProperty(V_FILE, 'initialState', 'doFetchPending', false); 40 | expect(vio.getContent(V_FILE)).to.equal(`const initialState = { doFetchPending: false };`); 41 | 42 | const CODE4 = `const initialState = { a: 1 };`; 43 | vio.put(V_FILE, CODE4); 44 | refactor.addObjectProperty(V_FILE, 'initialState', 'doFetchPending', false); 45 | expect(vio.getContent(V_FILE)).to.equal( 46 | `const initialState = { a: 1, doFetchPending: false };`, 47 | ); 48 | 49 | const CODE5 = `const initialState = { a: 1, };`; 50 | vio.put(V_FILE, CODE5); 51 | refactor.addObjectProperty(V_FILE, 'initialState', 'doFetchPending', false); 52 | expect(vio.getContent(V_FILE)).to.equal( 53 | `const initialState = { a: 1, doFetchPending: false, };`, 54 | ); 55 | }); 56 | 57 | const CODE = `\ 58 | const obj = { 59 | p1: 1, 60 | p2: 2, 61 | p3: 'abc', 62 | p4: true, 63 | }; 64 | 65 | const obj1 = { 66 | }; 67 | 68 | const obj2 = { p: 1 }; 69 | const obj3 = {}; 70 | const obj4 = { p1: 1, p2: 2, p3: 3 }; 71 | 72 | const c = obj.p1; 73 | `; 74 | it('addObjectProperty should add new property when not exist', () => { 75 | vio.put(V_FILE, CODE); 76 | refactor.addObjectProperty(V_FILE, 'obj', 'p5', 'true'); 77 | expectLines(V_FILE, [' p5: true,']); 78 | }); 79 | 80 | it('addObjectProperty should not add new property when already exist', () => { 81 | vio.put(V_FILE, CODE); 82 | refactor.addObjectProperty(V_FILE, 'obj', 'p4', 'false'); 83 | expectLines(V_FILE, [' p4: true,']); 84 | }); 85 | 86 | it('addObjectProperty should handle one line object declaration', () => { 87 | vio.put(V_FILE, CODE); 88 | refactor.addObjectProperty(V_FILE, 'obj2', 'p2', 'true'); 89 | refactor.addObjectProperty(V_FILE, 'obj3', 'p', "'abc'"); 90 | expectLines(V_FILE, ['const obj2 = { p: 1, p2: true };', "const obj3 = { p: 'abc' };"]); 91 | }); 92 | 93 | it('setObjectProperty should set the new value', () => { 94 | vio.put(V_FILE, CODE); 95 | refactor.setObjectProperty(V_FILE, 'obj', 'p2', '345'); 96 | expectLines(V_FILE, [' p2: 345,']); 97 | }); 98 | 99 | it('renameObjectProperty should rename property correctly', () => { 100 | vio.put(V_FILE, CODE); 101 | refactor.renameObjectProperty(V_FILE, 'obj', 'p1', 'n1'); 102 | refactor.renameObjectProperty(V_FILE, 'obj2', 'p', 'n'); 103 | expectLines(V_FILE, [' n1: 1,', 'const obj2 = { n: 1 };']); 104 | }); 105 | 106 | it('removeObjectProperty should rename property correctly', () => { 107 | vio.put(V_FILE, CODE); 108 | refactor.removeObjectProperty(V_FILE, 'obj', 'p1'); 109 | refactor.removeObjectProperty(V_FILE, 'obj', 'p3'); 110 | refactor.removeObjectProperty(V_FILE, 'obj4', 'p2'); 111 | expectNoLines(V_FILE, [' p1: 1,', " p3: 'abc',"]); 112 | 113 | expectLines(V_FILE, ['const obj4 = { p1: 1, p3: 3 };']); 114 | 115 | refactor.removeObjectProperty(V_FILE, 'obj4', 'p1'); 116 | 117 | expectLines(V_FILE, ['const obj4 = { p3: 3 };']); 118 | 119 | refactor.removeObjectProperty(V_FILE, 'obj4', 'p3'); 120 | expectLines(V_FILE, ['const obj4 = { };']); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /tests/refactor/string.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const vio = require('../../core/vio'); 4 | const refactor = require('../../core/refactor'); 5 | const helpers = require('../helpers'); 6 | 7 | const V_FILE = 'vio-temp-file.js'; 8 | 9 | const expectLines = helpers.expectLines; 10 | 11 | describe('refactor string tests', function() { 12 | // eslint-disable-line 13 | before(() => { 14 | vio.reset(); 15 | }); 16 | 17 | const CODE = `\ 18 | import A from './src/A'; 19 | const s1 = 'abcde'; 20 | const s2 = 'ghijk'; 21 | `; 22 | 23 | it('renameStringLiteral', () => { 24 | vio.put(V_FILE, CODE); 25 | refactor.renameStringLiteral(V_FILE, './src/A', './src/B'); 26 | refactor.renameStringLiteral(V_FILE, 'abcde', '12345'); 27 | expectLines(V_FILE, ["const s1 = '12345';", "import A from './src/B';"]); 28 | }); 29 | it('replaceStringLiteral', () => { 30 | vio.put(V_FILE, CODE); 31 | refactor.replaceStringLiteral(V_FILE, 'hij', '234', false); 32 | expectLines(V_FILE, ["const s2 = 'g234k';"]); 33 | }); 34 | 35 | const CODE2 = ` 36 | const str1 = 'abcdefg'; 37 | const jsx = ( 38 |
39 |

sub-title

40 |
41 | ); 42 | 43 | `; 44 | it('should only replace full string when fullMatch === true', () => { 45 | vio.put(V_FILE, CODE2); 46 | refactor.replaceStringLiteral(V_FILE, 'abcdefg', 'new-str'); 47 | expectLines(V_FILE, ["const str1 = 'new-str';"]); 48 | refactor.replaceStringLiteral(V_FILE, 'new', 'xxx'); 49 | expectLines(V_FILE, ["const str1 = 'new-str';"]); 50 | }); 51 | 52 | it('should only replace full string when fullMatch === false', () => { 53 | vio.put(V_FILE, CODE2); 54 | refactor.replaceStringLiteral(V_FILE, 'sub-title', 'second-title', false); 55 | expectLines(V_FILE, ['

sub-title

']); 56 | }); 57 | }); 58 | -------------------------------------------------------------------------------- /tests/refactor/style.test.js: -------------------------------------------------------------------------------- 1 | /* eslint quotes: 0 */ 2 | 'use strict'; 3 | 4 | const vio = require('../../core/vio'); 5 | const style = require('../../core/refactor/style'); 6 | const helpers = require('../helpers'); 7 | 8 | const expectLines = helpers.expectLines; 9 | const V_FILE = 'vio-temp-file.js'; 10 | describe('renameCssClassName', () => { 11 | const CODE = `\ 12 | import React, { PureComponent } from 'react'; 13 | 14 | export class Hello extends PureComponent { 15 | render() { 16 | return ( 17 |

18 | 19 | Welcome to your Rekit project! 20 |

21 | ); 22 | } 23 | } 24 | 25 | export default Hello; 26 | `; 27 | it('rename className property', () => { 28 | vio.put(V_FILE, CODE); 29 | style.renameCssClassName(V_FILE, 'home-hello', 'home-new-hello'); 30 | expectLines(V_FILE, [ 31 | '

', 32 | ' ', 33 | ]); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /tests/rekit.js: -------------------------------------------------------------------------------- 1 | // This script is only used to test the global rekit command creates project with all correct configs. 2 | 3 | // const path = require('path'); 4 | // const shell = require('shelljs'); 5 | // const expect = require('chai').expect; 6 | 7 | // const prjPath = path.join(__dirname, '../../'); 8 | // const TEMP_APP_NAME = 'rekit_temp_app'; 9 | // const tempAppFolder = path.join(prjPath, TEMP_APP_NAME); 10 | 11 | // function exec(cmd, options) { 12 | // expect(shell.exec(cmd, options || {}).code).to.equal(0); 13 | // } 14 | 15 | // shell.rm('-rf', tempAppFolder); 16 | // exec(`rekit ${TEMP_APP_NAME}`, { cwd: prjPath }); // create rekit project 17 | // console.log('Installing dependecies...'); 18 | // exec('npm install --registry=https://registry.npm.taobao.org', { cwd: tempAppFolder }); // install dependecies 19 | // console.log('Running tests...'); 20 | // exec('npm run test', { cwd: tempAppFolder }); // ensure test passes for the new project 21 | -------------------------------------------------------------------------------- /tests/template.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const expect = require('chai').expect; 4 | const vio = require('../core/vio'); 5 | const template = require('../core/template'); 6 | 7 | const V_FILE = '/vio-temp-file.js'; 8 | const TPL_1 = `\ 9 | const actionType = $\{actionType}; 10 | `; 11 | 12 | const RES_1 = `\ 13 | const actionType = ACTION_TYPE; 14 | `; 15 | 16 | describe('template tests', function() { 17 | // eslint-disable-line 18 | before(() => { 19 | vio.reset(); 20 | }); 21 | 22 | describe('generate template', () => { 23 | it('should generate result correctly', () => { 24 | vio.put(V_FILE, TPL_1); 25 | template.generate(V_FILE + '.res', { 26 | templateFile: V_FILE, 27 | context: { actionType: 'ACTION_TYPE' }, 28 | }); 29 | expect(vio.getContent(V_FILE + '.res')).to.equal(RES_1); 30 | }); 31 | }); 32 | }); 33 | -------------------------------------------------------------------------------- /tests/test-prj/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rekit-test-prj", 3 | "version": "0.0.1", 4 | "private": true, 5 | "description": "This is a fake project for running tests which only contains necessary files for testing.", 6 | "rekit": {}, 7 | "dependencies": {} 8 | } 9 | -------------------------------------------------------------------------------- /tests/test-prj/rekit.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleAlias": { 3 | "app": "./src", 4 | "src": "./src" 5 | } 6 | } -------------------------------------------------------------------------------- /tests/test-prj/src/common/configStore.js: -------------------------------------------------------------------------------- 1 | import { createStore, applyMiddleware, compose } from 'redux'; 2 | import thunk from 'redux-thunk'; 3 | import rootReducer from './rootReducer'; 4 | 5 | const middlewares = [ // REKIT_ARCHOR_DO_NOT_CHANGE 6 | thunk, 7 | ]; // REKIT_ARCHOR_DO_NOT_CHANGE 8 | 9 | let devToolsExtension = f => f; 10 | 11 | /* istanbul ignore if */ 12 | if (process.env.NODE_ENV === 'dev') { 13 | const createLogger = require('redux-logger'); 14 | 15 | const logger = createLogger({ collapsed: true }); 16 | middlewares.push(logger); 17 | 18 | if (window.devToolsExtension) { 19 | devToolsExtension = window.devToolsExtension(); 20 | } 21 | } 22 | 23 | export default function configureStore(initialState) { 24 | const store = createStore(rootReducer, initialState, compose( 25 | applyMiddleware(...middlewares), 26 | devToolsExtension 27 | )); 28 | 29 | if (module.hot) { 30 | // Enable Webpack hot module replacement for reducers 31 | module.hot.accept('./rootReducer', () => { 32 | const nextRootReducer = require('./rootReducer').default; // eslint-disable-line 33 | store.replaceReducer(nextRootReducer); 34 | }); 35 | } 36 | 37 | return store; 38 | } 39 | -------------------------------------------------------------------------------- /tests/test-prj/src/common/rootReducer.js: -------------------------------------------------------------------------------- 1 | // This file is auto maintained by Rekit, you usually don't need to edit it manually. 2 | 3 | import { combineReducers } from 'redux'; 4 | import { routerReducer } from 'react-router-redux'; 5 | import homeReducer from '../features/home/redux/reducer'; 6 | import commonReducer from '../features/common/redux/reducer'; 7 | 8 | // NOTE 1: DO NOT CHANGE the 'reducerMap' name and the declaration pattern. 9 | // This is used for Rekit cmds to register new features, remove features, etc. 10 | 11 | // NOTE 2: always use the camel case of the feature folder name as the store branch name 12 | // So that it's easy for others to understand it and Rekit could manage theme. 13 | 14 | const reducerMap = { 15 | routing: routerReducer, 16 | home: homeReducer, 17 | common: commonReducer, 18 | }; 19 | 20 | export default combineReducers(reducerMap); 21 | -------------------------------------------------------------------------------- /tests/test-prj/src/common/routeConfig.js: -------------------------------------------------------------------------------- 1 | import App from '../containers/App'; 2 | import { PageNotFound } from '../features/common'; 3 | import homeRoute from '../features/home/route'; 4 | import commonRoute from '../features/common/route'; 5 | 6 | // NOTE: DO NOT CHANGE the 'childRoutes' name and the declaration pattern. 7 | // This is used for Rekit cmds to register routes config for new features, and remove config when remove features, etc. 8 | const childRoutes = [ 9 | homeRoute, 10 | commonRoute, 11 | ]; 12 | 13 | const routes = [{ 14 | path: '/', 15 | component: App, 16 | childRoutes: [ 17 | ...childRoutes, 18 | { path: '*', name: 'Page not found', component: PageNotFound }, 19 | ].filter(r => r.component || (r.childRoutes && r.childRoutes.length > 0)), 20 | }]; 21 | 22 | // Handle isIndex property of route config: 23 | // 1. remove the first child with isIndex=true if no path property from childRoutes 24 | // 2. assign it to the indexRoute property of the parent. 25 | function handleIndexRoute(route) { 26 | if (!route.childRoutes || !route.childRoutes.length) { 27 | return; 28 | } 29 | 30 | route.childRoutes = route.childRoutes.filter(child => { // eslint-disable-line 31 | if (child.isIndex) { 32 | /* istanbul ignore next */ 33 | if (process.env.NODE_ENV === 'dev' && route.indexRoute) { 34 | console.error('More than one index route: ', route); 35 | } 36 | 37 | /* istanbul ignore else */ 38 | if (!route.indexRoute) { 39 | const indexRoute = { ...child }; 40 | delete indexRoute.path; 41 | route.indexRoute = indexRoute; // eslint-disable-line 42 | if (!child.path) return false; 43 | } 44 | } 45 | return true; 46 | }); 47 | 48 | route.childRoutes.forEach(handleIndexRoute); 49 | } 50 | 51 | routes.forEach(handleIndexRoute); 52 | export default routes; 53 | -------------------------------------------------------------------------------- /tests/test-prj/src/styles/index.less: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rekit/rekit-core/85838e3b23b2137f39ec390ec247285a5c02d8e0/tests/test-prj/src/styles/index.less -------------------------------------------------------------------------------- /tests/utils.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const expect = require('chai').expect; 5 | const utils = require('../core/utils'); 6 | const vio = require('../core/vio'); 7 | 8 | describe('util tests', function() { // eslint-disable-line 9 | before(() => { 10 | vio.reset(); 11 | }); 12 | }); 13 | 14 | -------------------------------------------------------------------------------- /tests/vio.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | const expect = require('chai').expect; 5 | const vio = require('../core/vio'); 6 | const utils = require('../core/utils'); 7 | const paths = require('../core/paths'); 8 | require('./helpers'); 9 | 10 | describe('vio', function() { 11 | beforeEach(() => { 12 | vio.reset(); 13 | }); 14 | 15 | // describe('getAst', function () { 16 | // beforeEach(() => { 17 | // vio.reset(); 18 | // }); 19 | // it('should parse code correctly', () => { 20 | // const code = `\ 21 | // const initialState = { 22 | // fetchApiSchemaPending: false, 23 | // fetchApiSchemaError: null, 24 | // apiSchema:[] 25 | // }; 26 | 27 | // export default initialState; 28 | // `; 29 | // vio.put('V_FILE', code); 30 | // const ast = vio.getAst('V_FILE'); 31 | // expect(ast).to.exist; 32 | // }); 33 | // }); 34 | 35 | describe('move dir', function() { 36 | beforeEach(() => { 37 | vio.reset(); 38 | }); 39 | it('file content cache should be updated', () => { 40 | vio.put('d1/d2/f1', 'c1'); 41 | vio.moveDir('d1', 'nd1'); 42 | expect(vio.getContent('nd1/d2/f1')).to.equal('c1'); 43 | expect('nd1/d2/f1').to.satisfy(vio.fileExists); 44 | expect('d1/d2/f1').to.satisfy(vio.fileNotExists); 45 | }); 46 | 47 | it('new files should be updated', () => { 48 | vio.save('d1/d2/f1', 'c1'); 49 | vio.moveDir('d1', 'nd1'); 50 | expect(vio.getContent('nd1/d2/f1')).to.equal('c1'); 51 | expect('nd1/d2/f1').to.satisfy(vio.fileExists); 52 | expect('d1/d2/f1').to.satisfy(vio.fileNotExists); 53 | }); 54 | 55 | it('files to delete should be updated', () => { 56 | vio.save('d1/d2/f1', 'c1'); 57 | vio.del('d1/d2/f1'); 58 | vio.moveDir('d1', 'nd1'); 59 | expect('nd1/d2/f1').to.satisfy(vio.fileNotExists); 60 | }); 61 | 62 | it('files to move should be updated', () => { 63 | vio.save('d1/d2/f1', 'c1'); 64 | vio.move('d1/d2/f1', 'd1/d2/nf1'); 65 | vio.moveDir('d1', 'nd1'); 66 | expect('d1/d2/nf1').to.satisfy(vio.fileNotExists); 67 | expect('nd1/d2/nf1').to.satisfy(vio.fileExists); 68 | }); 69 | 70 | it('dirs to create should be updated', () => { 71 | vio.mkdir('d1/d2'); 72 | expect('d1/d2').to.satisfy(vio.dirExists); 73 | vio.moveDir('d1/d2', 'nd1/d2'); 74 | expect('d1/d2').to.not.satisfy(vio.dirExists); 75 | expect('nd1/d2').to.satisfy(vio.dirExists); 76 | }); 77 | 78 | it('dirs to delete should be updated', () => { 79 | vio.mkdir('d1/d2'); 80 | expect('d1/d2').to.satisfy(vio.dirExists); 81 | vio.del('d1/d2'); 82 | vio.moveDir('d1/d2', 'nd1/d2'); 83 | expect('d1/d2').to.not.satisfy(vio.dirExists); 84 | expect('nd1/d2').to.not.satisfy(vio.dirExists); 85 | }); 86 | }); 87 | 88 | describe('ls', () => { 89 | paths.setProjectRoot(path.join(__dirname, './test-prj')); 90 | it('list files under src/common', () => { 91 | vio.save('src/common/aaa.js', 'aaa.js'); 92 | vio.del('src/common/routeConfig.js'); 93 | vio.del('src/common/rootReducer.js'); 94 | const res = vio.ls('src/common'); 95 | expect(res.length).to.equal(2); 96 | }); 97 | }); 98 | 99 | describe('move file/dir in disk folder', () => { 100 | paths.setProjectRoot(path.join(__dirname, './test-prj')); 101 | it('file exists after move', () => { 102 | vio.move('src/common/rootReducer.js', 'src/common/rootReducer2.js'); 103 | expect(vio.fileExists('src/common/rootReducer2.js')).to.equal(true); 104 | }); 105 | 106 | it('file exists after move dir', () => { 107 | vio.moveDir('src/common', 'src/common2'); 108 | expect(vio.fileExists('src/common2/rootReducer.js')).to.equal(true); 109 | }); 110 | 111 | it('file content exists after move dir', () => { 112 | vio.moveDir('src/common', 'src/common2'); 113 | expect(vio.getContent('src/common2/rootReducer.js')).to.exist; 114 | }); 115 | }); 116 | }); 117 | --------------------------------------------------------------------------------