Skip to content

Commit f20391a

Browse files
Imlement cli commands for react (#63)
* Implement react * Small refactoring * Improves and add command 'add view' * Refactoring * Fix lint * Add prompts and refactoring * Fixes and refactoring * Update template * Refactoring * Update * Add new line * Add new line * Refactoring * Update template * Refactoring * Rename layout
1 parent 72e9f72 commit f20391a

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

47 files changed

+1932
-2
lines changed

.eslintignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/templates

commands/application.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const angularApplication = require('./application.angular');
2+
const reactApplication = require('./application.react');
23
const printHelp = require('./help').printHelp;
34

45
const isApplicationCommand = (command) => {
@@ -18,6 +19,11 @@ const run = (commands, options, devextremeConfig) => {
1819
return;
1920
}
2021

22+
if(commands[1] === 'react-app') {
23+
reactApplication.create(commands[2] || 'my-app', options);
24+
return;
25+
}
26+
2127
console.error(`The '${commands[1]}' application type is not valid`);
2228
printHelp(commands[0]);
2329
} else {
@@ -27,6 +33,11 @@ const run = (commands, options, devextremeConfig) => {
2733
return;
2834
}
2935

36+
if(commands[1] === 'devextreme-react') {
37+
reactApplication.install(options);
38+
return;
39+
}
40+
3041
if(commands[1] === 'angular-template') {
3142
angularApplication.addTemplate(commands[2], options);
3243
return;
@@ -39,6 +50,13 @@ const run = (commands, options, devextremeConfig) => {
3950
console.error('Invalid command');
4051
printHelp(commands[0]);
4152
}
53+
} else if(devextremeConfig.applicationEngine === 'react') {
54+
if(commands[1] === 'view') {
55+
reactApplication.addView(commands[2], options);
56+
} else {
57+
console.error('Invalid command');
58+
printHelp(commands[0]);
59+
}
4260
} else {
4361
console.log('The DevExtreme application cannot be found');
4462
}

commands/application.react.js

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
const runCommand = require('../utility/run-command');
2+
const path = require('path');
3+
const fs = require('fs');
4+
const runPrompts = require('../utility/prompts');
5+
const templateCreator = require('../utility/template-creator');
6+
const packageJsonUtils = require('../utility/package-json-utils');
7+
const modifyJson = require('../utility/modify-json-file');
8+
const insertItemToArray = require('../utility/file-content').insertItemToArray;
9+
const moduleUtils = require('../utility/module');
10+
const stringUtils = require('../utility/string');
11+
const pathToPagesIndex = path.join(process.cwd(), 'src', 'pages', 'index.js');
12+
const defaultStyles = [
13+
'devextreme/dist/css/dx.light.css',
14+
'devextreme/dist/css/dx.common.css'
15+
];
16+
const layouts = [
17+
{ fullName: 'side-nav-outer-toolbar', title: 'Side navigation (outer toolbar)', value: 'SideNavOuterToolbar' },
18+
{ fullName: 'side-nav-inner-toolbar', title: 'Side navigation (inner toolbar)', value: 'SideNavInnerToolbar' }
19+
];
20+
21+
const addDevextremeToPackageJson = (appPath, dxversion) => {
22+
const packagePath = path.join(appPath, 'package.json');
23+
const depends = [
24+
{ name: 'devextreme', version: dxversion },
25+
{ name: 'devextreme-react', version: dxversion }
26+
];
27+
28+
packageJsonUtils.addDependencies(packagePath, depends);
29+
};
30+
31+
const preparePackageJsonForTemplate = (packagePath, appName) => {
32+
const depends = [
33+
{ name: 'node-sass', version: '^4.11.0' },
34+
{ name: 'react-router-dom', version: '^5.0.0' }
35+
];
36+
const devDepends = [
37+
{ name: 'devextreme-cli', version: 'latest' }
38+
];
39+
const scripts = [
40+
{ name: 'build-themes', value: 'devextreme build' },
41+
{ name: 'postinstall', value: 'npm run build-themes' }
42+
];
43+
44+
packageJsonUtils.addDependencies(packagePath, depends);
45+
packageJsonUtils.addDependencies(packagePath, devDepends, 'dev');
46+
packageJsonUtils.updateScripts(packagePath, scripts);
47+
updateJsonPropName(packagePath, appName);
48+
};
49+
50+
const updateJsonPropName = (path, name) => {
51+
modifyJson(path, content => {
52+
content.name = name;
53+
54+
return content;
55+
});
56+
};
57+
58+
const getLayout = (options) => {
59+
const currentLayout = layouts.filter((layout) => {
60+
return layout.fullName === options.layout;
61+
});
62+
63+
return currentLayout.length ? [currentLayout[0].value] : undefined;
64+
};
65+
66+
const create = (appName, options) => {
67+
const commandArguments = ['create-react-app', appName];
68+
const prompts = [
69+
{
70+
type: 'select',
71+
name: 'layout',
72+
message: 'What layout do you want to add?',
73+
choices: layouts
74+
}
75+
];
76+
77+
runPrompts(options, prompts, getLayout).then((promptsResult) => {
78+
runCommand('npx', commandArguments).then(() => {
79+
const appPath = path.join(process.cwd(), appName);
80+
const humanizedName = stringUtils.humanize(appName);
81+
const templateOptions = Object.assign({}, options, {
82+
project: humanizedName,
83+
skipFolder: options.empty ? 'pages' : '',
84+
layout: promptsResult.layout
85+
});
86+
modifyIndexHtml(appPath, humanizedName);
87+
addTemplate(appPath, appName, templateOptions);
88+
});
89+
});
90+
};
91+
92+
const modifyIndexHtml = (appPath, appName) => {
93+
const indexHtmlPath = path.join(appPath, 'public', 'index.html');
94+
let htmlContent = fs.readFileSync(indexHtmlPath).toString();
95+
96+
htmlContent = htmlContent.replace(/<title>(\w+\s*)+<\/title>/, `<title>${appName}<\/title>`);
97+
htmlContent = htmlContent.replace('<body>', '<body class="dx-viewport">');
98+
fs.writeFileSync(indexHtmlPath, htmlContent);
99+
};
100+
101+
const addTemplate = (appPath, appName, templateOptions) => {
102+
const templateSourcePath = path.join(__dirname, '..', 'templates', 'react', 'application');
103+
const packagePath = path.join(appPath, 'package.json');
104+
const manifestPath = path.join(appPath, 'public', 'manifest.json');
105+
const styles = [
106+
'devextreme/dist/css/dx.common.css',
107+
'./themes/generated/theme.additional.css',
108+
'./themes/generated/theme.base.css'];
109+
110+
templateCreator.moveTemplateFilesToProject(templateSourcePath, appPath, templateOptions);
111+
preparePackageJsonForTemplate(packagePath, appName);
112+
updateJsonPropName(manifestPath, appName);
113+
install({}, appPath, styles);
114+
};
115+
116+
const install = (options, appPath, styles) => {
117+
appPath = appPath ? appPath : process.cwd();
118+
const pathToMainComponent = path.join(appPath, 'src', 'App.js');
119+
addStylesToApp(pathToMainComponent, styles || defaultStyles);
120+
addDevextremeToPackageJson(appPath, options.dxversion || 'latest');
121+
122+
runCommand('npm', ['install'], { cwd: appPath });
123+
};
124+
125+
const addStylesToApp = (filePath, styles) => {
126+
styles.forEach(style => {
127+
moduleUtils.insertImport(filePath, style);
128+
});
129+
};
130+
131+
const getComponentPageName = (viewName) => {
132+
return `${stringUtils.classify(viewName)}Page`;
133+
};
134+
135+
const getNavigationData = (viewName, componentName, icon) => {
136+
const pagePath = stringUtils.dasherize(viewName);
137+
return {
138+
route: `\n{\n path: \'/${pagePath}\',\n component: ${componentName}\n }`,
139+
navigation: `{\n text: \'${stringUtils.humanize(viewName)}\',\n path: \'/${pagePath}\',\n icon: \'${icon}\'\n }`
140+
};
141+
};
142+
143+
const createPathToPage = (pageName) => {
144+
const pagesPath = path.join(process.cwd(), 'src', 'pages');
145+
const newPagePath = path.join(pagesPath, pageName);
146+
147+
if(!fs.existsSync(pagesPath)) {
148+
fs.mkdirSync(pagesPath);
149+
createPagesIndex();
150+
}
151+
152+
if(fs.existsSync(newPagePath)) {
153+
console.error('The page already exists');
154+
process.exit();
155+
}
156+
157+
fs.mkdirSync(newPagePath);
158+
159+
return newPagePath;
160+
};
161+
162+
const createPagesIndex = () => {
163+
fs.writeFileSync(pathToPagesIndex, '');
164+
};
165+
166+
const addView = (pageName, options) => {
167+
const componentName = getComponentPageName(pageName);
168+
const pathToPage = createPathToPage(pageName);
169+
const pageTemplatePath = path.join(__dirname, '..', 'templates', 'react', 'page');
170+
const routingModulePath = path.join(process.cwd(), 'src', 'app-routes.js');
171+
const navigationModulePath = path.join(process.cwd(), 'src', 'app-navigation.js');
172+
const navigationData = getNavigationData(pageName, componentName, options && options.icon || 'home');
173+
174+
templateCreator.addPageToApp(pageName, pathToPage, pageTemplatePath);
175+
moduleUtils.insertExport(pathToPagesIndex, componentName, `./${pageName}/${pageName}`);
176+
moduleUtils.insertImport(routingModulePath, './pages', componentName);
177+
insertItemToArray(routingModulePath, navigationData.route);
178+
insertItemToArray(navigationModulePath, navigationData.navigation);
179+
};
180+
181+
module.exports = {
182+
install,
183+
create,
184+
addTemplate,
185+
addView
186+
};

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "devextreme-cli",
3-
"version": "1.0.3",
3+
"version": "1.1.0-beta.0",
44
"description": "DevExtreme CLI",
55
"keywords": [
66
"devexpress",
@@ -20,10 +20,12 @@
2020
"repository": "https://github.com/DevExpress/devextreme-cli",
2121
"license": "MIT",
2222
"dependencies": {
23-
"devextreme-themebuilder": "^18.2.5",
23+
"devextreme-themebuilder": "^18.2.5 || >=19.1.1-pre-19058",
2424
"less": "^3.8.1",
2525
"minimist": "^1.2.0",
26+
"mustache": "^3.0.1",
2627
"node-sass": "^4.9.3",
28+
"prompts": "^2.0.4",
2729
"semver": "^5.6.0"
2830
},
2931
"devDependencies": {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"applicationEngine": "react",
3+
"build": {
4+
"commands": [
5+
{
6+
"command": "build-theme",
7+
"options": {
8+
"inputFile": "src/themes/metadata.base.json",
9+
"outputFile": "src/themes/generated/theme.base.css"
10+
}
11+
},
12+
{
13+
"command": "build-theme",
14+
"options": {
15+
"inputFile": "src/themes/metadata.additional.json",
16+
"outputFile": "src/themes/generated/theme.additional.css"
17+
}
18+
},
19+
{
20+
"command": "export-theme-vars",
21+
"options": {
22+
"inputFile": "src/themes/metadata.base.json",
23+
"outputFile": "src/themes/generated/variables.base.scss"
24+
}
25+
},
26+
{
27+
"command": "export-theme-vars",
28+
"options": {
29+
"inputFile": "src/themes/metadata.additional.json",
30+
"outputFile": "src/themes/generated/variables.additional.scss"
31+
}
32+
}
33+
]
34+
}
35+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import React, { Component } from 'react';
2+
import { HashRouter as Router, Redirect, Route, Switch } from 'react-router-dom';
3+
import appInfo from './app-info';
4+
import { navigation } from './app-navigation';
5+
import routes from './app-routes';
6+
import './App.scss';
7+
import './dx-styles.scss';
8+
import { Footer, LoginForm } from './components';
9+
import {
10+
<%=layout%> as SideNavBarLayout,
11+
SingleCard
12+
} from './layouts';
13+
import { sizes, subscribe, unsubscribe } from './utils/media-query';
14+
15+
const LoginContainer = ({ logIn }) => <LoginForm onLoginClick={logIn} />;
16+
17+
const NotAuthPage = (props) => (
18+
<SingleCard>
19+
<Route render={() => <LoginContainer {...props} />} />
20+
</SingleCard>
21+
);
22+
23+
const AuthPage = (props) => (
24+
<SideNavBarLayout menuItems={navigation} title={appInfo.title} {...props}>
25+
<Switch>
26+
{routes.map(item => (
27+
<Route
28+
exact
29+
key={item.path}
30+
path={item.path}
31+
component={item.component}
32+
/>
33+
))}<%=^empty%>
34+
<Redirect to={'/home'} /><%=/empty%>
35+
</Switch>
36+
<Footer>
37+
Copyright © 2011-2019 Developer Express Inc.
38+
<br />
39+
All trademarks or registered trademarks are property of their
40+
respective owners.
41+
</Footer>
42+
</SideNavBarLayout>
43+
);
44+
45+
class App extends Component {
46+
constructor(props) {
47+
super(props);
48+
49+
this.state = {
50+
loggedIn: true,
51+
screenSizeClass: this.getScreenSizeClass()
52+
};
53+
54+
this.userMenuItems = [
55+
{
56+
text: 'Profile',
57+
icon: 'user'
58+
},
59+
{
60+
text: 'Logout',
61+
icon: 'runner',
62+
onClick: this.logOut
63+
}
64+
];
65+
}
66+
67+
componentDidMount() {
68+
subscribe(this.screenSizeChanged);
69+
}
70+
71+
componentWillUnmount() {
72+
unsubscribe(this.screenSizeChanged);
73+
}
74+
75+
render() {
76+
const { loggedIn } = this.state;
77+
78+
return (
79+
<div className={`app ${this.state.screenSizeClass}`}>
80+
<Router>{loggedIn ? <AuthPage userMenuItems={this.userMenuItems} /> : <NotAuthPage logIn={this.logIn} />}</Router>
81+
</div>
82+
);
83+
}
84+
85+
getScreenSizeClass() {
86+
const screenSizes = sizes();
87+
return Object.keys(screenSizes).filter(cl => screenSizes[cl]).join(' ');
88+
}
89+
90+
screenSizeChanged = () => {
91+
this.setState({
92+
screenSizeClass: this.getScreenSizeClass()
93+
});
94+
}
95+
96+
logIn = () => {
97+
this.setState({ loggedIn: true });
98+
};
99+
100+
logOut = () => {
101+
this.setState({ loggedIn: false });
102+
};
103+
}
104+
105+
export default App;

0 commit comments

Comments
 (0)